gdc-common-utils-ts 2.3.4 → 2.3.5
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/dist/constants/actor-session.d.ts +1 -0
- package/dist/constants/actor-session.js +7 -0
- package/dist/examples/professional.js +1 -112
- package/dist/models/indexing.js +1 -1
- package/dist/utils/bundle-entry-editor.js +2 -2
- package/dist/utils/bundle-query.d.ts +17 -0
- package/dist/utils/bundle-query.js +9 -2
- package/dist/utils/bundle-reader.d.ts +16 -0
- package/dist/utils/bundle-reader.js +49 -1
- package/dist/utils/client-assertion.d.ts +2 -2
- package/dist/utils/clinical-resource-filter-editor.d.ts +39 -0
- package/dist/utils/clinical-resource-filter-editor.js +84 -0
- package/dist/utils/communication-bundle-document-request.d.ts +9 -8
- package/dist/utils/communication-bundle-document-request.js +14 -9
- package/dist/utils/communication-fhir-r4.js +24 -17
- package/package.json +1 -1
|
@@ -38,6 +38,7 @@ export declare const ActorCapabilities: Readonly<{
|
|
|
38
38
|
readonly IndividualImportIps: "individual.import_ips";
|
|
39
39
|
readonly IndividualGenerateDigitalTwin: "individual.generate_digital_twin";
|
|
40
40
|
readonly IndividualIngestCommunication: "individual.ingest_communication";
|
|
41
|
+
readonly IndividualReadClinicalSummary: "individual.read_clinical_summary";
|
|
41
42
|
readonly IndividualUpsertRelatedPerson: "individual.upsert_related_person";
|
|
42
43
|
readonly IndividualMemberDisable: "individual_member.disable";
|
|
43
44
|
readonly IndividualMemberPurge: "individual_member.purge";
|
|
@@ -40,6 +40,7 @@ export const ActorCapabilities = Object.freeze({
|
|
|
40
40
|
IndividualImportIps: 'individual.import_ips',
|
|
41
41
|
IndividualGenerateDigitalTwin: 'individual.generate_digital_twin',
|
|
42
42
|
IndividualIngestCommunication: 'individual.ingest_communication',
|
|
43
|
+
IndividualReadClinicalSummary: 'individual.read_clinical_summary',
|
|
43
44
|
IndividualUpsertRelatedPerson: 'individual.upsert_related_person',
|
|
44
45
|
IndividualMemberDisable: 'individual_member.disable',
|
|
45
46
|
IndividualMemberPurge: 'individual_member.purge',
|
|
@@ -161,6 +162,12 @@ export const ActorCapabilityDocs = Object.freeze({
|
|
|
161
162
|
programmingHint: 'Choose the route family carefully (`api`, `didcomm-plain`, `legacy-fhir`) to match the runtime transport profile.',
|
|
162
163
|
relatedMethods: ['ingestCommunicationAndUpdateIndex'],
|
|
163
164
|
},
|
|
165
|
+
[ActorCapabilities.IndividualReadClinicalSummary]: {
|
|
166
|
+
actorKind: ActorKinds.IndividualController,
|
|
167
|
+
summary: 'Reads the clinical summary currently available for one subject through an auditable Communication.',
|
|
168
|
+
programmingHint: 'Use requestClinicalSummary with Subject/$summary and attached FHIR Parameters. Do not route reads through ingestion methods.',
|
|
169
|
+
relatedMethods: ['requestClinicalSummary'],
|
|
170
|
+
},
|
|
164
171
|
[ActorCapabilities.IndividualUpsertRelatedPerson]: {
|
|
165
172
|
actorKind: ActorKinds.IndividualController,
|
|
166
173
|
summary: 'Creates or updates one related-person/member relationship for the individual scope.',
|
|
@@ -210,115 +210,4 @@ function buildConsentDecisionScenario(params) {
|
|
|
210
210
|
* consent state + actor target + role + purpose + requested scope.
|
|
211
211
|
*/
|
|
212
212
|
export const EXAMPLE_PROFESSIONAL_CONSENT_SCENARIOS = Object.freeze({
|
|
213
|
-
|
|
214
|
-
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
215
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
216
|
-
purpose: HealthcareConsentPurposes.Treatment,
|
|
217
|
-
consentActions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
|
|
218
|
-
requestedSections: HealthcareBasicSections.AllergiesAndIntolerances.claim,
|
|
219
|
-
includedTypes: [
|
|
220
|
-
ResourceTypesFhirR4.Composition,
|
|
221
|
-
ResourceTypesFhirR4.AllergyIntolerance,
|
|
222
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
223
|
-
],
|
|
224
|
-
expectedSmartTokenDecision: 'allowed',
|
|
225
|
-
reason: 'physician is targeted directly by email and role for continuous care over allergies section',
|
|
226
|
-
}),
|
|
227
|
-
physicianByEmailEmergencySummaryAllowed: buildConsentDecisionScenario({
|
|
228
|
-
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
229
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
230
|
-
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
231
|
-
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
232
|
-
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
233
|
-
includedTypes: [
|
|
234
|
-
ResourceTypesFhirR4.Composition,
|
|
235
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
236
|
-
ResourceTypesFhirR4.Observation,
|
|
237
|
-
],
|
|
238
|
-
expectedSmartTokenDecision: 'allowed',
|
|
239
|
-
reason: 'physician is targeted directly by email and role for emergency summary access',
|
|
240
|
-
}),
|
|
241
|
-
physicianByOrganizationResultsAllowed: buildConsentDecisionScenario({
|
|
242
|
-
actorId: { organizationUrl: EXAMPLE_PROVIDER_ORG_URL },
|
|
243
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
244
|
-
purpose: HealthcareConsentPurposes.Treatment,
|
|
245
|
-
consentActions: [HealthcareBasicSections.Results.claim],
|
|
246
|
-
requestedSections: HealthcareBasicSections.Results.claim,
|
|
247
|
-
includedTypes: [
|
|
248
|
-
ResourceTypesFhirR4.Composition,
|
|
249
|
-
ResourceTypesFhirR4.DiagnosticReport,
|
|
250
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
251
|
-
],
|
|
252
|
-
expectedSmartTokenDecision: 'allowed',
|
|
253
|
-
reason: 'consent is granted to a physician role within a given organization for continuous care results access',
|
|
254
|
-
}),
|
|
255
|
-
physicianByJurisdictionEmergencySummaryAllowed: buildConsentDecisionScenario({
|
|
256
|
-
actorId: EXAMPLE_JURISDICTION,
|
|
257
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
258
|
-
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
259
|
-
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
260
|
-
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
261
|
-
includedTypes: [
|
|
262
|
-
ResourceTypesFhirR4.Composition,
|
|
263
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
264
|
-
ResourceTypesFhirR4.Observation,
|
|
265
|
-
],
|
|
266
|
-
expectedSmartTokenDecision: 'allowed',
|
|
267
|
-
reason: 'consent is granted at jurisdiction level for physician emergency access',
|
|
268
|
-
}),
|
|
269
|
-
nursingByOrganizationMedicationHistoryAllowed: buildConsentDecisionScenario({
|
|
270
|
-
actorId: { organizationUrl: EXAMPLE_PROVIDER_ORG_URL },
|
|
271
|
-
actorRole: HealthcareActorRoles.NursingProfessional,
|
|
272
|
-
purpose: HealthcareConsentPurposes.Treatment,
|
|
273
|
-
consentActions: [HealthcareBasicSections.HistoryOfMedicationUse.claim],
|
|
274
|
-
requestedSections: HealthcareBasicSections.HistoryOfMedicationUse.claim,
|
|
275
|
-
includedTypes: [
|
|
276
|
-
ResourceTypesFhirR4.Composition,
|
|
277
|
-
ResourceTypesFhirR4.MedicationStatement,
|
|
278
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
279
|
-
],
|
|
280
|
-
expectedSmartTokenDecision: 'allowed',
|
|
281
|
-
reason: 'nursing professional is allowed to read medication history for treatment through organization-scoped consent',
|
|
282
|
-
}),
|
|
283
|
-
paramedicByJurisdictionEmergencySummaryAllowed: buildConsentDecisionScenario({
|
|
284
|
-
actorId: EXAMPLE_JURISDICTION,
|
|
285
|
-
actorRole: HealthcareActorRoles.Paramedic,
|
|
286
|
-
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
287
|
-
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
288
|
-
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
289
|
-
includedTypes: [
|
|
290
|
-
ResourceTypesFhirR4.Composition,
|
|
291
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
292
|
-
ResourceTypesFhirR4.Observation,
|
|
293
|
-
],
|
|
294
|
-
expectedSmartTokenDecision: 'allowed',
|
|
295
|
-
reason: 'paramedic receives emergency-only jurisdiction-scoped access to patient summary',
|
|
296
|
-
}),
|
|
297
|
-
physicianObstetricianDeniedWhenOnlyAllergiesConsent: buildConsentDecisionScenario({
|
|
298
|
-
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
299
|
-
actorRole: `${HealthcareActorRoles.Physician}:obstetrician`,
|
|
300
|
-
purpose: HealthcareConsentPurposes.Treatment,
|
|
301
|
-
consentActions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
|
|
302
|
-
requestedSections: HealthcareBasicSections.Results.claim,
|
|
303
|
-
includedTypes: [
|
|
304
|
-
ResourceTypesFhirR4.Composition,
|
|
305
|
-
ResourceTypesFhirR4.DiagnosticReport,
|
|
306
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
307
|
-
],
|
|
308
|
-
expectedSmartTokenDecision: 'denied',
|
|
309
|
-
reason: 'requested SMART scope targets results but active consent only covers allergies section',
|
|
310
|
-
}),
|
|
311
|
-
physicianByEmailDeniedWhenConsentRevokedAndNoOrgNorJurisdictionConsentIsActive: buildConsentDecisionScenario({
|
|
312
|
-
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
313
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
314
|
-
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
315
|
-
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
316
|
-
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
317
|
-
includedTypes: [
|
|
318
|
-
ResourceTypesFhirR4.Composition,
|
|
319
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
320
|
-
],
|
|
321
|
-
expectedSmartTokenDecision: 'denied',
|
|
322
|
-
reason: 'matching consent rule existed before but is no longer active after controller deactivation/revocation',
|
|
323
|
-
}),
|
|
324
|
-
});
|
|
213
|
+
physicianB
|
package/dist/models/indexing.js
CHANGED
|
@@ -193,7 +193,7 @@ export function buildIndexParametersFromClaims(claims, allowedClaims) {
|
|
|
193
193
|
const parameter = {
|
|
194
194
|
name: claimKey,
|
|
195
195
|
value: value,
|
|
196
|
-
type: inferParameterTypeFromClaimKey(claimKey, rawValue),
|
|
196
|
+
type: inferPa type: inferParameterTypeFromClaimKey(claimKey, rawValue),
|
|
197
197
|
...(claimKey === ObservationClaim.ValueQuantityUnit && typeof rawValue === 'string'
|
|
198
198
|
? { unit: rawValue }
|
|
199
199
|
: {}),
|
|
@@ -234,7 +234,7 @@ export class BundleEntryEditor {
|
|
|
234
234
|
resourceType: this.bundleEditor.getAllowedResourceType() || EmployeeResourceTypes.employee,
|
|
235
235
|
meta: { claims: {} },
|
|
236
236
|
};
|
|
237
|
-
entry.resource.meta = entry.resource.meta || {};
|
|
237
|
+
entry.resource.meta = entry.resource.meta || { claims: {} };
|
|
238
238
|
entry.resource.meta.claims = {
|
|
239
239
|
...(entry.resource.meta.claims || {}),
|
|
240
240
|
[String(key).trim()]: cloneClaimValue(value),
|
|
@@ -264,7 +264,7 @@ export class BundleEntryEditor {
|
|
|
264
264
|
resourceType: this.bundleEditor.getAllowedResourceType() || EmployeeResourceTypes.employee,
|
|
265
265
|
meta: { claims: {} },
|
|
266
266
|
};
|
|
267
|
-
entry.resource.meta = entry.resource.meta || {};
|
|
267
|
+
entry.resource.meta = entry.resource.meta || { claims: {} };
|
|
268
268
|
entry.resource.meta.claims = claims;
|
|
269
269
|
return this;
|
|
270
270
|
}
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import type { BundleEntry, BundleJsonApi } from '../models/bundle.js';
|
|
2
|
+
/** Canonical date interval shared by clinical Bundle read filters. */
|
|
3
|
+
export type BundleResourceDateFilter = Readonly<{
|
|
4
|
+
start?: string;
|
|
5
|
+
end?: string;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Filters Bundle resources by section, resource type and clinical date.
|
|
9
|
+
*
|
|
10
|
+
* Use `types` and `date` in new code. The flat names remain temporary
|
|
11
|
+
* compatibility aliases for callers created before the document readers were
|
|
12
|
+
* aligned with `FhirDocumentFacade`.
|
|
13
|
+
*/
|
|
2
14
|
export type BundleResourceIdFilters = Readonly<{
|
|
3
15
|
sections?: string | readonly string[];
|
|
16
|
+
types?: string | readonly string[];
|
|
17
|
+
date?: BundleResourceDateFilter;
|
|
18
|
+
/** @deprecated Use `types`. */
|
|
4
19
|
resourceTypes?: string | readonly string[];
|
|
20
|
+
/** @deprecated Use `date.start`. */
|
|
5
21
|
dateFrom?: string;
|
|
22
|
+
/** @deprecated Use `date.end`. */
|
|
6
23
|
dateTo?: string;
|
|
7
24
|
}>;
|
|
8
25
|
/**
|
|
@@ -82,7 +82,7 @@ export class BundleQuery {
|
|
|
82
82
|
}
|
|
83
83
|
matchesResourceFilters(entry, filters) {
|
|
84
84
|
const resourceType = asTrimmedString(entry?.resource?.resourceType);
|
|
85
|
-
const resourceTypeFilters = normalizeTokenInput(filters.resourceTypes);
|
|
85
|
+
const resourceTypeFilters = normalizeTokenInput(filters.types !== undefined ? filters.types : filters.resourceTypes);
|
|
86
86
|
if (resourceTypeFilters.length > 0 && !resourceTypeFilters.includes(resourceType)) {
|
|
87
87
|
return false;
|
|
88
88
|
}
|
|
@@ -92,7 +92,9 @@ export class BundleQuery {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
const entryDate = this.resolveEntryDate(claims);
|
|
95
|
-
|
|
95
|
+
const dateFrom = filters.date !== undefined ? filters.date.start : filters.dateFrom;
|
|
96
|
+
const dateTo = filters.date !== undefined ? filters.date.end : filters.dateTo;
|
|
97
|
+
if (!this.matchesDateRange(entryDate, dateFrom, dateTo)) {
|
|
96
98
|
return false;
|
|
97
99
|
}
|
|
98
100
|
return true;
|
|
@@ -116,6 +118,11 @@ export class BundleQuery {
|
|
|
116
118
|
const normalized = String(key || '').toLowerCase();
|
|
117
119
|
if (normalized.endsWith('.date')
|
|
118
120
|
|| normalized.endsWith('.effective')
|
|
121
|
+
|| normalized.endsWith('.effective-datetime')
|
|
122
|
+
|| normalized.endsWith('.effective-period-start')
|
|
123
|
+
|| normalized.endsWith('.onset-datetime')
|
|
124
|
+
|| normalized.endsWith('.occurrence-datetime')
|
|
125
|
+
|| normalized.endsWith('.recorded-date')
|
|
119
126
|
|| normalized.endsWith('.sent')
|
|
120
127
|
|| normalized.endsWith('.authored-on')) {
|
|
121
128
|
const dateValue = asTrimmedString(value);
|
|
@@ -114,6 +114,21 @@ export declare class BundleReader {
|
|
|
114
114
|
getDocumentSectionResourceCount(sectionCodeOrClaim: string): number;
|
|
115
115
|
/** Returns bundle resource references listed under one document section. */
|
|
116
116
|
getDocumentSectionResourceReferences(sectionCodeOrClaim: string): string[];
|
|
117
|
+
/**
|
|
118
|
+
* Returns stable resource IDs that both belong to one Composition section
|
|
119
|
+
* and match the optional resource-type/date filters.
|
|
120
|
+
*
|
|
121
|
+
* Use this after `$summary` when a screen or channel needs the concrete
|
|
122
|
+
* resources for one section. `getDocumentSectionResourceCount(...)` counts
|
|
123
|
+
* declared Composition references; this method resolves those references
|
|
124
|
+
* against the returned Bundle and can narrow the result.
|
|
125
|
+
*/
|
|
126
|
+
getDocumentSectionResourceIds(sectionCodeOrClaim: string, filters?: BundleResourceIdFilters): string[];
|
|
127
|
+
/**
|
|
128
|
+
* Returns cloned Bundle entries for the resources selected from one
|
|
129
|
+
* Composition section, optionally filtered by type and inclusive date range.
|
|
130
|
+
*/
|
|
131
|
+
getDocumentSectionResourceEntries(sectionCodeOrClaim: string, filters?: BundleResourceIdFilters): BundleReaderEntry[];
|
|
117
132
|
/** Returns the active entry response status when present. */
|
|
118
133
|
getEntryResponseStatus(): string | undefined;
|
|
119
134
|
/** Returns all active entry issue severities. */
|
|
@@ -150,6 +165,7 @@ export declare class BundleReader {
|
|
|
150
165
|
private buildEntrySummary;
|
|
151
166
|
private buildSeverityBucket;
|
|
152
167
|
private resolveEntryIdentifier;
|
|
168
|
+
private resolveEntryReferenceCandidates;
|
|
153
169
|
}
|
|
154
170
|
export declare function unwrapBundleLikeResponseBody(input: unknown): Record<string, unknown>;
|
|
155
171
|
export declare function readFirstBundleResourceFromResponseBody(input: unknown): Record<string, unknown> | undefined;
|
|
@@ -235,7 +235,9 @@ export class BundleReader {
|
|
|
235
235
|
if (!normalized) {
|
|
236
236
|
return undefined;
|
|
237
237
|
}
|
|
238
|
-
|
|
238
|
+
const normalizedClaim = normalizeSectionClaim(normalized);
|
|
239
|
+
return this.getDocumentSections().find((section) => section.code === normalized
|
|
240
|
+
|| (section.claim !== undefined && normalizeSectionClaim(section.claim) === normalizedClaim));
|
|
239
241
|
}
|
|
240
242
|
/** Returns the number of resource references inside one document section. */
|
|
241
243
|
getDocumentSectionResourceCount(sectionCodeOrClaim) {
|
|
@@ -245,6 +247,30 @@ export class BundleReader {
|
|
|
245
247
|
getDocumentSectionResourceReferences(sectionCodeOrClaim) {
|
|
246
248
|
return [...(this.getDocumentSectionByCode(sectionCodeOrClaim)?.entryReferences || [])];
|
|
247
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Returns stable resource IDs that both belong to one Composition section
|
|
252
|
+
* and match the optional resource-type/date filters.
|
|
253
|
+
*
|
|
254
|
+
* Use this after `$summary` when a screen or channel needs the concrete
|
|
255
|
+
* resources for one section. `getDocumentSectionResourceCount(...)` counts
|
|
256
|
+
* declared Composition references; this method resolves those references
|
|
257
|
+
* against the returned Bundle and can narrow the result.
|
|
258
|
+
*/
|
|
259
|
+
getDocumentSectionResourceIds(sectionCodeOrClaim, filters = {}) {
|
|
260
|
+
const references = new Set(this.getDocumentSectionResourceReferences(sectionCodeOrClaim));
|
|
261
|
+
if (references.size === 0) {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
return this.getResourceIds(filters).filter((resourceId) => this.getEntriesByIds([resourceId]).some((entry) => this.resolveEntryReferenceCandidates(entry)
|
|
265
|
+
.some((reference) => references.has(reference))));
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Returns cloned Bundle entries for the resources selected from one
|
|
269
|
+
* Composition section, optionally filtered by type and inclusive date range.
|
|
270
|
+
*/
|
|
271
|
+
getDocumentSectionResourceEntries(sectionCodeOrClaim, filters = {}) {
|
|
272
|
+
return this.getEntriesByIds(this.getDocumentSectionResourceIds(sectionCodeOrClaim, filters));
|
|
273
|
+
}
|
|
248
274
|
/** Returns the active entry response status when present. */
|
|
249
275
|
getEntryResponseStatus() {
|
|
250
276
|
const entry = this.getRequiredActiveEntry();
|
|
@@ -434,10 +460,32 @@ export class BundleReader {
|
|
|
434
460
|
.find((key) => String(key || '').toLowerCase().endsWith('.identifier'));
|
|
435
461
|
return identifierKey ? normalizeOptionalString(claims[identifierKey]) : undefined;
|
|
436
462
|
}
|
|
463
|
+
resolveEntryReferenceCandidates(entry) {
|
|
464
|
+
const resource = asRecord(entry.resource);
|
|
465
|
+
const resourceType = asNonEmptyString(resource.resourceType);
|
|
466
|
+
const resourceId = asNonEmptyString(resource.id);
|
|
467
|
+
return Array.from(new Set([
|
|
468
|
+
asNonEmptyString(entry.id),
|
|
469
|
+
asNonEmptyString(entry.fullUrl),
|
|
470
|
+
resourceId,
|
|
471
|
+
resourceType && resourceId ? `${resourceType}/${resourceId}` : undefined,
|
|
472
|
+
this.resolveEntryIdentifier(entry),
|
|
473
|
+
].filter((value) => Boolean(value))));
|
|
474
|
+
}
|
|
437
475
|
}
|
|
438
476
|
function normalizeOptionalString(value) {
|
|
439
477
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
440
478
|
}
|
|
479
|
+
function normalizeSectionClaim(value) {
|
|
480
|
+
const [system, ...codeParts] = value.split('|');
|
|
481
|
+
if (codeParts.length === 0) {
|
|
482
|
+
return value.trim().toLowerCase();
|
|
483
|
+
}
|
|
484
|
+
const normalizedSystem = system.trim().toLowerCase() === 'http://loinc.org'
|
|
485
|
+
? 'loinc'
|
|
486
|
+
: system.trim().toLowerCase();
|
|
487
|
+
return `${normalizedSystem}|${codeParts.join('|').trim().toLowerCase()}`;
|
|
488
|
+
}
|
|
441
489
|
export function unwrapBundleLikeResponseBody(input) {
|
|
442
490
|
const body = input && typeof input === 'object' ? input : {};
|
|
443
491
|
const nested = body.body && typeof body.body === 'object' ? body.body : undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type JWK } from 'jose';
|
|
2
2
|
export type ClientAssertionJwtAlgorithm = 'ES256' | 'ES384' | 'ES512' | 'EdDSA';
|
|
3
3
|
export type BuildClientAssertionJwtInput = {
|
|
4
4
|
clientId: string;
|
|
@@ -34,5 +34,5 @@ export declare function buildClientAssertionJwt(input: BuildClientAssertionJwtIn
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function buildClientAssertionFixture(input: BuildClientAssertionJwtInput): Promise<{
|
|
36
36
|
jwt: string;
|
|
37
|
-
publicJwk:
|
|
37
|
+
publicJwk: JWK;
|
|
38
38
|
}>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BundleResourceDateFilter, BundleResourceIdFilters } from './bundle-query';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical high-level filter accepted by both Bundle section readers and
|
|
4
|
+
* `FhirDocumentFacade`.
|
|
5
|
+
*/
|
|
6
|
+
export type ClinicalResourceFilter = Readonly<{
|
|
7
|
+
sections?: string[];
|
|
8
|
+
types?: string[];
|
|
9
|
+
date?: BundleResourceDateFilter;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Chainable editor for one clinical document resource filter.
|
|
13
|
+
*
|
|
14
|
+
* The built value can be passed unchanged to:
|
|
15
|
+
*
|
|
16
|
+
* - `BundleReader.getDocumentSectionResourceIds(...)`
|
|
17
|
+
* - `BundleReader.getDocumentSectionResourceEntries(...)`
|
|
18
|
+
* - `FhirDocumentFacade.getResourcesByFilter(...)`
|
|
19
|
+
* - `FhirDocumentFacade.getResourceCount(...)`
|
|
20
|
+
*/
|
|
21
|
+
export declare class ClinicalResourceFilterEditor {
|
|
22
|
+
private sections?;
|
|
23
|
+
private types?;
|
|
24
|
+
private date?;
|
|
25
|
+
/** Replaces the selected Composition sections. An empty list means all. */
|
|
26
|
+
setSections(values: readonly string[]): this;
|
|
27
|
+
/** Replaces the selected FHIR resource types. An empty list means all. */
|
|
28
|
+
setTypes(values: readonly string[]): this;
|
|
29
|
+
/** Sets the complete clinical date interval. Either boundary may be omitted. */
|
|
30
|
+
setPeriod(start?: string, end?: string): this;
|
|
31
|
+
/** Sets or clears only the start boundary of the clinical date interval. */
|
|
32
|
+
setPeriodStart(start?: string): this;
|
|
33
|
+
/** Sets or clears only the end boundary of the clinical date interval. */
|
|
34
|
+
setPeriodEnd(end?: string): this;
|
|
35
|
+
/** Removes both clinical date boundaries. */
|
|
36
|
+
clearPeriod(): this;
|
|
37
|
+
/** Returns a detached canonical filter safe for either public reader. */
|
|
38
|
+
build(): ClinicalResourceFilter & BundleResourceIdFilters;
|
|
39
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
+
/**
|
|
3
|
+
* Chainable editor for one clinical document resource filter.
|
|
4
|
+
*
|
|
5
|
+
* The built value can be passed unchanged to:
|
|
6
|
+
*
|
|
7
|
+
* - `BundleReader.getDocumentSectionResourceIds(...)`
|
|
8
|
+
* - `BundleReader.getDocumentSectionResourceEntries(...)`
|
|
9
|
+
* - `FhirDocumentFacade.getResourcesByFilter(...)`
|
|
10
|
+
* - `FhirDocumentFacade.getResourceCount(...)`
|
|
11
|
+
*/
|
|
12
|
+
export class ClinicalResourceFilterEditor {
|
|
13
|
+
sections;
|
|
14
|
+
types;
|
|
15
|
+
date;
|
|
16
|
+
/** Replaces the selected Composition sections. An empty list means all. */
|
|
17
|
+
setSections(values) {
|
|
18
|
+
this.sections = normalizeTokens(values);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
/** Replaces the selected FHIR resource types. An empty list means all. */
|
|
22
|
+
setTypes(values) {
|
|
23
|
+
this.types = normalizeTokens(values);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
/** Sets the complete clinical date interval. Either boundary may be omitted. */
|
|
27
|
+
setPeriod(start, end) {
|
|
28
|
+
this.date = buildDateFilter(start, end);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
/** Sets or clears only the start boundary of the clinical date interval. */
|
|
32
|
+
setPeriodStart(start) {
|
|
33
|
+
this.date = buildDateFilter(start, this.date?.end);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
/** Sets or clears only the end boundary of the clinical date interval. */
|
|
37
|
+
setPeriodEnd(end) {
|
|
38
|
+
this.date = buildDateFilter(this.date?.start, end);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
/** Removes both clinical date boundaries. */
|
|
42
|
+
clearPeriod() {
|
|
43
|
+
this.date = undefined;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/** Returns a detached canonical filter safe for either public reader. */
|
|
47
|
+
build() {
|
|
48
|
+
return {
|
|
49
|
+
...(this.sections?.length ? { sections: [...this.sections] } : {}),
|
|
50
|
+
...(this.types?.length ? { types: [...this.types] } : {}),
|
|
51
|
+
...(this.date ? { date: { ...this.date } } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function normalizeTokens(values) {
|
|
56
|
+
const normalized = [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
|
|
57
|
+
return normalized.length ? normalized : undefined;
|
|
58
|
+
}
|
|
59
|
+
function buildDateFilter(start, end) {
|
|
60
|
+
const normalizedStart = normalizeDate(start, 'start');
|
|
61
|
+
const normalizedEnd = normalizeDate(end, 'end');
|
|
62
|
+
if (normalizedStart && normalizedEnd) {
|
|
63
|
+
if (Date.parse(normalizedStart) > Date.parse(normalizedEnd)) {
|
|
64
|
+
throw new RangeError('Clinical resource filter period start must not be after end.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!normalizedStart && !normalizedEnd) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...(normalizedStart ? { start: normalizedStart } : {}),
|
|
72
|
+
...(normalizedEnd ? { end: normalizedEnd } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function normalizeDate(value, boundary) {
|
|
76
|
+
const normalized = String(value || '').trim();
|
|
77
|
+
if (!normalized) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (!Number.isFinite(Date.parse(normalized))) {
|
|
81
|
+
throw new TypeError(`Clinical resource filter period ${boundary} must be a valid date.`);
|
|
82
|
+
}
|
|
83
|
+
return normalized;
|
|
84
|
+
}
|
|
@@ -150,9 +150,10 @@ export declare function buildBundleSearchReferenceUrl(input: Readonly<{
|
|
|
150
150
|
/**
|
|
151
151
|
* Creates the canonical semantic parameters for an IPS summary-style request.
|
|
152
152
|
*
|
|
153
|
-
* These parameters are the source of truth.
|
|
154
|
-
*
|
|
155
|
-
* them
|
|
153
|
+
* These parameters are the source of truth. The canonical `$summary` read
|
|
154
|
+
* attaches them as one FHIR `Parameters` resource to an auditable
|
|
155
|
+
* `Communication`. Flattening them into a `Bundle/_search` reference is a
|
|
156
|
+
* compatibility path and must not be taught as the primary 101 read flow.
|
|
156
157
|
*/
|
|
157
158
|
export declare function createSummaryOperationRequestParameters(subjectIdOrInput: string | CreateSummaryOperationParametersInput, filterSections?: string[]): ReadonlyArray<ParameterData>;
|
|
158
159
|
/**
|
|
@@ -161,8 +162,8 @@ export declare function createSummaryOperationRequestParameters(subjectIdOrInput
|
|
|
161
162
|
*/
|
|
162
163
|
export declare function createSummaryOperationRequestReferencePath(parameters: ReadonlyArray<ParameterData>): string;
|
|
163
164
|
/**
|
|
164
|
-
* Builds the
|
|
165
|
-
*
|
|
165
|
+
* Builds the canonical FHIR `Parameters` body attached to a `$summary`
|
|
166
|
+
* request `Communication`.
|
|
166
167
|
*/
|
|
167
168
|
export declare function createSummaryOperationRequestParametersResource(parameters: ReadonlyArray<ParameterData>): FhirParametersResource;
|
|
168
169
|
export declare function buildCommunicationRequestOperationWithAttachedParametersClaims(input: CreateSummaryOperationCommunicationInput): Record<string, unknown>;
|
|
@@ -188,11 +189,11 @@ export declare const flattenParametersToSearchReference: typeof createSummaryOpe
|
|
|
188
189
|
* carried inside `Communication`.
|
|
189
190
|
*
|
|
190
191
|
* Current split:
|
|
191
|
-
* - `
|
|
192
|
-
* search-url contract
|
|
193
|
-
* - `setRequestSummaryOperation(...)` builds the operation contract where
|
|
192
|
+
* - `setRequestSummaryOperation(...)` is the canonical 101 read contract where
|
|
194
193
|
* `content-reference` points to the operation path and
|
|
195
194
|
* `content-attachment-data` carries the serialized FHIR `Parameters`
|
|
195
|
+
* - `newSearchWithReferencePath(...)` keeps the older flattened `_search`
|
|
196
|
+
* compatibility contract
|
|
196
197
|
*/
|
|
197
198
|
export declare const communication: Readonly<{
|
|
198
199
|
/**
|
|
@@ -217,9 +217,10 @@ export function buildBundleSearchReferenceUrl(input) {
|
|
|
217
217
|
/**
|
|
218
218
|
* Creates the canonical semantic parameters for an IPS summary-style request.
|
|
219
219
|
*
|
|
220
|
-
* These parameters are the source of truth.
|
|
221
|
-
*
|
|
222
|
-
* them
|
|
220
|
+
* These parameters are the source of truth. The canonical `$summary` read
|
|
221
|
+
* attaches them as one FHIR `Parameters` resource to an auditable
|
|
222
|
+
* `Communication`. Flattening them into a `Bundle/_search` reference is a
|
|
223
|
+
* compatibility path and must not be taught as the primary 101 read flow.
|
|
223
224
|
*/
|
|
224
225
|
export function createSummaryOperationRequestParameters(subjectIdOrInput, filterSections) {
|
|
225
226
|
const input = typeof subjectIdOrInput === 'string'
|
|
@@ -234,10 +235,14 @@ export function createSummaryOperationRequestParameters(subjectIdOrInput, filter
|
|
|
234
235
|
if (!documentTypeDescriptor) {
|
|
235
236
|
throw new Error(`Unsupported documentType: ${String(documentType)}`);
|
|
236
237
|
}
|
|
238
|
+
const sections = normalizeStringArray(input.filterSections);
|
|
239
|
+
if (sections.includes('*')) {
|
|
240
|
+
throw new Error('Omit filterSections to request all available sections; "*" is reserved for SMART permission scopes.');
|
|
241
|
+
}
|
|
237
242
|
return [
|
|
238
243
|
buildSubjectParameter(subjectDid),
|
|
239
244
|
buildDocumentTypeParameter(documentTypeDescriptor.id, documentTypeDescriptor.attributeValue),
|
|
240
|
-
...buildSectionParameters(
|
|
245
|
+
...buildSectionParameters(sections),
|
|
241
246
|
];
|
|
242
247
|
}
|
|
243
248
|
/**
|
|
@@ -267,8 +272,8 @@ export function createSummaryOperationRequestReferencePath(parameters) {
|
|
|
267
272
|
return `individual/org.hl7.fhir.r4/Bundle/_search?${params.filter(Boolean).join('&')}`;
|
|
268
273
|
}
|
|
269
274
|
/**
|
|
270
|
-
* Builds the
|
|
271
|
-
*
|
|
275
|
+
* Builds the canonical FHIR `Parameters` body attached to a `$summary`
|
|
276
|
+
* request `Communication`.
|
|
272
277
|
*/
|
|
273
278
|
export function createSummaryOperationRequestParametersResource(parameters) {
|
|
274
279
|
return buildFhirParametersResourceFromParameterData(parameters);
|
|
@@ -348,11 +353,11 @@ export const flattenParametersToSearchReference = createSummaryOperationRequestR
|
|
|
348
353
|
* carried inside `Communication`.
|
|
349
354
|
*
|
|
350
355
|
* Current split:
|
|
351
|
-
* - `
|
|
352
|
-
* search-url contract
|
|
353
|
-
* - `setRequestSummaryOperation(...)` builds the operation contract where
|
|
356
|
+
* - `setRequestSummaryOperation(...)` is the canonical 101 read contract where
|
|
354
357
|
* `content-reference` points to the operation path and
|
|
355
358
|
* `content-attachment-data` carries the serialized FHIR `Parameters`
|
|
359
|
+
* - `newSearchWithReferencePath(...)` keeps the older flattened `_search`
|
|
360
|
+
* compatibility contract
|
|
356
361
|
*/
|
|
357
362
|
export const communication = Object.freeze({
|
|
358
363
|
/**
|
|
@@ -35,7 +35,8 @@ export function transformCommunicationClaimsToResourceFhirR4(communicationClaims
|
|
|
35
35
|
const hasReference = Boolean(payloadReference);
|
|
36
36
|
const hasCode = Boolean(payloadCodeRaw);
|
|
37
37
|
const payloadKinds = [hasAttachment, hasReference, hasCode].filter(Boolean).length;
|
|
38
|
-
|
|
38
|
+
const isOperationReferenceWithParameters = hasAttachment && hasReference && !hasCode;
|
|
39
|
+
if (payloadKinds > 1 && !isOperationReferenceWithParameters) {
|
|
39
40
|
const msg = `Communication[${index}] has more than one payload kind (attachment/reference/code).`;
|
|
40
41
|
if (mode === 'strict')
|
|
41
42
|
throw new Error(msg);
|
|
@@ -50,10 +51,10 @@ export function transformCommunicationClaimsToResourceFhirR4(communicationClaims
|
|
|
50
51
|
throw new Error(msg);
|
|
51
52
|
warnings.push(`${msg} Keeping first note only.`);
|
|
52
53
|
}
|
|
53
|
-
const payload =
|
|
54
|
+
const payload = buildPayloads({
|
|
54
55
|
hasAttachment,
|
|
55
|
-
hasReference,
|
|
56
|
-
hasCode,
|
|
56
|
+
hasReference: isOperationReferenceWithParameters || (!hasAttachment && hasReference),
|
|
57
|
+
hasCode: !hasAttachment && !hasReference && hasCode,
|
|
57
58
|
payloadAttachmentData,
|
|
58
59
|
payloadAttachmentType,
|
|
59
60
|
payloadAttachmentTitle,
|
|
@@ -95,8 +96,8 @@ export function transformCommunicationClaimsToResourceFhirR4(communicationClaims
|
|
|
95
96
|
resource['sender'] = { reference: sender };
|
|
96
97
|
if (partOf)
|
|
97
98
|
resource['partOf'] = [{ reference: partOf }];
|
|
98
|
-
if (payload)
|
|
99
|
-
resource['payload'] =
|
|
99
|
+
if (payload.length)
|
|
100
|
+
resource['payload'] = payload;
|
|
100
101
|
if (noteValues.length)
|
|
101
102
|
resource['note'] = [{ text: noteValues[0] }];
|
|
102
103
|
return resource;
|
|
@@ -126,10 +127,13 @@ export function extractCommunicationClaimsFromResourceFhirR4(resource, options =
|
|
|
126
127
|
const partOfRef = resource?.partOf?.[0]?.reference;
|
|
127
128
|
const noteText = resource?.note?.[0]?.text;
|
|
128
129
|
const categoryCoding = resource?.category?.[0]?.coding?.[0];
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
130
|
+
const payloads = resource?.payload || [];
|
|
131
|
+
const referencePayload = payloads.find((payload) => payload.contentReference !== undefined);
|
|
132
|
+
const attachmentPayload = payloads.find((payload) => payload.contentAttachment !== undefined);
|
|
133
|
+
const codePayload = payloads.find((payload) => payload.contentCodeableConcept !== undefined);
|
|
134
|
+
const contentReference = referencePayload?.contentReference?.reference;
|
|
135
|
+
const contentAttachment = attachmentPayload?.contentAttachment;
|
|
136
|
+
const contentCodeableConcept = codePayload?.contentCodeableConcept?.coding?.[0];
|
|
133
137
|
setIf(claims, CommunicationClaim.Identifier, identifierValue);
|
|
134
138
|
setIf(claims, CommunicationClaim.Status, status);
|
|
135
139
|
setIf(claims, CommunicationClaim.Sent, sent);
|
|
@@ -177,8 +181,12 @@ function normalizeNoteValues(raw) {
|
|
|
177
181
|
}
|
|
178
182
|
return [];
|
|
179
183
|
}
|
|
180
|
-
function
|
|
184
|
+
function buildPayloads(input) {
|
|
181
185
|
const { hasAttachment, hasReference, hasCode, payloadAttachmentData, payloadAttachmentType, payloadAttachmentTitle, payloadAttachmentUrl, payloadReference, payloadCodeRaw, } = input;
|
|
186
|
+
const payloads = [];
|
|
187
|
+
if (hasReference) {
|
|
188
|
+
payloads.push({ contentReference: { reference: payloadReference } });
|
|
189
|
+
}
|
|
182
190
|
if (hasAttachment) {
|
|
183
191
|
const value = {};
|
|
184
192
|
if (payloadAttachmentData)
|
|
@@ -189,13 +197,12 @@ function buildPayload(input) {
|
|
|
189
197
|
value['title'] = payloadAttachmentTitle;
|
|
190
198
|
if (payloadAttachmentUrl)
|
|
191
199
|
value['url'] = payloadAttachmentUrl;
|
|
192
|
-
|
|
200
|
+
payloads.push({ contentAttachment: value });
|
|
201
|
+
}
|
|
202
|
+
if (hasCode && payloadCodeRaw) {
|
|
203
|
+
payloads.push({ contentCodeableConcept: { coding: [parseSystemCode(payloadCodeRaw)] } });
|
|
193
204
|
}
|
|
194
|
-
|
|
195
|
-
return { contentReference: { reference: payloadReference } };
|
|
196
|
-
if (hasCode && payloadCodeRaw)
|
|
197
|
-
return { contentCodeableConcept: { coding: [parseSystemCode(payloadCodeRaw)] } };
|
|
198
|
-
return undefined;
|
|
205
|
+
return payloads;
|
|
199
206
|
}
|
|
200
207
|
function parseSystemCode(value) {
|
|
201
208
|
const trimmed = String(value || '').trim();
|