-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f5b418d
commit db8bb4f
Showing
35 changed files
with
15,180 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
PORT=0 | ||
HOST="127.0.0.1" | ||
NODE_ENV="test" | ||
JOB_THROTTLE=100 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -128,3 +128,7 @@ dist | |
.yarn/build-state.yml | ||
.yarn/install-state.gz | ||
.pnp.* | ||
.DS_Store | ||
test-jobs/* | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
module.exports = { | ||
|
||
extensions: ["ts"], | ||
|
||
spec: ["./test/**/*.test.ts",], | ||
|
||
timeout: 15000, | ||
checkLeaks: true, | ||
allowUncaught: true, | ||
jobs: 1, | ||
parallel: false, | ||
retries: 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
engine-strict=true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
20 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"cSpell.words": [ | ||
"Pfannerstill", | ||
"Rosalina" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,34 @@ | ||
# bulk-match-provider | ||
Bulk Match Provider | ||
|
||
|
||
## Matching | ||
|
||
Patients can either be matched by identifier (SSN and MRN implemented), in which | ||
case we have a "hard" match with 100% confidence, of by fuzzy matching combination | ||
of parameter. The minimal required set of parameters to match includes `name` and | ||
`birthDate` and provides a confidence of `0.6` (60%). Any additionally matched | ||
parameter will further increase the confidence score. | ||
|
||
|
||
### Comparing Names | ||
1. We compare flattened versions of [human names](https://hl7.org/fhir/R4/datatypes.html#HumanName) | ||
consisting of `given` and `family` name values concatenated with a space character. | ||
2. Our comparison is case-insensitive | ||
3. We ignore the `use`, `text`, `prefix`, `suffix`, and `period` properties if present. | ||
4. The input resource and the source Patient that we compare with can both have multiple | ||
names. The match is successful only if the re is one (or more) overlapping name | ||
between the two name arrays. | ||
|
||
Note that having a matching name is not sufficient match a patient and needs to | ||
be combined with other matched properties. | ||
|
||
|
||
### Comparing DOB | ||
|
||
We compare birth dates with a **day precision**, meaning that we consider two birth | ||
dates equal if they represent the same year month and day, ignoring the time part (if any). | ||
Note that in FHIR the [birthDate](https://hl7.org/fhir/R4/patient-definitions.html#Patient.birthDate) | ||
can also be loosely specified as `YYYY` or `YYYY-MM`. This means that, for example, | ||
we would match all of the following dates as equal: `2020`, `2020-01`, `2020-01-01`, `2020-01-01T10:12:34` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
import { FormEvent, useReducer } from "react" | ||
import PresetSelector from "./PresetSelector" | ||
import type { Preset } from "./presets" | ||
import Collapse from "./Collapse" | ||
import Status from "./Status" | ||
import MatchRequest from "./MatchRequest" | ||
import MatchResults from "./MatchResults" | ||
import { MatchManifest } from ".." | ||
|
||
|
||
interface State { | ||
error : Error | null | ||
matchRequest: { | ||
loading : boolean | ||
baseUrl : string | ||
onlySingleMatch : boolean | ||
onlyCertainMatches: boolean | ||
count ?: number | ||
_outputFormat ?: string | ||
resources : string//(Partial<fhir4.Patient>)[] | ||
submittedAt ?: number | ||
} | ||
matchResponse: { | ||
statusHeading: string, | ||
text: string | ||
} | ||
statusURL: string | ||
statusResponses: { | ||
statusHeading: string, | ||
text: string | ||
}[] | ||
manifest?: MatchManifest | null | ||
snippet: Preset | null | ||
} | ||
|
||
const initialState: State = { | ||
error : null, | ||
snippet : null, | ||
matchRequest : { | ||
loading : false, | ||
baseUrl : "http://127.0.0.1:3456/fhir/", | ||
onlySingleMatch : false, | ||
onlyCertainMatches: false, | ||
count : 0, | ||
submittedAt : 0, | ||
resources : `[]` | ||
}, | ||
matchResponse: { | ||
statusHeading: "", | ||
text: "" | ||
}, | ||
statusURL : "", | ||
statusResponses: [], | ||
manifest : null | ||
|
||
} | ||
|
||
function reducer(state: State, payload: Partial<State>): State { | ||
return { ...state, ...payload }; | ||
} | ||
|
||
|
||
|
||
export default function App() { | ||
|
||
const [state, dispatch] = useReducer(reducer, initialState); | ||
|
||
async function sendMatchRequest(e: FormEvent) { | ||
|
||
e.preventDefault() | ||
|
||
const { | ||
matchRequest: { | ||
baseUrl, | ||
resources, | ||
onlySingleMatch, | ||
onlyCertainMatches, | ||
_outputFormat, | ||
count | ||
} | ||
} = state | ||
|
||
const url = new URL("Patient/$bulk-match", baseUrl + "") | ||
|
||
const body: fhir4.Parameters = { | ||
resourceType: "Parameters", | ||
parameter: [] | ||
} | ||
|
||
try { | ||
var patients = JSON.parse(resources + "") | ||
} catch (error) { | ||
return dispatch({ error: error as Error }) | ||
} | ||
|
||
if (patients) { | ||
patients.forEach((p: any) => body.parameter!.push({ name: "resource", resource: p })) | ||
} | ||
|
||
body.parameter!.push({ name: "onlySingleMatch" , valueBoolean: onlySingleMatch }) | ||
body.parameter!.push({ name: "onlyCertainMatches", valueBoolean: onlyCertainMatches }) | ||
|
||
if (count) { | ||
body.parameter!.push({ name: "count", valueInteger: count }) | ||
} | ||
|
||
if (_outputFormat) { | ||
body.parameter!.push({ name: "_outputFormat", valueString: _outputFormat }) | ||
} | ||
|
||
dispatch({ error: null, matchRequest: { ...state.matchRequest, loading: true } }) | ||
|
||
try { | ||
const res = await fetch(url, { | ||
method : "POST", | ||
body: JSON.stringify(body), | ||
headers: { | ||
"Content-Type": "application/json", | ||
accept: "application/fhir+ndjson" | ||
} | ||
}) | ||
|
||
const json = await res.json() | ||
|
||
let txt = [] | ||
res.headers.forEach((value, key) => { | ||
txt.push(`${key}: ${value}\n`) | ||
}) | ||
txt.push("\n") | ||
txt.push(JSON.stringify(json, null, 4)) | ||
|
||
dispatch({ | ||
matchResponse: { | ||
statusHeading: res.status + " " + res.statusText, | ||
text: txt.join("") | ||
}, | ||
matchRequest: { | ||
...state.matchRequest, | ||
loading: false, | ||
submittedAt: Date.now() | ||
}, | ||
statusURL: res.headers.get("content-location") || "" | ||
}) | ||
|
||
} catch (ex) { | ||
dispatch({ error: ex as Error }) | ||
} | ||
} | ||
|
||
return ( | ||
<> | ||
<nav className="navbar sticky-top navbar-expand-lg bg-primary"> | ||
<div className="container"> | ||
<a className="navbar-brand text-white" href="/"> | ||
<i className="bi bi-fire me-1" /> | ||
Bulk-Match <small className="opacity-50">Sample App</small> | ||
</a> | ||
<button | ||
className="navbar-toggler" | ||
type="button" | ||
data-bs-toggle="collapse" | ||
data-bs-target="#navbarSupportedContent" | ||
aria-controls="navbarSupportedContent" | ||
aria-expanded="false" | ||
aria-label="Toggle navigation"> | ||
<span className="navbar-toggler-icon"></span> | ||
</button> | ||
<div className="collapse navbar-collapse" id="navbarSupportedContent"> | ||
<ul className="navbar-nav me-auto"></ul> | ||
<PresetSelector value={state.snippet} onChange={s => { | ||
const resources = JSON.stringify(s?.params.resources || [], null, 2) | ||
dispatch({ | ||
...initialState, | ||
matchRequest: { | ||
...initialState.matchRequest, | ||
// loading: false, | ||
// baseUrl : initialState.matchRequest.baseUrl, | ||
onlySingleMatch : s?.params.onlySingleMatch ?? initialState.matchRequest.onlySingleMatch, | ||
onlyCertainMatches: s?.params.onlyCertainMatches ?? initialState.matchRequest.onlyCertainMatches, | ||
count : s?.params.count ?? initialState.matchRequest.count, | ||
resources | ||
}, | ||
snippet: s, | ||
// error: null, | ||
// manifest: undefined, | ||
// matchResponse: null | ||
}) | ||
}} /> | ||
</div> | ||
</div> | ||
</nav> | ||
<div className="container my-3"> | ||
<Collapse header={ <h5 className="m-0">Bulk-Match Request</h5> } open> | ||
<MatchRequest | ||
state={{ | ||
baseUrl : state.matchRequest.baseUrl, | ||
onlyCertainMatches: state.matchRequest.onlyCertainMatches, | ||
onlySingleMatch : state.matchRequest.onlySingleMatch, | ||
count : state.matchRequest.count, | ||
resources : state.matchRequest.resources, | ||
}} | ||
onChange={p => dispatch({ matchRequest: { ...state.matchRequest, ...p }})} | ||
onSubmit={sendMatchRequest} | ||
/> | ||
</Collapse> | ||
{ state.matchResponse.statusHeading && | ||
<Collapse header={ <h5 className="m-0">Bulk-Match Response</h5> }> | ||
<hr className="my-1" /> | ||
{ | ||
state.matchRequest.loading ? | ||
"Loading..." : | ||
<> | ||
<div className="row"> | ||
<div className="col"><b>{state.matchResponse.statusHeading}</b></div> | ||
</div> | ||
<div className="row mb-4"> | ||
<div className="col"> | ||
<pre>{state.matchResponse.text}</pre> | ||
</div> | ||
</div> | ||
</> | ||
} | ||
</Collapse> | ||
} | ||
{ state.statusURL && <Status | ||
statusURL={state.statusURL} | ||
key={"status-" + state.matchRequest.submittedAt} | ||
onComplete={ manifest => dispatch({ manifest }) } | ||
/> } | ||
{ state.manifest && <MatchResults manifest={state.manifest} key={"result-" + state.matchRequest.submittedAt} /> } | ||
</div> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { ReactNode, useState } from "react" | ||
|
||
|
||
export default function Collapse({ children, header, open }: { children?: ReactNode, header: ReactNode, open?: boolean }) { | ||
const [isOpen, setIsOpen] = useState(!!open) | ||
return ( | ||
<div> | ||
<div className="d-flex align-items-center mb-1" style={{ cursor: "pointer" }} onClick={() => setIsOpen(!isOpen)}> | ||
<i className={ isOpen ? "bi bi-caret-down-fill me-1" : "bi bi-caret-right-fill me-1" } /> | ||
{ header } | ||
</div> | ||
<div style={{ marginLeft: "1.3em" }}> | ||
{ isOpen && children } | ||
</div> | ||
</div> | ||
) | ||
} |
Oops, something went wrong.