@peopl-health/nexus 5.13.0-dev.1152 → 5.13.0-dev.1155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/fhir/helpers/resourceHelper.js +2 -2
- package/lib/fhir/projections/clusterProjection.js +113 -14
- package/lib/fhir/projections/contingencyProjection.js +1 -1
- package/lib/fhir/projections/interventionProjection.js +1 -1
- package/lib/fhir/projections/registerProjectors.js +0 -2
- package/lib/fhir/projections/routingProjection.js +1 -1
- package/lib/fhir/projections/symptomCaseProjection.js +1 -1
- package/lib/fhir/resources/ClinicalImpression.js +3 -48
- package/lib/fhir/resources/Provenance.js +1 -22
- package/lib/fhir/services/clusterService.js +12 -60
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ function baseResource(resourceType, id, slug, patientId) {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function provenance({ provenanceId, targetRefs, recordedAt,
|
|
14
|
+
function provenance({ provenanceId, targetRefs, recordedAt, activity, agentDevice = null, onBehalfOf = null }) {
|
|
15
15
|
return {
|
|
16
16
|
resourceType: 'Provenance',
|
|
17
17
|
id: fhirId(provenanceId),
|
|
@@ -21,7 +21,7 @@ function provenance({ provenanceId, targetRefs, recordedAt, activityText, agentD
|
|
|
21
21
|
who: reference('Device', agentDevice || getDeviceId()),
|
|
22
22
|
onBehalfOf: reference('Organization', onBehalfOf || getOrganizationId()),
|
|
23
23
|
}],
|
|
24
|
-
activity
|
|
24
|
+
activity,
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -1,24 +1,123 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { Provenance } = require('../resources/Provenance');
|
|
1
|
+
const { ZodError } = require('zod');
|
|
3
2
|
|
|
4
|
-
const
|
|
3
|
+
const { codeSystemUrl } = require('../helpers/fhirHelper');
|
|
4
|
+
const { identifier, reference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
|
|
5
|
+
const { baseResource, provenance } = require('../helpers/resourceHelper');
|
|
6
|
+
const { extMap, identifierValue, refToId } = require('../helpers/fhirReadHelper');
|
|
7
|
+
const { CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG } = require('../constants/projectionSlugs');
|
|
8
|
+
const { CLUSTER_EXT } = require('../constants/extensionSlugs');
|
|
9
|
+
const { ClusterImpression } = require('../../shared/dtos/ClusterImpression');
|
|
10
|
+
const { logger } = require('../../utils/logger');
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
const LIST_SEPARATOR = '; ';
|
|
13
|
+
const STATUS_TO_FHIR = { active: 'in-progress', dissolved: 'completed' };
|
|
14
|
+
const ACTIVITY_CODESYSTEM = 'clinical-activity';
|
|
15
|
+
const ACTIVITY_CODES = {
|
|
16
|
+
opened: 'cluster-hypothesis-opened',
|
|
17
|
+
updated: 'cluster-hypothesis-updated',
|
|
18
|
+
dissolved: 'cluster-hypothesis-dissolved',
|
|
19
|
+
};
|
|
20
|
+
const DEFAULT_ACTIVITY_CODE = 'cluster-hypothesis';
|
|
21
|
+
|
|
22
|
+
function toFhir({ cluster, history = [] }) {
|
|
8
23
|
const records = history.length
|
|
9
24
|
? [...history].sort((a, b) => Date.parse(a.recordedAt) - Date.parse(b.recordedAt))
|
|
10
25
|
: [{ turnId: 'opened', action: 'opened', recordedAt: cluster.openedAt, transitionReason: null }];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
return [clusterResource(cluster), ...records.map((record) => historyProvenance(cluster.clusterId, record))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clusterResource(cluster) {
|
|
30
|
+
const payload = {
|
|
31
|
+
...baseResource('ClinicalImpression', cluster.clusterId, CLUSTER_IMPRESSION_SLUG, cluster.patientId),
|
|
32
|
+
status: STATUS_TO_FHIR[cluster.status],
|
|
33
|
+
problem: cluster.memberCaseIds.map((caseId) => reference('Condition', caseId)),
|
|
34
|
+
extension: clusterExtensions(cluster),
|
|
35
|
+
};
|
|
36
|
+
if (cluster.label) payload.identifier.push(identifier(CLUSTER_LABEL_SLUG, cluster.label));
|
|
37
|
+
if (cluster.relationshipModel) payload.summary = cluster.relationshipModel;
|
|
38
|
+
if (cluster.openingReasoning) payload.description = cluster.openingReasoning;
|
|
39
|
+
if (cluster.dissolvedAt) payload.effectivePeriod = { start: toIso(cluster.openedAt), end: toIso(cluster.dissolvedAt) };
|
|
40
|
+
else payload.effectiveDateTime = toIso(cluster.openedAt);
|
|
41
|
+
if (cluster.dissolveCategory) payload.statusReason = codeableConcept(cluster.dissolveCategory);
|
|
42
|
+
return payload;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clusterExtensions(cluster) {
|
|
46
|
+
const out = [
|
|
47
|
+
extension(CLUSTER_EXT.HYPOTHESIS_TYPE, { valueString: cluster.hypothesisType }),
|
|
48
|
+
extension(CLUSTER_EXT.CONFIDENCE, { valueString: cluster.confidence }),
|
|
49
|
+
extension(CLUSTER_EXT.CLINICAL_SIGNIFICANCE, { valueString: cluster.clinicalSignificance }),
|
|
50
|
+
];
|
|
51
|
+
if (cluster.strengthensIf.length) {
|
|
52
|
+
out.push(extension(CLUSTER_EXT.STRENGTHENS_IF, { valueString: cluster.strengthensIf.join(LIST_SEPARATOR) }));
|
|
53
|
+
}
|
|
54
|
+
if (cluster.weakensIf.length) {
|
|
55
|
+
out.push(extension(CLUSTER_EXT.WEAKENS_IF, { valueString: cluster.weakensIf.join(LIST_SEPARATOR) }));
|
|
56
|
+
}
|
|
57
|
+
out.push(extension(CLUSTER_EXT.STATUS, { valueString: cluster.status }));
|
|
58
|
+
if (cluster.consultationId) out.push(extension(CLUSTER_EXT.CONSULTATION_ID, { valueString: cluster.consultationId }));
|
|
59
|
+
if (cluster.dissolveReason) out.push(extension(CLUSTER_EXT.DISSOLVE_REASON, { valueString: cluster.dissolveReason }));
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function historyProvenance(clusterId, record) {
|
|
64
|
+
const code = ACTIVITY_CODES[record.action] || DEFAULT_ACTIVITY_CODE;
|
|
65
|
+
const payload = provenance({
|
|
66
|
+
provenanceId: `${clusterId}-prov-${record.turnId}-${record.action}`,
|
|
67
|
+
targetRefs: [reference('ClinicalImpression', clusterId)],
|
|
15
68
|
recordedAt: record.recordedAt,
|
|
16
|
-
|
|
17
|
-
})
|
|
18
|
-
|
|
69
|
+
activity: codeableConcept(code, { code, system: codeSystemUrl(ACTIVITY_CODESYSTEM) }),
|
|
70
|
+
});
|
|
71
|
+
if (record.transitionReason) {
|
|
72
|
+
payload.extension = [extension('transition-reason', { valueString: record.transitionReason })];
|
|
73
|
+
}
|
|
74
|
+
return payload;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fromFhir(resources, { patientId }) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const resource of resources) {
|
|
80
|
+
if (resource.resourceType !== 'ClinicalImpression') continue;
|
|
81
|
+
if (!identifierValue(resource, CLUSTER_IMPRESSION_SLUG)) continue;
|
|
82
|
+
try {
|
|
83
|
+
out.push(new ClusterImpression(reconstruct(resource, patientId)));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof ZodError) {
|
|
86
|
+
logger.warn({ clusterId: resource.id }, 'skipping malformed cluster ClinicalImpression');
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function reconstruct(resource, patientId) {
|
|
96
|
+
const ext = extMap(resource);
|
|
97
|
+
const splitList = (value) => (value ? value.split(LIST_SEPARATOR) : []);
|
|
98
|
+
return {
|
|
99
|
+
schemaVersion: '1',
|
|
100
|
+
clusterId: identifierValue(resource, CLUSTER_IMPRESSION_SLUG),
|
|
101
|
+
patientId,
|
|
102
|
+
label: identifierValue(resource, CLUSTER_LABEL_SLUG) || '',
|
|
103
|
+
hypothesisType: ext[CLUSTER_EXT.HYPOTHESIS_TYPE],
|
|
104
|
+
status: ext[CLUSTER_EXT.STATUS],
|
|
105
|
+
confidence: ext[CLUSTER_EXT.CONFIDENCE],
|
|
106
|
+
clinicalSignificance: ext[CLUSTER_EXT.CLINICAL_SIGNIFICANCE],
|
|
107
|
+
memberCaseIds: (resource.problem || []).map((problemRef) => refToId(problemRef.reference)).filter(Boolean),
|
|
108
|
+
consultationId: ext[CLUSTER_EXT.CONSULTATION_ID] || null,
|
|
109
|
+
relationshipModel: resource.summary || '',
|
|
110
|
+
strengthensIf: splitList(ext[CLUSTER_EXT.STRENGTHENS_IF]),
|
|
111
|
+
weakensIf: splitList(ext[CLUSTER_EXT.WEAKENS_IF]),
|
|
112
|
+
openingReasoning: resource.description || '',
|
|
113
|
+
openedAt: resource.effectiveDateTime || resource.effectivePeriod?.start,
|
|
114
|
+
dissolvedAt: resource.effectivePeriod?.end || null,
|
|
115
|
+
dissolveReason: ext[CLUSTER_EXT.DISSOLVE_REASON] || null,
|
|
116
|
+
dissolveCategory: resource.statusReason?.text || null,
|
|
117
|
+
};
|
|
19
118
|
}
|
|
20
119
|
|
|
21
120
|
module.exports = {
|
|
22
|
-
|
|
23
|
-
|
|
121
|
+
toFhir,
|
|
122
|
+
fromFhir,
|
|
24
123
|
};
|
|
@@ -34,7 +34,7 @@ function toFhir(intervention) {
|
|
|
34
34
|
provenanceId: `${intervention.interventionId}-prov`,
|
|
35
35
|
targetRefs: [{ reference: `${primary.resourceType}/${fhirId(intervention.interventionId)}` }],
|
|
36
36
|
recordedAt: intervention.deliveredAt,
|
|
37
|
-
|
|
37
|
+
activity: codeableConcept(ACTIVITY_TEXT),
|
|
38
38
|
})];
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -5,7 +5,6 @@ const { projectReminder, PROJECTOR_NAME: REMINDER_PROJECTOR_NAME } = require('./
|
|
|
5
5
|
const { projectPatientRisk, PROJECTOR_NAME: RISK_PROJECTOR_NAME } = require('./riskProjection');
|
|
6
6
|
const { projectSafetyGate, PROJECTOR_NAME: SAFETY_GATE_PROJECTOR_NAME } = require('./safetyGateProjection');
|
|
7
7
|
const { projectRecommendationPlan, PROJECTOR_NAME: RECOMMENDATION_PLAN_PROJECTOR_NAME } = require('./recommendationPlanProjection');
|
|
8
|
-
const { projectCluster, PROJECTOR_NAME: CLUSTER_PROJECTOR_NAME } = require('./clusterProjection');
|
|
9
8
|
const { projectPalliativeAssessment, PROJECTOR_NAME: PALLIATIVE_PROJECTOR_NAME } = require('./palliativeProjection');
|
|
10
9
|
const { projectPalliativeCarePlan, PROJECTOR_NAME: PALLIATIVE_CAREPLAN_PROJECTOR_NAME } = require('./palliativeCarePlanProjection');
|
|
11
10
|
|
|
@@ -16,7 +15,6 @@ const PROJECTORS = [
|
|
|
16
15
|
{ name: RISK_PROJECTOR_NAME, project: projectPatientRisk },
|
|
17
16
|
{ name: SAFETY_GATE_PROJECTOR_NAME, project: projectSafetyGate },
|
|
18
17
|
{ name: RECOMMENDATION_PLAN_PROJECTOR_NAME, project: projectRecommendationPlan },
|
|
19
|
-
{ name: CLUSTER_PROJECTOR_NAME, project: projectCluster },
|
|
20
18
|
{ name: PALLIATIVE_PROJECTOR_NAME, project: projectPalliativeAssessment },
|
|
21
19
|
{ name: PALLIATIVE_CAREPLAN_PROJECTOR_NAME, project: projectPalliativeCarePlan },
|
|
22
20
|
];
|
|
@@ -128,7 +128,7 @@ function toFhir(routingDecision) {
|
|
|
128
128
|
reference(dispositionResource.resourceType, disposition.dispositionId),
|
|
129
129
|
],
|
|
130
130
|
recordedAt: prov.recordedAt,
|
|
131
|
-
|
|
131
|
+
activity: codeableConcept(ACTIVITY_TEXT),
|
|
132
132
|
agentDevice: prov.agentDevice,
|
|
133
133
|
onBehalfOf: prov.onBehalfOf,
|
|
134
134
|
}),
|
|
@@ -41,7 +41,7 @@ function toFhir(managedSymptom) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function caseProvenance(anchorId, recordedAt, targetRefs) {
|
|
44
|
-
return provenance({ provenanceId: `${anchorId}-prov`, targetRefs, recordedAt,
|
|
44
|
+
return provenance({ provenanceId: `${anchorId}-prov`, targetRefs, recordedAt, activity: codeableConcept(ACTIVITY_TEXT) });
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function assessmentResources(assessment, placeholderIds) {
|
|
@@ -1,56 +1,13 @@
|
|
|
1
1
|
const { fhirId } = require('../helpers/fhirHelper');
|
|
2
|
-
const { identifier,
|
|
3
|
-
const {
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
const CLUSTER_STATUS_TO_FHIR = { active: 'in-progress', dissolved: 'completed' };
|
|
7
|
-
const CLUSTER_LIST_SEPARATOR = '; ';
|
|
2
|
+
const { identifier, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
|
|
3
|
+
const { RECOMMENDATION_ELEMENTS_SLUG, PALLIATIVE_ASSESSMENT_SLUG } = require('../constants/projectionSlugs');
|
|
4
|
+
const { PALLIATIVE_EXT } = require('../constants/extensionSlugs');
|
|
8
5
|
|
|
9
6
|
class ClinicalImpression {
|
|
10
7
|
constructor(payload) {
|
|
11
8
|
Object.assign(this, payload);
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
static fromClusterImpression({ cluster }) {
|
|
15
|
-
const epistemicExts = [
|
|
16
|
-
extension(CLUSTER_EXT.HYPOTHESIS_TYPE, { valueString: cluster.hypothesisType }),
|
|
17
|
-
extension(CLUSTER_EXT.CONFIDENCE, { valueString: cluster.confidence }),
|
|
18
|
-
extension(CLUSTER_EXT.CLINICAL_SIGNIFICANCE, { valueString: cluster.clinicalSignificance }),
|
|
19
|
-
];
|
|
20
|
-
if (cluster.strengthensIf.length) {
|
|
21
|
-
epistemicExts.push(extension(CLUSTER_EXT.STRENGTHENS_IF, { valueString: cluster.strengthensIf.join(CLUSTER_LIST_SEPARATOR) }));
|
|
22
|
-
}
|
|
23
|
-
if (cluster.weakensIf.length) {
|
|
24
|
-
epistemicExts.push(extension(CLUSTER_EXT.WEAKENS_IF, { valueString: cluster.weakensIf.join(CLUSTER_LIST_SEPARATOR) }));
|
|
25
|
-
}
|
|
26
|
-
const lifecycleExts = [extension(CLUSTER_EXT.STATUS, { valueString: cluster.status })];
|
|
27
|
-
if (cluster.consultationId) lifecycleExts.push(extension(CLUSTER_EXT.CONSULTATION_ID, { valueString: cluster.consultationId }));
|
|
28
|
-
if (cluster.dissolveReason) lifecycleExts.push(extension(CLUSTER_EXT.DISSOLVE_REASON, { valueString: cluster.dissolveReason }));
|
|
29
|
-
|
|
30
|
-
const identifiers = [identifier(CLUSTER_IMPRESSION_SLUG, cluster.clusterId)];
|
|
31
|
-
if (cluster.label) identifiers.push(identifier(CLUSTER_LABEL_SLUG, cluster.label));
|
|
32
|
-
|
|
33
|
-
const payload = {
|
|
34
|
-
resourceType: 'ClinicalImpression',
|
|
35
|
-
id: fhirId(cluster.clusterId),
|
|
36
|
-
identifier: identifiers,
|
|
37
|
-
status: CLUSTER_STATUS_TO_FHIR[cluster.status],
|
|
38
|
-
subject: patientReference(cluster.patientId),
|
|
39
|
-
problem: cluster.memberCaseIds.map((caseId) => reference('Condition', caseId)),
|
|
40
|
-
extension: [...epistemicExts, ...lifecycleExts],
|
|
41
|
-
};
|
|
42
|
-
if (cluster.relationshipModel) payload.summary = cluster.relationshipModel;
|
|
43
|
-
if (cluster.openingReasoning) payload.description = cluster.openingReasoning;
|
|
44
|
-
if (cluster.dissolvedAt) {
|
|
45
|
-
// toIso normalizes to Z; a non-Z openedAt/dissolvedAt does not byte-for-byte round-trip (known loss).
|
|
46
|
-
payload.effectivePeriod = { start: toIso(cluster.openedAt), end: toIso(cluster.dissolvedAt) };
|
|
47
|
-
} else {
|
|
48
|
-
payload.effectiveDateTime = toIso(cluster.openedAt);
|
|
49
|
-
}
|
|
50
|
-
if (cluster.dissolveCategory) payload.statusReason = codeableConcept(cluster.dissolveCategory);
|
|
51
|
-
return new ClinicalImpression(payload);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
11
|
static fromRecommendationPlan({ plan }) {
|
|
55
12
|
const finding = [...plan.elements]
|
|
56
13
|
.sort((a, b) => a.rank - b.rank)
|
|
@@ -117,6 +74,4 @@ function palliativeFinding(finding) {
|
|
|
117
74
|
|
|
118
75
|
module.exports = {
|
|
119
76
|
ClinicalImpression,
|
|
120
|
-
CLUSTER_STATUS_TO_FHIR,
|
|
121
|
-
CLUSTER_LIST_SEPARATOR,
|
|
122
77
|
};
|
|
@@ -1,33 +1,12 @@
|
|
|
1
1
|
const { getDeviceId, getOrganizationId } = require('../config/fhirConfig');
|
|
2
|
-
const { fhirId
|
|
2
|
+
const { fhirId } = require('../helpers/fhirHelper');
|
|
3
3
|
const { extension, reference, codeableConcept, toIso } = require('../helpers/elementHelper');
|
|
4
4
|
|
|
5
|
-
const CLUSTER_ACTIVITY_CODESYSTEM = 'clinical-activity';
|
|
6
|
-
const CLUSTER_ACTIVITY_CODES = {
|
|
7
|
-
opened: 'cluster-hypothesis-opened',
|
|
8
|
-
updated: 'cluster-hypothesis-updated',
|
|
9
|
-
dissolved: 'cluster-hypothesis-dissolved',
|
|
10
|
-
};
|
|
11
|
-
|
|
12
5
|
class Provenance {
|
|
13
6
|
constructor(payload) {
|
|
14
7
|
Object.assign(this, payload);
|
|
15
8
|
}
|
|
16
9
|
|
|
17
|
-
static fromClusterImpression({ clusterId, turnId, action, recordedAt, transitionReason }) {
|
|
18
|
-
const activityCode = CLUSTER_ACTIVITY_CODES[action] || 'cluster-hypothesis';
|
|
19
|
-
const payload = {
|
|
20
|
-
resourceType: 'Provenance',
|
|
21
|
-
id: fhirId(`${clusterId}-prov-${turnId}-${action}`),
|
|
22
|
-
target: [{ reference: `ClinicalImpression/${fhirId(clusterId)}` }],
|
|
23
|
-
recorded: toIso(recordedAt),
|
|
24
|
-
agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
|
|
25
|
-
activity: codeableConcept(activityCode, { code: activityCode, system: codeSystemUrl(CLUSTER_ACTIVITY_CODESYSTEM) }),
|
|
26
|
-
};
|
|
27
|
-
if (transitionReason) payload.extension = [extension('transition-reason', { valueString: transitionReason })];
|
|
28
|
-
return new Provenance(payload);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
10
|
static fromMentionProjection({ target, provenance, activityText }) {
|
|
32
11
|
const payload = {
|
|
33
12
|
resourceType: 'Provenance',
|
|
@@ -1,75 +1,27 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
const { fhirId, identifierSystem } = require('../helpers/fhirHelper');
|
|
4
|
-
const { extMap, identifierValue, refToId } = require('../helpers/fhirReadHelper');
|
|
5
|
-
const { CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG } = require('../constants/projectionSlugs');
|
|
6
|
-
const { CLUSTER_LIST_SEPARATOR } = require('../resources/ClinicalImpression');
|
|
7
|
-
const { CLUSTER_EXT } = require('../constants/extensionSlugs');
|
|
8
|
-
const { PROJECTOR_NAME } = require('../projections/clusterProjection');
|
|
1
|
+
const { fhirId } = require('../helpers/fhirHelper');
|
|
2
|
+
const { CLUSTER_IMPRESSION_SLUG } = require('../constants/projectionSlugs');
|
|
9
3
|
const { getFhirStore } = require('../stores/fhirStore');
|
|
10
|
-
const {
|
|
11
|
-
const {
|
|
12
|
-
const { project, storeResources } = require('./fhirService');
|
|
4
|
+
const { toFhir, fromFhir } = require('../projections/clusterProjection');
|
|
5
|
+
const { storeResources } = require('./fhirService');
|
|
13
6
|
|
|
14
7
|
async function storeCluster({ patientId, cluster, history = [] }) {
|
|
15
8
|
if (cluster.patientId !== patientId) {
|
|
16
9
|
throw new Error(`storeCluster patientId mismatch: expected ${patientId}`);
|
|
17
10
|
}
|
|
18
|
-
const
|
|
19
|
-
const resources = bundle.entry.map((entry) => entry.resource);
|
|
20
|
-
const { stored } = await storeResources(resources);
|
|
11
|
+
const { stored } = await storeResources(toFhir({ cluster, history }));
|
|
21
12
|
return { count: stored.length, ids: stored };
|
|
22
13
|
}
|
|
23
14
|
|
|
24
15
|
async function readClusters({ patientId, label = null, status = null }) {
|
|
25
16
|
const store = getFhirStore();
|
|
26
17
|
const patient = `Patient/${fhirId(patientId)}`;
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
cluster = new ClusterImpression(reconstructCluster(impression, patientId));
|
|
36
|
-
} catch (error) {
|
|
37
|
-
if (error instanceof ZodError) {
|
|
38
|
-
logger.warn({ clusterId: impression.id }, 'skipping malformed cluster ClinicalImpression');
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
if (label && cluster.label !== label) continue;
|
|
44
|
-
if (status && cluster.status !== status) continue;
|
|
45
|
-
clusters.push(cluster);
|
|
46
|
-
}
|
|
47
|
-
return clusters;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function reconstructCluster(resource, patientId) {
|
|
51
|
-
const ext = extMap(resource);
|
|
52
|
-
const period = resource.effectivePeriod || {};
|
|
53
|
-
return {
|
|
54
|
-
schemaVersion: '1',
|
|
55
|
-
clusterId: identifierValue(resource, CLUSTER_IMPRESSION_SLUG),
|
|
56
|
-
patientId,
|
|
57
|
-
label: identifierValue(resource, CLUSTER_LABEL_SLUG) || '',
|
|
58
|
-
hypothesisType: ext[CLUSTER_EXT.HYPOTHESIS_TYPE],
|
|
59
|
-
status: ext[CLUSTER_EXT.STATUS],
|
|
60
|
-
confidence: ext[CLUSTER_EXT.CONFIDENCE],
|
|
61
|
-
clinicalSignificance: ext[CLUSTER_EXT.CLINICAL_SIGNIFICANCE],
|
|
62
|
-
memberCaseIds: (resource.problem || []).map((problemRef) => refToId(problemRef.reference)).filter(Boolean),
|
|
63
|
-
consultationId: ext[CLUSTER_EXT.CONSULTATION_ID] || null,
|
|
64
|
-
relationshipModel: resource.summary || '',
|
|
65
|
-
strengthensIf: ext[CLUSTER_EXT.STRENGTHENS_IF] ? ext[CLUSTER_EXT.STRENGTHENS_IF].split(CLUSTER_LIST_SEPARATOR) : [],
|
|
66
|
-
weakensIf: ext[CLUSTER_EXT.WEAKENS_IF] ? ext[CLUSTER_EXT.WEAKENS_IF].split(CLUSTER_LIST_SEPARATOR) : [],
|
|
67
|
-
openingReasoning: resource.description || '',
|
|
68
|
-
openedAt: resource.effectiveDateTime || period.start,
|
|
69
|
-
dissolvedAt: period.end || null,
|
|
70
|
-
dissolveReason: ext[CLUSTER_EXT.DISSOLVE_REASON] || null,
|
|
71
|
-
dissolveCategory: (resource.statusReason && resource.statusReason.text) || null,
|
|
72
|
-
};
|
|
18
|
+
const resources = await store.find({
|
|
19
|
+
patient,
|
|
20
|
+
resourceType: 'ClinicalImpression',
|
|
21
|
+
identifierSlug: CLUSTER_IMPRESSION_SLUG,
|
|
22
|
+
});
|
|
23
|
+
return fromFhir(resources, { patientId })
|
|
24
|
+
.filter((cluster) => (!label || cluster.label === label) && (!status || cluster.status === status));
|
|
73
25
|
}
|
|
74
26
|
|
|
75
27
|
module.exports = {
|