gdc-common-utils-ts 2.2.2 → 2.3.1
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/README.md +4 -2
- package/dist/constants/data-collections.js +0 -1
- package/dist/constants/fhir-resource-types.d.ts +14 -0
- package/dist/constants/fhir-resource-types.js +6 -0
- package/dist/constants/healthcare.d.ts +348 -193
- package/dist/constants/healthcare.js +99 -37
- package/dist/constants/hl7-roles.d.ts +38 -4
- package/dist/constants/hl7-roles.js +92 -4
- package/dist/examples/shared.d.ts +1 -1
- package/dist/examples/shared.js +1 -1
- package/dist/i18n/role-codes.i18n.d.ts +9 -1
- package/dist/i18n/role-codes.i18n.js +12 -1
- package/dist/models/device-license.d.ts +20 -0
- package/dist/models/interoperable-claims/invoice-claims.d.ts +1 -1
- package/dist/models/interoperable-claims/invoice-claims.js +2 -2
- package/dist/models/interoperable-claims/observation-claims.d.ts +2 -2
- package/dist/models/interoperable-claims/observation-claims.js +2 -2
- package/dist/models/interoperable-claims/related-person-claims.d.ts +13 -0
- package/dist/models/interoperable-claims/related-person-claims.js +16 -0
- package/dist/utils/allergy-intolerance-entry-editor.js +2 -2
- package/dist/utils/bundle-document-builder.d.ts +35 -0
- package/dist/utils/bundle-document-builder.js +176 -0
- package/dist/utils/clinical-resource-view.d.ts +16 -0
- package/dist/utils/clinical-resource-view.js +267 -33
- package/dist/utils/communication-consent-access-editor.d.ts +8 -4
- package/dist/utils/communication-consent-access-editor.js +8 -4
- package/dist/utils/condition-entry-editor.js +2 -2
- package/dist/utils/consent-duplicate-rules.js +1 -1
- package/dist/utils/did.d.ts +2 -2
- package/dist/utils/did.js +3 -3
- package/dist/utils/license-list-search.d.ts +5 -0
- package/dist/utils/license-list-search.js +9 -0
- package/dist/utils/license.d.ts +45 -1
- package/dist/utils/license.js +86 -2
- package/dist/utils/related-person-list.d.ts +4 -0
- package/dist/utils/related-person-list.js +26 -6
- package/package.json +1 -1
|
@@ -14,8 +14,43 @@ export type BundleDocumentResourceFilter = Readonly<{
|
|
|
14
14
|
section?: string;
|
|
15
15
|
resourceType?: string;
|
|
16
16
|
}>;
|
|
17
|
+
export type PrepareBundleDocumentForSubjectOptions = Readonly<{
|
|
18
|
+
context?: string;
|
|
19
|
+
}>;
|
|
17
20
|
export declare function getSimpleClaimAttributeName(key: string): string;
|
|
18
21
|
export declare function extractFlatClaimValue(record: Record<string, any> | undefined, key: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Validates and prepares an imported FHIR document for one authorized subject.
|
|
24
|
+
*
|
|
25
|
+
* The input is cloned. Every resource receives a deterministic flat
|
|
26
|
+
* `meta.claims` projection. Resources referenced by `Composition.section`
|
|
27
|
+
* receive the canonical section tokens in `Composition.section`, and patient
|
|
28
|
+
* subject references are rebound to the selected `did:web` subject. Existing
|
|
29
|
+
* claims are retained, except subject/patient values are forced to the
|
|
30
|
+
* selected subject so an imported local `Patient/{id}` cannot escape the
|
|
31
|
+
* controller-authorized index scope. The source object is never mutated.
|
|
32
|
+
*
|
|
33
|
+
* This is a conversion primitive only. It neither confirms that a mismatched
|
|
34
|
+
* Patient is the right person nor persists anything; confirmation belongs to
|
|
35
|
+
* the product UI and transport/authorization remain GW concerns.
|
|
36
|
+
*/
|
|
37
|
+
export declare function prepareBundleDocumentForSubject(bundle: Record<string, any>, subjectDid: string, options?: PrepareBundleDocumentForSubjectOptions): Record<string, any>;
|
|
38
|
+
/**
|
|
39
|
+
* Returns the Patient Full Name addressed by the document Composition.
|
|
40
|
+
*
|
|
41
|
+
* Resolution prefers the Patient referenced by `Composition.subject`, then
|
|
42
|
+
* falls back to the first Patient for compatibility with incomplete imports.
|
|
43
|
+
* Name selection prefers `use=official`, then `usual`, then the first name;
|
|
44
|
+
* `HumanName.text` wins over assembling prefix/given/family/suffix.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getBundleDocumentPatientFullName(bundle: Record<string, any>): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Temporary Full Name comparison key: whitespace folding plus uppercase only.
|
|
49
|
+
* Diacritics and punctuation are intentionally preserved until the shared
|
|
50
|
+
* ICAO transliteration contract exists, so `JOSE` and `JOSÉ` do not compare as
|
|
51
|
+
* equal by accident.
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeFullNameForComparison(value: unknown): string;
|
|
19
54
|
export declare function detectClaimsResourceType(claims: BundleDocumentClaims): string | undefined;
|
|
20
55
|
export declare function convertClaimsToFhirResource(claims: BundleDocumentClaims, version?: 'r4'): FhirResource;
|
|
21
56
|
/**
|
|
@@ -99,6 +99,182 @@ function normalizeSectionCode(value) {
|
|
|
99
99
|
return { code: system };
|
|
100
100
|
return { system, code };
|
|
101
101
|
}
|
|
102
|
+
function toCanonicalSectionToken(system, code) {
|
|
103
|
+
const normalizedSystem = asTrimmedString(system);
|
|
104
|
+
const normalizedCode = asTrimmedString(code);
|
|
105
|
+
if (!normalizedCode)
|
|
106
|
+
return '';
|
|
107
|
+
if (!normalizedSystem)
|
|
108
|
+
return normalizedCode;
|
|
109
|
+
return `${normalizedSystem === 'http://loinc.org' ? 'LOINC' : normalizedSystem}|${normalizedCode}`;
|
|
110
|
+
}
|
|
111
|
+
function resourceReferenceAliases(entry) {
|
|
112
|
+
const resource = entry?.resource;
|
|
113
|
+
const resourceType = asTrimmedString(resource?.resourceType);
|
|
114
|
+
const resourceId = asTrimmedString(resource?.id);
|
|
115
|
+
const fullUrl = asTrimmedString(entry?.fullUrl);
|
|
116
|
+
return Array.from(new Set([
|
|
117
|
+
fullUrl,
|
|
118
|
+
resourceType && resourceId ? `${resourceType}/${resourceId}` : '',
|
|
119
|
+
resourceId,
|
|
120
|
+
fullUrl ? fullUrl.split('/').slice(-2).join('/') : '',
|
|
121
|
+
].filter(Boolean)));
|
|
122
|
+
}
|
|
123
|
+
function sectionMembershipByReference(composition) {
|
|
124
|
+
const memberships = new Map();
|
|
125
|
+
const visit = (sections) => {
|
|
126
|
+
if (!Array.isArray(sections))
|
|
127
|
+
return;
|
|
128
|
+
for (const section of sections) {
|
|
129
|
+
const coding = Array.isArray(section?.code?.coding) ? section.code.coding[0] : undefined;
|
|
130
|
+
const token = toCanonicalSectionToken(coding?.system, coding?.code);
|
|
131
|
+
if (token && Array.isArray(section?.entry)) {
|
|
132
|
+
for (const item of section.entry) {
|
|
133
|
+
const reference = asTrimmedString(item?.reference);
|
|
134
|
+
if (!reference)
|
|
135
|
+
continue;
|
|
136
|
+
const aliases = [reference, reference.split('/').slice(-2).join('/'), reference.split('/').pop() || ''];
|
|
137
|
+
for (const alias of aliases.filter(Boolean)) {
|
|
138
|
+
memberships.set(alias, Array.from(new Set([...(memberships.get(alias) || []), token])));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
visit(section?.section);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
visit(composition?.section);
|
|
146
|
+
return memberships;
|
|
147
|
+
}
|
|
148
|
+
function allCompositionSectionTokens(composition) {
|
|
149
|
+
const tokens = [];
|
|
150
|
+
const visit = (sections) => {
|
|
151
|
+
if (!Array.isArray(sections))
|
|
152
|
+
return;
|
|
153
|
+
for (const section of sections) {
|
|
154
|
+
const coding = Array.isArray(section?.code?.coding) ? section.code.coding[0] : undefined;
|
|
155
|
+
const token = toCanonicalSectionToken(coding?.system, coding?.code);
|
|
156
|
+
if (token)
|
|
157
|
+
tokens.push(token);
|
|
158
|
+
visit(section?.section);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
visit(composition?.section);
|
|
162
|
+
return Array.from(new Set(tokens));
|
|
163
|
+
}
|
|
164
|
+
function replacePatientSubjectReference(resource, subjectDid) {
|
|
165
|
+
const resourceType = asTrimmedString(resource?.resourceType);
|
|
166
|
+
if (resourceType === ResourceTypesFhirR4.Composition || resource?.subject?.reference) {
|
|
167
|
+
resource.subject = { ...(resource.subject || {}), reference: subjectDid };
|
|
168
|
+
}
|
|
169
|
+
if (resource?.patient?.reference) {
|
|
170
|
+
resource.patient = { ...(resource.patient || {}), reference: subjectDid };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Validates and prepares an imported FHIR document for one authorized subject.
|
|
175
|
+
*
|
|
176
|
+
* The input is cloned. Every resource receives a deterministic flat
|
|
177
|
+
* `meta.claims` projection. Resources referenced by `Composition.section`
|
|
178
|
+
* receive the canonical section tokens in `Composition.section`, and patient
|
|
179
|
+
* subject references are rebound to the selected `did:web` subject. Existing
|
|
180
|
+
* claims are retained, except subject/patient values are forced to the
|
|
181
|
+
* selected subject so an imported local `Patient/{id}` cannot escape the
|
|
182
|
+
* controller-authorized index scope. The source object is never mutated.
|
|
183
|
+
*
|
|
184
|
+
* This is a conversion primitive only. It neither confirms that a mismatched
|
|
185
|
+
* Patient is the right person nor persists anything; confirmation belongs to
|
|
186
|
+
* the product UI and transport/authorization remain GW concerns.
|
|
187
|
+
*/
|
|
188
|
+
export function prepareBundleDocumentForSubject(bundle, subjectDid, options = {}) {
|
|
189
|
+
const validation = validateBundleDocumentBasic(bundle);
|
|
190
|
+
if (!validation.ok)
|
|
191
|
+
throw new Error(validation.issues.join(' '));
|
|
192
|
+
const normalizedSubjectDid = asTrimmedString(subjectDid);
|
|
193
|
+
if (!normalizedSubjectDid)
|
|
194
|
+
throw new Error('A subject DID is required.');
|
|
195
|
+
const prepared = JSON.parse(JSON.stringify(bundle));
|
|
196
|
+
const entries = prepared.entry;
|
|
197
|
+
const composition = entries[0]?.resource;
|
|
198
|
+
const membership = sectionMembershipByReference(composition);
|
|
199
|
+
const allSections = allCompositionSectionTokens(composition);
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
const resource = entry?.resource;
|
|
202
|
+
if (!resource || typeof resource !== 'object')
|
|
203
|
+
continue;
|
|
204
|
+
replacePatientSubjectReference(resource, normalizedSubjectDid);
|
|
205
|
+
const generated = convertFhirResourceToClaims(resource, options.context || 'org.hl7.fhir.r4');
|
|
206
|
+
const existing = resource?.meta?.claims;
|
|
207
|
+
const claims = {
|
|
208
|
+
...generated,
|
|
209
|
+
...(existing && typeof existing === 'object' && !Array.isArray(existing) ? existing : {}),
|
|
210
|
+
};
|
|
211
|
+
const resourceType = asTrimmedString(resource.resourceType);
|
|
212
|
+
if (resourceType !== 'Patient') {
|
|
213
|
+
if (resource?.subject?.reference)
|
|
214
|
+
claims[`${resourceType}.subject`] = normalizedSubjectDid;
|
|
215
|
+
if (resource?.patient?.reference)
|
|
216
|
+
claims[`${resourceType}.patient`] = normalizedSubjectDid;
|
|
217
|
+
}
|
|
218
|
+
const resourceSections = resourceType === ResourceTypesFhirR4.Composition
|
|
219
|
+
? allSections
|
|
220
|
+
: resourceReferenceAliases(entry).flatMap((alias) => membership.get(alias) || []);
|
|
221
|
+
if (resourceSections.length > 0) {
|
|
222
|
+
claims[CompositionClaim.Section] = Array.from(new Set(resourceSections)).join(',');
|
|
223
|
+
}
|
|
224
|
+
resource.meta = {
|
|
225
|
+
...(resource.meta && typeof resource.meta === 'object' ? resource.meta : {}),
|
|
226
|
+
claims,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return prepared;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Returns the Patient Full Name addressed by the document Composition.
|
|
233
|
+
*
|
|
234
|
+
* Resolution prefers the Patient referenced by `Composition.subject`, then
|
|
235
|
+
* falls back to the first Patient for compatibility with incomplete imports.
|
|
236
|
+
* Name selection prefers `use=official`, then `usual`, then the first name;
|
|
237
|
+
* `HumanName.text` wins over assembling prefix/given/family/suffix.
|
|
238
|
+
*/
|
|
239
|
+
export function getBundleDocumentPatientFullName(bundle) {
|
|
240
|
+
const entries = Array.isArray(bundle?.entry) ? bundle.entry : [];
|
|
241
|
+
const composition = entries[0]?.resource;
|
|
242
|
+
const subjectReference = asTrimmedString(composition?.subject?.reference);
|
|
243
|
+
const patientEntry = entries.find((entry) => {
|
|
244
|
+
if (entry?.resource?.resourceType !== 'Patient')
|
|
245
|
+
return false;
|
|
246
|
+
if (!subjectReference)
|
|
247
|
+
return true;
|
|
248
|
+
return resourceReferenceAliases(entry).some((alias) => alias === subjectReference
|
|
249
|
+
|| alias === subjectReference.split('/').slice(-2).join('/')
|
|
250
|
+
|| alias === subjectReference.split('/').pop());
|
|
251
|
+
}) || entries.find((entry) => entry?.resource?.resourceType === 'Patient');
|
|
252
|
+
const names = Array.isArray(patientEntry?.resource?.name) ? patientEntry.resource.name : [];
|
|
253
|
+
const name = names.find((item) => item?.use === 'official')
|
|
254
|
+
|| names.find((item) => item?.use === 'usual')
|
|
255
|
+
|| names[0];
|
|
256
|
+
if (!name)
|
|
257
|
+
return undefined;
|
|
258
|
+
const text = asTrimmedString(name.text);
|
|
259
|
+
if (text)
|
|
260
|
+
return text;
|
|
261
|
+
const parts = [
|
|
262
|
+
...(Array.isArray(name.prefix) ? name.prefix : []),
|
|
263
|
+
...(Array.isArray(name.given) ? name.given : []),
|
|
264
|
+
name.family,
|
|
265
|
+
...(Array.isArray(name.suffix) ? name.suffix : []),
|
|
266
|
+
].map(asTrimmedString).filter(Boolean);
|
|
267
|
+
return parts.length > 0 ? parts.join(' ') : undefined;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Temporary Full Name comparison key: whitespace folding plus uppercase only.
|
|
271
|
+
* Diacritics and punctuation are intentionally preserved until the shared
|
|
272
|
+
* ICAO transliteration contract exists, so `JOSE` and `JOSÉ` do not compare as
|
|
273
|
+
* equal by accident.
|
|
274
|
+
*/
|
|
275
|
+
export function normalizeFullNameForComparison(value) {
|
|
276
|
+
return asTrimmedString(value).replace(/\s+/g, ' ').toUpperCase();
|
|
277
|
+
}
|
|
102
278
|
function resolveContainedReferenceListClaimKey(resourceType) {
|
|
103
279
|
switch (resourceType) {
|
|
104
280
|
case ResourceTypesFhirR4.MedicationStatement:
|
|
@@ -29,6 +29,14 @@ export type ClinicalResourceExpandedView = Readonly<{
|
|
|
29
29
|
xhtml?: string;
|
|
30
30
|
notes: string[];
|
|
31
31
|
}>;
|
|
32
|
+
/** UI-ready summary for one Composition section and its resolved entries. */
|
|
33
|
+
export type ClinicalSectionView = Readonly<{
|
|
34
|
+
code?: string;
|
|
35
|
+
title: string;
|
|
36
|
+
emptyReason?: string;
|
|
37
|
+
resources: ClinicalResourceCardView[];
|
|
38
|
+
unresolvedReferences: string[];
|
|
39
|
+
}>;
|
|
32
40
|
export type ClinicalResourceLike = Readonly<{
|
|
33
41
|
resourceType?: string;
|
|
34
42
|
text?: {
|
|
@@ -87,6 +95,14 @@ export declare function toClinicalResourceExpandedView(entry: ClinicalResourceEn
|
|
|
87
95
|
* Maps all entries from a Bundle into expanded views.
|
|
88
96
|
*/
|
|
89
97
|
export declare function toClinicalResourceExpandedViews(bundle: ClinicalResourceBundleLike): ClinicalResourceExpandedView[];
|
|
98
|
+
/**
|
|
99
|
+
* Builds section cards from the references authored in an IPS Composition.
|
|
100
|
+
*
|
|
101
|
+
* The Composition is authoritative for grouping. Resource type is deliberately
|
|
102
|
+
* not used to guess a section because Observation and supporting resources can
|
|
103
|
+
* be referenced from several IPS sections.
|
|
104
|
+
*/
|
|
105
|
+
export declare function toClinicalSectionViews(bundle: ClinicalResourceBundleLike): ClinicalSectionView[];
|
|
90
106
|
/**
|
|
91
107
|
* Returns the most useful local text plus international display pair that can
|
|
92
108
|
* be inferred from one FHIR-like resource and its `meta.claims`.
|
|
@@ -17,16 +17,17 @@ const GENERIC_ASSERTER_CLAIM_SUFFIX = '.asserter';
|
|
|
17
17
|
export function toClinicalResourceCommonView(entry) {
|
|
18
18
|
const claims = readClaims(entry);
|
|
19
19
|
const resourceType = resolveResourceType(entry, claims);
|
|
20
|
-
const
|
|
20
|
+
const resource = entry.resource;
|
|
21
|
+
const title = resolveTitle(resourceType, claims, resource);
|
|
21
22
|
return {
|
|
22
23
|
title,
|
|
23
24
|
resourceType,
|
|
24
|
-
identifier: resolveIdentifier(resourceType, claims),
|
|
25
|
-
date: resolveDate(resourceType, claims),
|
|
26
|
-
periodStart: resolvePeriodStart(resourceType, claims),
|
|
27
|
-
periodEnd: resolvePeriodEnd(resourceType, claims),
|
|
25
|
+
identifier: resolveIdentifier(resourceType, claims, resource),
|
|
26
|
+
date: resolveDate(resourceType, claims, resource),
|
|
27
|
+
periodStart: resolvePeriodStart(resourceType, claims, resource),
|
|
28
|
+
periodEnd: resolvePeriodEnd(resourceType, claims, resource),
|
|
28
29
|
fullUrl: trimValue(entry.fullUrl),
|
|
29
|
-
actors: resolveActors(resourceType, claims),
|
|
30
|
+
actors: resolveActors(resourceType, claims, resource),
|
|
30
31
|
claims,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
@@ -72,6 +73,51 @@ export function toClinicalResourceExpandedView(entry) {
|
|
|
72
73
|
export function toClinicalResourceExpandedViews(bundle) {
|
|
73
74
|
return readBundleEntries(bundle).map((entry) => toClinicalResourceExpandedView(entry));
|
|
74
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds section cards from the references authored in an IPS Composition.
|
|
78
|
+
*
|
|
79
|
+
* The Composition is authoritative for grouping. Resource type is deliberately
|
|
80
|
+
* not used to guess a section because Observation and supporting resources can
|
|
81
|
+
* be referenced from several IPS sections.
|
|
82
|
+
*/
|
|
83
|
+
export function toClinicalSectionViews(bundle) {
|
|
84
|
+
const entries = readBundleEntries(bundle);
|
|
85
|
+
const composition = entries.find((entry) => entry.resource?.resourceType === ResourceTypesFhirR4.Composition);
|
|
86
|
+
if (!composition?.resource) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const entryIndex = indexEntriesByFhirReference(entries);
|
|
90
|
+
return flattenCompositionSections(asArray(composition.resource.section)).map((section) => {
|
|
91
|
+
const references = asArray(section.entry)
|
|
92
|
+
.map((item) => trimValue(asRecord(item).reference))
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
const resolvedEntries = [];
|
|
95
|
+
const unresolvedReferences = [];
|
|
96
|
+
references.forEach((reference) => {
|
|
97
|
+
const resolved = resolveIndexedEntry(entryIndex, reference);
|
|
98
|
+
if (resolved) {
|
|
99
|
+
resolvedEntries.push(resolved);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
unresolvedReferences.push(reference);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const code = resolveCodeableConceptToken(section.code);
|
|
106
|
+
const title = trimValue(section.title)
|
|
107
|
+
|| resolveCodeableConceptLabel(section.code)
|
|
108
|
+
|| code
|
|
109
|
+
|| 'Section';
|
|
110
|
+
return {
|
|
111
|
+
...(code ? { code } : {}),
|
|
112
|
+
title,
|
|
113
|
+
...(resolveCodeableConceptLabel(section.emptyReason)
|
|
114
|
+
? { emptyReason: resolveCodeableConceptLabel(section.emptyReason) }
|
|
115
|
+
: {}),
|
|
116
|
+
resources: resolvedEntries.map((entry) => toClinicalResourceCardView(entry)),
|
|
117
|
+
unresolvedReferences,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
75
121
|
/**
|
|
76
122
|
* Returns the most useful local text plus international display pair that can
|
|
77
123
|
* be inferred from one FHIR-like resource and its `meta.claims`.
|
|
@@ -158,18 +204,19 @@ function resolveResourceType(entry, claims) {
|
|
|
158
204
|
}
|
|
159
205
|
return 'Unknown';
|
|
160
206
|
}
|
|
161
|
-
function resolveTitle(resourceType, claims) {
|
|
207
|
+
function resolveTitle(resourceType, claims, resource) {
|
|
162
208
|
if (resourceType === ResourceTypesFhirR4.Communication) {
|
|
163
209
|
return (trimValue(claims[CommunicationClaim.ContentAttachmentTitle])
|
|
164
210
|
|| trimValue(claims[CommunicationClaim.Text])
|
|
165
211
|
|| trimValue(claims[CommunicationClaim.NoteText])
|
|
212
|
+
|| resolveFhirResourceTitle(resourceType, resource)
|
|
166
213
|
|| resourceType);
|
|
167
214
|
}
|
|
168
215
|
if (resourceType === ResourceTypesFhirR4.Consent) {
|
|
169
216
|
const firstCategory = splitCsv(claims[ClaimConsent.category])[0];
|
|
170
217
|
const firstAction = splitCsv(claims[ClaimConsent.action])[0];
|
|
171
218
|
const firstPurpose = splitCsv(claims[ClaimConsent.purpose])[0];
|
|
172
|
-
return firstCategory || firstAction || firstPurpose || resourceType;
|
|
219
|
+
return firstCategory || firstAction || firstPurpose || resolveFhirResourceTitle(resourceType, resource) || resourceType;
|
|
173
220
|
}
|
|
174
221
|
if (resourceType === ResourceTypesFhirR4.MedicationStatement) {
|
|
175
222
|
const text = firstClaimValue(claims, [
|
|
@@ -180,7 +227,7 @@ function resolveTitle(resourceType, claims) {
|
|
|
180
227
|
MedicationStatementClaim.Code,
|
|
181
228
|
MedicationStatementClaimsFhirApi.Code,
|
|
182
229
|
]);
|
|
183
|
-
return text || firstCode || resourceType;
|
|
230
|
+
return text || firstCode || resolveFhirResourceTitle(resourceType, resource) || resourceType;
|
|
184
231
|
}
|
|
185
232
|
if (resourceType === ResourceTypesFhirR4.Condition) {
|
|
186
233
|
const firstCode = firstClaimCsvValue(claims, [
|
|
@@ -191,7 +238,7 @@ function resolveTitle(resourceType, claims) {
|
|
|
191
238
|
ConditionClaim.Category,
|
|
192
239
|
ConditionClaimsFhirApi.Category,
|
|
193
240
|
]);
|
|
194
|
-
return firstCode || firstCategory || resourceType;
|
|
241
|
+
return firstCode || firstCategory || resolveFhirResourceTitle(resourceType, resource) || resourceType;
|
|
195
242
|
}
|
|
196
243
|
if (resourceType === ResourceTypesFhirR4.AllergyIntolerance) {
|
|
197
244
|
const firstCode = firstClaimCsvValue(claims, [
|
|
@@ -202,78 +249,78 @@ function resolveTitle(resourceType, claims) {
|
|
|
202
249
|
AllergyIntoleranceClaim.Category,
|
|
203
250
|
AllergyIntoleranceClaimsFhirApi.Category,
|
|
204
251
|
]);
|
|
205
|
-
return firstCode || firstCategory || resourceType;
|
|
252
|
+
return firstCode || firstCategory || resolveFhirResourceTitle(resourceType, resource) || resourceType;
|
|
206
253
|
}
|
|
207
|
-
return resourceType;
|
|
254
|
+
return resolveFhirResourceTitle(resourceType, resource) || resourceType;
|
|
208
255
|
}
|
|
209
|
-
function resolveIdentifier(resourceType, claims) {
|
|
256
|
+
function resolveIdentifier(resourceType, claims, resource) {
|
|
210
257
|
if (resourceType === ResourceTypesFhirR4.Communication) {
|
|
211
|
-
return trimValue(claims[CommunicationClaim.Identifier]);
|
|
258
|
+
return trimValue(claims[CommunicationClaim.Identifier]) || resolveFhirIdentifier(resource);
|
|
212
259
|
}
|
|
213
260
|
if (resourceType === ResourceTypesFhirR4.Consent) {
|
|
214
|
-
return trimValue(claims[ClaimConsent.identifier]);
|
|
261
|
+
return trimValue(claims[ClaimConsent.identifier]) || resolveFhirIdentifier(resource);
|
|
215
262
|
}
|
|
216
263
|
if (resourceType === ResourceTypesFhirR4.MedicationStatement) {
|
|
217
264
|
return firstClaimValue(claims, [
|
|
218
265
|
MedicationStatementClaim.Identifier,
|
|
219
266
|
MedicationStatementClaimsFhirApi.Identifier,
|
|
220
|
-
]);
|
|
267
|
+
]) || resolveFhirIdentifier(resource);
|
|
221
268
|
}
|
|
222
269
|
if (resourceType === ResourceTypesFhirR4.Condition) {
|
|
223
270
|
return firstClaimValue(claims, [
|
|
224
271
|
ConditionClaim.Identifier,
|
|
225
272
|
ConditionClaimsFhirApi.Identifier,
|
|
226
|
-
]);
|
|
273
|
+
]) || resolveFhirIdentifier(resource);
|
|
227
274
|
}
|
|
228
275
|
if (resourceType === ResourceTypesFhirR4.AllergyIntolerance) {
|
|
229
276
|
return firstClaimValue(claims, [
|
|
230
277
|
AllergyIntoleranceClaim.Identifier,
|
|
231
278
|
AllergyIntoleranceClaimsFhirApi.Identifier,
|
|
232
|
-
]);
|
|
279
|
+
]) || resolveFhirIdentifier(resource);
|
|
233
280
|
}
|
|
234
|
-
return
|
|
281
|
+
return resolveFhirIdentifier(resource);
|
|
235
282
|
}
|
|
236
|
-
function resolveDate(resourceType, claims) {
|
|
283
|
+
function resolveDate(resourceType, claims, resource) {
|
|
237
284
|
if (resourceType === ResourceTypesFhirR4.Communication) {
|
|
238
|
-
return trimValue(claims[CommunicationClaim.Sent]);
|
|
285
|
+
return trimValue(claims[CommunicationClaim.Sent]) || resolveFhirDate(resourceType, resource);
|
|
239
286
|
}
|
|
240
287
|
if (resourceType === ResourceTypesFhirR4.Consent) {
|
|
241
|
-
return trimValue(claims[ClaimConsent.date]);
|
|
288
|
+
return trimValue(claims[ClaimConsent.date]) || resolveFhirDate(resourceType, resource);
|
|
242
289
|
}
|
|
243
290
|
if (resourceType === ResourceTypesFhirR4.MedicationStatement) {
|
|
244
291
|
return firstClaimValue(claims, [
|
|
245
292
|
MedicationStatementClaim.Effective,
|
|
246
293
|
MedicationStatementClaimsFhirApi.Effective,
|
|
247
|
-
]);
|
|
294
|
+
]) || resolveFhirDate(resourceType, resource);
|
|
248
295
|
}
|
|
249
296
|
if (resourceType === ResourceTypesFhirR4.Condition) {
|
|
250
297
|
return firstClaimValue(claims, [
|
|
251
298
|
ConditionClaim.OnsetDateTime,
|
|
252
299
|
ConditionClaimsFhirApi.OnsetDateTime,
|
|
253
|
-
]);
|
|
300
|
+
]) || resolveFhirDate(resourceType, resource);
|
|
254
301
|
}
|
|
255
302
|
if (resourceType === ResourceTypesFhirR4.AllergyIntolerance) {
|
|
256
303
|
return firstClaimValue(claims, [
|
|
257
304
|
AllergyIntoleranceClaim.OnsetDateTime,
|
|
258
305
|
AllergyIntoleranceClaimsFhirApi.OnsetDateTime,
|
|
259
|
-
]);
|
|
306
|
+
]) || resolveFhirDate(resourceType, resource);
|
|
260
307
|
}
|
|
261
308
|
const genericDate = findBySuffix(claims, '.date');
|
|
262
|
-
return genericDate || undefined;
|
|
309
|
+
return genericDate || resolveFhirDate(resourceType, resource) || undefined;
|
|
263
310
|
}
|
|
264
|
-
function resolvePeriodStart(resourceType, claims) {
|
|
311
|
+
function resolvePeriodStart(resourceType, claims, resource) {
|
|
265
312
|
if (resourceType === ResourceTypesFhirR4.Consent) {
|
|
266
|
-
return trimValue(claims[ClaimConsent.periodStart]);
|
|
313
|
+
return trimValue(claims[ClaimConsent.periodStart]) || resolveFhirPeriod(resource).start;
|
|
267
314
|
}
|
|
268
|
-
return findBySuffix(claims, '.period-start') || undefined;
|
|
315
|
+
return findBySuffix(claims, '.period-start') || resolveFhirPeriod(resource).start || undefined;
|
|
269
316
|
}
|
|
270
|
-
function resolvePeriodEnd(resourceType, claims) {
|
|
317
|
+
function resolvePeriodEnd(resourceType, claims, resource) {
|
|
271
318
|
if (resourceType === ResourceTypesFhirR4.Consent) {
|
|
272
|
-
return trimValue(claims[ClaimConsent.periodEnd]);
|
|
319
|
+
return trimValue(claims[ClaimConsent.periodEnd]) || resolveFhirPeriod(resource).end;
|
|
273
320
|
}
|
|
274
|
-
return findBySuffix(claims, '.period-end') || undefined;
|
|
321
|
+
return findBySuffix(claims, '.period-end') || resolveFhirPeriod(resource).end || undefined;
|
|
275
322
|
}
|
|
276
|
-
function resolveActors(resourceType, claims) {
|
|
323
|
+
function resolveActors(resourceType, claims, resource) {
|
|
277
324
|
const out = [];
|
|
278
325
|
if (resourceType === ResourceTypesFhirR4.Consent) {
|
|
279
326
|
const identifiers = splitCsv(claims[ClaimConsent.actorIdentifier]);
|
|
@@ -350,6 +397,7 @@ function resolveActors(resourceType, claims) {
|
|
|
350
397
|
appendGenericActorType(out, claims, GENERIC_CREATOR_CLAIM_SUFFIX, 'creator');
|
|
351
398
|
appendGenericActorType(out, claims, GENERIC_PERFORMER_CLAIM_SUFFIX, 'performer');
|
|
352
399
|
appendGenericActorType(out, claims, GENERIC_ASSERTER_CLAIM_SUFFIX, 'asserter');
|
|
400
|
+
appendFhirActors(out, resource);
|
|
353
401
|
return out;
|
|
354
402
|
}
|
|
355
403
|
function resolveXhtml(entry, claims) {
|
|
@@ -464,6 +512,192 @@ function resolveResourceCodeDisplay(resource) {
|
|
|
464
512
|
}
|
|
465
513
|
return undefined;
|
|
466
514
|
}
|
|
515
|
+
function resolveFhirResourceTitle(resourceType, resource) {
|
|
516
|
+
if (!resource) {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
const codeableConceptCandidates = [];
|
|
520
|
+
if (resourceType === ResourceTypesFhirR4.MedicationStatement
|
|
521
|
+
|| resourceType === ResourceTypesFhirR4.MedicationRequest) {
|
|
522
|
+
codeableConceptCandidates.push(resource.medicationCodeableConcept);
|
|
523
|
+
const medicationDisplay = trimValue(asRecord(resource.medicationReference).display);
|
|
524
|
+
if (medicationDisplay)
|
|
525
|
+
return medicationDisplay;
|
|
526
|
+
}
|
|
527
|
+
if (resourceType === ResourceTypesFhirR4.Immunization) {
|
|
528
|
+
codeableConceptCandidates.push(resource.vaccineCode);
|
|
529
|
+
}
|
|
530
|
+
if (resourceType === ResourceTypesFhirR4.Device) {
|
|
531
|
+
const deviceName = asArray(resource.deviceName)
|
|
532
|
+
.map((item) => trimValue(asRecord(item).name))
|
|
533
|
+
.find(Boolean);
|
|
534
|
+
if (deviceName)
|
|
535
|
+
return deviceName;
|
|
536
|
+
codeableConceptCandidates.push(resource.type);
|
|
537
|
+
}
|
|
538
|
+
if (resourceType === ResourceTypesFhirR4.DeviceUseStatement) {
|
|
539
|
+
const deviceDisplay = trimValue(asRecord(resource.device).display);
|
|
540
|
+
if (deviceDisplay)
|
|
541
|
+
return deviceDisplay;
|
|
542
|
+
}
|
|
543
|
+
codeableConceptCandidates.push(resource.code, resource.title, resource.name, resource.description, resource.scope, resource.category);
|
|
544
|
+
for (const candidate of codeableConceptCandidates) {
|
|
545
|
+
const plainText = trimValue(candidate);
|
|
546
|
+
if (plainText && typeof candidate !== 'object') {
|
|
547
|
+
return plainText;
|
|
548
|
+
}
|
|
549
|
+
const label = resolveCodeableConceptLabel(candidate);
|
|
550
|
+
if (label) {
|
|
551
|
+
return label;
|
|
552
|
+
}
|
|
553
|
+
for (const item of asArray(candidate)) {
|
|
554
|
+
const arrayLabel = resolveCodeableConceptLabel(item);
|
|
555
|
+
if (arrayLabel)
|
|
556
|
+
return arrayLabel;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
function resolveFhirIdentifier(resource) {
|
|
562
|
+
if (!resource)
|
|
563
|
+
return undefined;
|
|
564
|
+
for (const identifier of asArray(resource.identifier)) {
|
|
565
|
+
const record = asRecord(identifier);
|
|
566
|
+
const value = trimValue(record.value);
|
|
567
|
+
if (value)
|
|
568
|
+
return value;
|
|
569
|
+
}
|
|
570
|
+
return trimValue(resource.id) || undefined;
|
|
571
|
+
}
|
|
572
|
+
function resolveFhirDate(resourceType, resource) {
|
|
573
|
+
if (!resource)
|
|
574
|
+
return undefined;
|
|
575
|
+
const period = resolveFhirPeriod(resource);
|
|
576
|
+
const candidates = resourceType === ResourceTypesFhirR4.MedicationRequest
|
|
577
|
+
? [resource.authoredOn, period.start]
|
|
578
|
+
: resourceType === ResourceTypesFhirR4.Immunization
|
|
579
|
+
? [resource.occurrenceDateTime, resource.occurrenceString, resource.recorded]
|
|
580
|
+
: resourceType === ResourceTypesFhirR4.Procedure
|
|
581
|
+
? [resource.performedDateTime, resource.performedString, period.start]
|
|
582
|
+
: [
|
|
583
|
+
resource.effectiveDateTime,
|
|
584
|
+
resource.effectiveInstant,
|
|
585
|
+
resource.occurrenceDateTime,
|
|
586
|
+
resource.onsetDateTime,
|
|
587
|
+
resource.dateTime,
|
|
588
|
+
resource.date,
|
|
589
|
+
resource.issued,
|
|
590
|
+
resource.recordedDate,
|
|
591
|
+
resource.recordedOn,
|
|
592
|
+
resource.authoredOn,
|
|
593
|
+
resource.created,
|
|
594
|
+
period.start,
|
|
595
|
+
];
|
|
596
|
+
return firstDefinedText(candidates.map((candidate) => trimValue(candidate) || undefined));
|
|
597
|
+
}
|
|
598
|
+
function resolveFhirPeriod(resource) {
|
|
599
|
+
if (!resource)
|
|
600
|
+
return {};
|
|
601
|
+
const candidates = [
|
|
602
|
+
resource.effectivePeriod,
|
|
603
|
+
resource.performedPeriod,
|
|
604
|
+
resource.occurrencePeriod,
|
|
605
|
+
resource.timingPeriod,
|
|
606
|
+
resource.period,
|
|
607
|
+
resource.onsetPeriod,
|
|
608
|
+
];
|
|
609
|
+
for (const candidate of candidates) {
|
|
610
|
+
const period = asRecord(candidate);
|
|
611
|
+
const start = trimValue(period.start);
|
|
612
|
+
const end = trimValue(period.end);
|
|
613
|
+
if (start || end) {
|
|
614
|
+
return {
|
|
615
|
+
...(start ? { start } : {}),
|
|
616
|
+
...(end ? { end } : {}),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return {};
|
|
621
|
+
}
|
|
622
|
+
function appendFhirActors(out, resource) {
|
|
623
|
+
if (!resource)
|
|
624
|
+
return;
|
|
625
|
+
appendFhirReference(out, resource.subject || resource.patient, 'subject');
|
|
626
|
+
appendFhirReference(out, resource.informationSource || resource.source, 'source');
|
|
627
|
+
appendFhirReference(out, resource.sender, 'sender');
|
|
628
|
+
appendFhirReference(out, resource.recipient, 'recipient');
|
|
629
|
+
appendFhirReference(out, resource.recorder || resource.asserter, 'asserter');
|
|
630
|
+
appendFhirReference(out, resource.author, 'creator');
|
|
631
|
+
appendFhirReference(out, resource.performer, 'performer');
|
|
632
|
+
}
|
|
633
|
+
function appendFhirReference(out, value, type) {
|
|
634
|
+
const values = Array.isArray(value) ? value : [value];
|
|
635
|
+
values.forEach((item) => {
|
|
636
|
+
const record = asRecord(item);
|
|
637
|
+
const actor = asRecord(record.actor);
|
|
638
|
+
const identifierRecord = asRecord(record.identifier);
|
|
639
|
+
const actorIdentifierRecord = asRecord(actor.identifier);
|
|
640
|
+
const identifier = firstDefinedText([
|
|
641
|
+
trimValue(record.reference) || undefined,
|
|
642
|
+
trimValue(actor.reference) || undefined,
|
|
643
|
+
trimValue(identifierRecord.value) || undefined,
|
|
644
|
+
trimValue(actorIdentifierRecord.value) || undefined,
|
|
645
|
+
trimValue(record.display) || undefined,
|
|
646
|
+
trimValue(actor.display) || undefined,
|
|
647
|
+
]);
|
|
648
|
+
if (identifier)
|
|
649
|
+
pushActor(out, { identifier, type });
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
function resolveCodeableConceptLabel(value) {
|
|
653
|
+
const concept = asRecord(value);
|
|
654
|
+
const localText = trimValue(concept.text) || undefined;
|
|
655
|
+
const internationalDisplay = asArray(concept.coding)
|
|
656
|
+
.map((coding) => trimValue(asRecord(coding).display))
|
|
657
|
+
.find(Boolean);
|
|
658
|
+
return buildCombinedLabel(localText, internationalDisplay);
|
|
659
|
+
}
|
|
660
|
+
function resolveCodeableConceptToken(value) {
|
|
661
|
+
const coding = asArray(asRecord(value).coding)
|
|
662
|
+
.map((item) => asRecord(item))
|
|
663
|
+
.find((item) => trimValue(item.code));
|
|
664
|
+
if (!coding)
|
|
665
|
+
return undefined;
|
|
666
|
+
const system = trimValue(coding.system);
|
|
667
|
+
const code = trimValue(coding.code);
|
|
668
|
+
return system ? `${system}|${code}` : code;
|
|
669
|
+
}
|
|
670
|
+
function flattenCompositionSections(values) {
|
|
671
|
+
const out = [];
|
|
672
|
+
values.forEach((value) => {
|
|
673
|
+
const section = asRecord(value);
|
|
674
|
+
out.push(section);
|
|
675
|
+
out.push(...flattenCompositionSections(asArray(section.section)));
|
|
676
|
+
});
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
function indexEntriesByFhirReference(entries) {
|
|
680
|
+
const index = new Map();
|
|
681
|
+
entries.forEach((entry) => {
|
|
682
|
+
const fullUrl = trimValue(entry.fullUrl);
|
|
683
|
+
const resourceType = trimValue(entry.resource?.resourceType);
|
|
684
|
+
const id = trimValue(entry.resource?.id);
|
|
685
|
+
if (fullUrl)
|
|
686
|
+
index.set(fullUrl, entry);
|
|
687
|
+
if (resourceType && id)
|
|
688
|
+
index.set(`${resourceType}/${id}`, entry);
|
|
689
|
+
if (id)
|
|
690
|
+
index.set(`urn:uuid:${id}`, entry);
|
|
691
|
+
});
|
|
692
|
+
return index;
|
|
693
|
+
}
|
|
694
|
+
function resolveIndexedEntry(index, reference) {
|
|
695
|
+
const direct = index.get(reference);
|
|
696
|
+
if (direct)
|
|
697
|
+
return direct;
|
|
698
|
+
const relativeReference = reference.match(/([A-Za-z]+\/[^/]+)$/)?.[1];
|
|
699
|
+
return relativeReference ? index.get(relativeReference) : undefined;
|
|
700
|
+
}
|
|
467
701
|
function firstDefinedText(values) {
|
|
468
702
|
for (const value of values) {
|
|
469
703
|
const normalized = trimValue(value);
|