gdc-common-utils-ts 1.14.13 → 1.14.15
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 +20 -7
- package/dist/examples/communication-attached-bundle-session.d.ts +22 -0
- package/dist/examples/communication-attached-bundle-session.js +79 -0
- package/dist/examples/employee.d.ts +41 -0
- package/dist/examples/employee.js +39 -0
- package/dist/examples/index.d.ts +2 -1
- package/dist/examples/index.js +2 -1
- package/dist/examples/ips-bundle.js +3 -3
- package/dist/models/bundle.d.ts +20 -2
- package/dist/models/bundle.js +8 -1
- package/dist/models/comm.d.ts +29 -0
- package/dist/models/confidential-message.d.ts +30 -2
- package/dist/models/interoperable-claims/diagnostic-report-claims.d.ts +1 -1
- package/dist/models/interoperable-claims/diagnostic-report-claims.js +1 -1
- package/dist/utils/communication-attached-bundle-session.d.ts +191 -0
- package/dist/utils/communication-attached-bundle-session.js +542 -0
- package/dist/utils/communication-bundle-document-request.d.ts +6 -0
- package/dist/utils/communication-bundle-document-request.js +8 -0
- package/dist/utils/employee.d.ts +87 -0
- package/dist/utils/employee.js +134 -0
- package/dist/utils/fhir-search.d.ts +26 -0
- package/dist/utils/fhir-search.js +84 -0
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
// Copyright 2026 Conectate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
|
|
2
|
+
// Always create JSDoc, do not use strings inline in keys nor values, use types instead, and reuse the data test examples.
|
|
3
|
+
import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
|
|
4
|
+
import { ClaimConsent } from '../models/consent-rule.js';
|
|
5
|
+
import { AllergyIntoleranceClaim } from '../models/interoperable-claims/allergy-intolerance-claims.js';
|
|
6
|
+
import { CommunicationClaim } from '../models/interoperable-claims/communication-claims.js';
|
|
7
|
+
import { ConditionClaim } from '../models/interoperable-claims/condition-claims.js';
|
|
8
|
+
import { DocumentReferenceClaim } from '../models/interoperable-claims/document-reference-claims.js';
|
|
9
|
+
import { BundleQuery } from './bundle-query.js';
|
|
10
|
+
import { addClaimValues } from '../claims/claim-list-helpers.js';
|
|
11
|
+
import { MedicationStatementClaim, } from '../models/interoperable-claims/medication-statement-claims.js';
|
|
12
|
+
/**
|
|
13
|
+
* Communication editing session with bundle-in-memory as source of truth.
|
|
14
|
+
*
|
|
15
|
+
* Design contract:
|
|
16
|
+
* - `activeEntry` is the real editing unit (not only `activeResource`), because
|
|
17
|
+
* it can include `fullUrl`, `request`, and entry-level context.
|
|
18
|
+
* - `Communication.content-attachment-data` is always derived from the
|
|
19
|
+
* in-memory bundle after each committed update.
|
|
20
|
+
* - saving can release active entry memory via `saveAndReleaseActiveEntry()`.
|
|
21
|
+
*/
|
|
22
|
+
export class CommunicationAttachedBundleSession {
|
|
23
|
+
communicationClaims;
|
|
24
|
+
bundleInMemory;
|
|
25
|
+
activeEntryIndex;
|
|
26
|
+
mode;
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.mode = options.mode || 'strict';
|
|
29
|
+
this.communicationClaims = {
|
|
30
|
+
...options.communicationClaims,
|
|
31
|
+
};
|
|
32
|
+
this.activeEntryIndex = null;
|
|
33
|
+
const providedBundle = options.initialBundle ? cloneBundle(options.initialBundle) : undefined;
|
|
34
|
+
this.bundleInMemory = providedBundle || this.decodeBundleFromClaims(this.communicationClaims);
|
|
35
|
+
this.syncAttachmentFromBundle();
|
|
36
|
+
}
|
|
37
|
+
/** Returns a deep copy of communication claims. */
|
|
38
|
+
getCommunicationClaims() {
|
|
39
|
+
return {
|
|
40
|
+
...this.communicationClaims,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Returns a deep copy of the current in-memory bundle. */
|
|
44
|
+
getBundleInMemory() {
|
|
45
|
+
return cloneBundle(this.bundleInMemory);
|
|
46
|
+
}
|
|
47
|
+
/** Returns the active entry index, or null when no entry is selected. */
|
|
48
|
+
getActiveEntryIndex() {
|
|
49
|
+
return this.activeEntryIndex;
|
|
50
|
+
}
|
|
51
|
+
/** Returns a deep copy of the active entry when selected. */
|
|
52
|
+
getActiveEntry() {
|
|
53
|
+
if (this.activeEntryIndex === null) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return cloneEntry(this.bundleInMemory.data[this.activeEntryIndex]);
|
|
57
|
+
}
|
|
58
|
+
/** Selects an active entry by index or fullUrl. */
|
|
59
|
+
selectActiveEntry(selection) {
|
|
60
|
+
if (typeof selection.index === 'number') {
|
|
61
|
+
this.assertEntryIndex(selection.index);
|
|
62
|
+
this.activeEntryIndex = selection.index;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
if (selection.fullUrl) {
|
|
66
|
+
const foundIndex = this.bundleInMemory.data.findIndex((entry) => String(entry?.fullUrl || '').trim() === selection.fullUrl);
|
|
67
|
+
if (foundIndex < 0) {
|
|
68
|
+
throw new Error(`Active entry not found for fullUrl: ${selection.fullUrl}`);
|
|
69
|
+
}
|
|
70
|
+
this.activeEntryIndex = foundIndex;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
throw new Error('selectActiveEntry requires either index or fullUrl.');
|
|
74
|
+
}
|
|
75
|
+
/** Clears active entry selection from memory. */
|
|
76
|
+
clearActiveEntry() {
|
|
77
|
+
this.activeEntryIndex = null;
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Upserts an entry in bundle memory and marks it as active.
|
|
82
|
+
* Matching priority: `fullUrl` if present, then resource claim identifier.
|
|
83
|
+
*/
|
|
84
|
+
upsertActiveEntry(input) {
|
|
85
|
+
const entry = this.createBundleEntry(input);
|
|
86
|
+
const nextIndex = this.findUpsertIndex(entry, input.fullUrl);
|
|
87
|
+
if (nextIndex >= 0) {
|
|
88
|
+
this.bundleInMemory.data[nextIndex] = entry;
|
|
89
|
+
this.activeEntryIndex = nextIndex;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.bundleInMemory.data.push(entry);
|
|
93
|
+
this.activeEntryIndex = this.bundleInMemory.data.length - 1;
|
|
94
|
+
}
|
|
95
|
+
this.syncAttachmentFromBundle();
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Consent-first helper for developer onboarding.
|
|
100
|
+
*
|
|
101
|
+
* Expected keys should come from `ClaimConsent` in caller code.
|
|
102
|
+
*/
|
|
103
|
+
upsertActiveConsentEntry(input) {
|
|
104
|
+
return this.upsertActiveEntry({
|
|
105
|
+
resourceType: ResourceTypesFhirR4.Consent,
|
|
106
|
+
claims: input.claims,
|
|
107
|
+
fullUrl: input.fullUrl,
|
|
108
|
+
type: input.type,
|
|
109
|
+
request: input.request,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* MedicationStatement helper for IPS-in-Communication use cases.
|
|
114
|
+
*
|
|
115
|
+
* Expected keys should come from MedicationStatement claims constants.
|
|
116
|
+
*/
|
|
117
|
+
upsertActiveMedicationStatementEntry(input) {
|
|
118
|
+
return this.upsertActiveEntry({
|
|
119
|
+
resourceType: ResourceTypesFhirR4.MedicationStatement,
|
|
120
|
+
claims: {
|
|
121
|
+
...input.claims,
|
|
122
|
+
},
|
|
123
|
+
fullUrl: input.fullUrl,
|
|
124
|
+
type: input.type,
|
|
125
|
+
request: input.request,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* DocumentReference helper for bundle-contained attachments linked from
|
|
130
|
+
* other clinical resources through `*.contained-documents`.
|
|
131
|
+
*/
|
|
132
|
+
upsertActiveDocumentReferenceEntry(input) {
|
|
133
|
+
return this.upsertActiveEntry({
|
|
134
|
+
resourceType: ResourceTypesFhirR4.DocumentReference,
|
|
135
|
+
claims: {
|
|
136
|
+
...input.claims,
|
|
137
|
+
},
|
|
138
|
+
fullUrl: input.fullUrl,
|
|
139
|
+
type: input.type,
|
|
140
|
+
request: input.request,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Condition helper for IPS-in-Communication use cases.
|
|
145
|
+
*
|
|
146
|
+
* Expected keys should come from Condition claims constants.
|
|
147
|
+
*/
|
|
148
|
+
upsertActiveConditionEntry(input) {
|
|
149
|
+
return this.upsertActiveEntry({
|
|
150
|
+
resourceType: ResourceTypesFhirR4.Condition,
|
|
151
|
+
claims: {
|
|
152
|
+
...input.claims,
|
|
153
|
+
},
|
|
154
|
+
fullUrl: input.fullUrl,
|
|
155
|
+
type: input.type,
|
|
156
|
+
request: input.request,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* AllergyIntolerance helper for IPS-in-Communication use cases.
|
|
161
|
+
*
|
|
162
|
+
* Expected keys should come from AllergyIntolerance claims constants.
|
|
163
|
+
*/
|
|
164
|
+
upsertActiveAllergyIntoleranceEntry(input) {
|
|
165
|
+
return this.upsertActiveEntry({
|
|
166
|
+
resourceType: ResourceTypesFhirR4.AllergyIntolerance,
|
|
167
|
+
claims: {
|
|
168
|
+
...input.claims,
|
|
169
|
+
},
|
|
170
|
+
fullUrl: input.fullUrl,
|
|
171
|
+
type: input.type,
|
|
172
|
+
request: input.request,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* TODO(ips-next):
|
|
177
|
+
* Add `upsertActiveDiagnosticReportEntry(...)` once the shared claim helpers
|
|
178
|
+
* for `DiagnosticReport` are in place.
|
|
179
|
+
*
|
|
180
|
+
* Expected shape should mirror the existing resource helpers:
|
|
181
|
+
* - `claims` authored with `@context = org.hl7.fhir.api`
|
|
182
|
+
* - matching priority by `DiagnosticReport.identifier`
|
|
183
|
+
* - support for linked `DocumentReference` ids through
|
|
184
|
+
* `DiagnosticReport.contained-documents`
|
|
185
|
+
*
|
|
186
|
+
* Intentionally not implemented in this pass:
|
|
187
|
+
* - IPS authoring already works for the currently documented resources
|
|
188
|
+
* - GW Core can already consume bundle-contained `DocumentReference` rows
|
|
189
|
+
* - adding the DiagnosticReport editing surface now would expand the IPS
|
|
190
|
+
* contract further than intended for this release slice
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* Creates or updates a linked `DocumentReference` entry and stores its
|
|
194
|
+
* identifier under the active resource `*.contained-documents` claim.
|
|
195
|
+
*/
|
|
196
|
+
addContainedDocumentToActiveEntry(input) {
|
|
197
|
+
if (this.activeEntryIndex === null) {
|
|
198
|
+
throw new Error('No active entry selected.');
|
|
199
|
+
}
|
|
200
|
+
const parentIndex = this.activeEntryIndex;
|
|
201
|
+
const parentEntry = cloneEntry(this.bundleInMemory.data[parentIndex]);
|
|
202
|
+
const parentResource = ensureEntryResource(parentEntry, this.mode);
|
|
203
|
+
const parentClaims = {
|
|
204
|
+
...(parentResource.meta?.claims || {}),
|
|
205
|
+
};
|
|
206
|
+
const parentResourceType = asTrimmedString(parentResource.resourceType);
|
|
207
|
+
const containedDocumentsClaimKey = resolveContainedDocumentsClaimKey(parentResourceType);
|
|
208
|
+
if (!containedDocumentsClaimKey) {
|
|
209
|
+
throw new Error(`Contained documents are not supported for resourceType: ${parentResourceType || 'unknown'}`);
|
|
210
|
+
}
|
|
211
|
+
const documentIdentifier = asTrimmedString(input.identifier)
|
|
212
|
+
|| asTrimmedString(input.claims?.[DocumentReferenceClaim.Identifier])
|
|
213
|
+
|| runtimeUuid('docref');
|
|
214
|
+
const documentSubject = asTrimmedString(input.claims?.[DocumentReferenceClaim.Subject])
|
|
215
|
+
|| resolveSubjectFromClaims(parentClaims)
|
|
216
|
+
|| asTrimmedString(this.communicationClaims[CommunicationClaim.Subject]);
|
|
217
|
+
const documentClaims = {
|
|
218
|
+
'@context': 'org.hl7.fhir.api',
|
|
219
|
+
...(input.claims || {}),
|
|
220
|
+
[DocumentReferenceClaim.Identifier]: documentIdentifier,
|
|
221
|
+
};
|
|
222
|
+
if (documentSubject) {
|
|
223
|
+
documentClaims[DocumentReferenceClaim.Subject] = documentSubject;
|
|
224
|
+
}
|
|
225
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.ContentType, input.attachmentContentType);
|
|
226
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.ContentData, input.attachmentDataBase64);
|
|
227
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.Location, input.attachmentUrl);
|
|
228
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.Description, input.description);
|
|
229
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.Date, input.date);
|
|
230
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.Language, input.language);
|
|
231
|
+
this.upsertActiveDocumentReferenceEntry({
|
|
232
|
+
claims: documentClaims,
|
|
233
|
+
fullUrl: input.fullUrl || `urn:uuid:${documentIdentifier}`,
|
|
234
|
+
});
|
|
235
|
+
parentResource.meta = parentResource.meta || {};
|
|
236
|
+
parentResource.meta.claims = addClaimValues(parentClaims, containedDocumentsClaimKey, [documentIdentifier]);
|
|
237
|
+
parentEntry.resource = parentResource;
|
|
238
|
+
this.bundleInMemory.data[parentIndex] = parentEntry;
|
|
239
|
+
this.activeEntryIndex = parentIndex;
|
|
240
|
+
this.syncAttachmentFromBundle();
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Patches active entry `resource.meta.claims` and synchronizes attachment data.
|
|
245
|
+
*/
|
|
246
|
+
patchActiveEntryClaims(claimPatch) {
|
|
247
|
+
if (this.activeEntryIndex === null) {
|
|
248
|
+
throw new Error('No active entry selected.');
|
|
249
|
+
}
|
|
250
|
+
const current = cloneEntry(this.bundleInMemory.data[this.activeEntryIndex]);
|
|
251
|
+
const resource = ensureEntryResource(current, this.mode);
|
|
252
|
+
resource.meta = resource.meta || {};
|
|
253
|
+
resource.meta.claims = {
|
|
254
|
+
...(resource.meta.claims || {}),
|
|
255
|
+
...claimPatch,
|
|
256
|
+
};
|
|
257
|
+
current.resource = resource;
|
|
258
|
+
this.bundleInMemory.data[this.activeEntryIndex] = current;
|
|
259
|
+
this.syncAttachmentFromBundle();
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Persists current memory state into communication claims attachment.
|
|
264
|
+
* No-op for active entry pointer.
|
|
265
|
+
*/
|
|
266
|
+
saveActiveEntry() {
|
|
267
|
+
this.syncAttachmentFromBundle();
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Persists and releases active entry memory pointer.
|
|
272
|
+
* This is the recommended step after a successful save operation.
|
|
273
|
+
*/
|
|
274
|
+
saveAndReleaseActiveEntry() {
|
|
275
|
+
this.syncAttachmentFromBundle();
|
|
276
|
+
this.clearActiveEntry();
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Returns stable resource IDs from bundle entries with optional filters.
|
|
281
|
+
*/
|
|
282
|
+
getResourceIds(filters = {}) {
|
|
283
|
+
const query = new BundleQuery(this.bundleInMemory);
|
|
284
|
+
return query.getResourceIds(filters);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Returns bundle entries matching resource IDs produced by `getResourceIds`.
|
|
288
|
+
*/
|
|
289
|
+
getResourceEntriesByIds(resourceIds) {
|
|
290
|
+
const query = new BundleQuery(this.bundleInMemory);
|
|
291
|
+
return query.getResourceEntriesByIds(resourceIds);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Resolves the entry URL (`fullUrl`) for a given entry/resource identifier.
|
|
295
|
+
*/
|
|
296
|
+
getEntryUrl(entryId) {
|
|
297
|
+
const query = new BundleQuery(this.bundleInMemory);
|
|
298
|
+
return query.getEntryUrl(entryId);
|
|
299
|
+
}
|
|
300
|
+
decodeBundleFromClaims(claims) {
|
|
301
|
+
const encoded = asTrimmedString(claims[CommunicationClaim.ContentAttachmentData]);
|
|
302
|
+
if (!encoded) {
|
|
303
|
+
return createEmptyBundle();
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const text = Buffer.from(encoded, 'base64').toString('utf8');
|
|
307
|
+
const parsed = JSON.parse(text);
|
|
308
|
+
validateBundleLike(parsed, this.mode);
|
|
309
|
+
return cloneBundle(parsed);
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (this.mode === 'normalize') {
|
|
313
|
+
return createEmptyBundle();
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`Invalid ${CommunicationClaim.ContentAttachmentData}: ${error.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
syncAttachmentFromBundle() {
|
|
319
|
+
this.communicationClaims[CommunicationClaim.ContentAttachmentType] = 'application/fhir+json';
|
|
320
|
+
this.communicationClaims[CommunicationClaim.ContentAttachmentData] = encodeBundleToBase64(this.bundleInMemory);
|
|
321
|
+
const activeSubject = this.resolveCurrentSubject();
|
|
322
|
+
if (activeSubject) {
|
|
323
|
+
this.communicationClaims[CommunicationClaim.Subject] = activeSubject;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
resolveCurrentSubject() {
|
|
327
|
+
if (this.activeEntryIndex !== null) {
|
|
328
|
+
const claims = this.bundleInMemory.data[this.activeEntryIndex]?.resource?.meta?.claims || {};
|
|
329
|
+
const consentSubject = asTrimmedString(claims[ClaimConsent.subject]);
|
|
330
|
+
if (consentSubject) {
|
|
331
|
+
return consentSubject;
|
|
332
|
+
}
|
|
333
|
+
const medicationSubject = asTrimmedString(claims[MedicationStatementClaim.Subject]);
|
|
334
|
+
if (medicationSubject) {
|
|
335
|
+
return medicationSubject;
|
|
336
|
+
}
|
|
337
|
+
const conditionSubject = asTrimmedString(claims[ConditionClaim.Subject]);
|
|
338
|
+
if (conditionSubject) {
|
|
339
|
+
return conditionSubject;
|
|
340
|
+
}
|
|
341
|
+
const allergySubject = asTrimmedString(claims[AllergyIntoleranceClaim.Subject] || claims[AllergyIntoleranceClaim.Patient]);
|
|
342
|
+
if (allergySubject) {
|
|
343
|
+
return allergySubject;
|
|
344
|
+
}
|
|
345
|
+
const documentReferenceSubject = asTrimmedString(claims[DocumentReferenceClaim.Subject]);
|
|
346
|
+
if (documentReferenceSubject) {
|
|
347
|
+
return documentReferenceSubject;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const fromClaims = asTrimmedString(this.communicationClaims[CommunicationClaim.Subject]);
|
|
351
|
+
return fromClaims || undefined;
|
|
352
|
+
}
|
|
353
|
+
findUpsertIndex(entry, fullUrl) {
|
|
354
|
+
if (fullUrl) {
|
|
355
|
+
const byFullUrl = this.bundleInMemory.data.findIndex((item) => String(item?.fullUrl || '').trim() === fullUrl);
|
|
356
|
+
if (byFullUrl >= 0) {
|
|
357
|
+
return byFullUrl;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const incomingClaims = entry.resource?.meta?.claims || {};
|
|
361
|
+
const incomingIdentifier = this.resolveEntryIdentifier(incomingClaims);
|
|
362
|
+
if (!incomingIdentifier) {
|
|
363
|
+
return -1;
|
|
364
|
+
}
|
|
365
|
+
return this.bundleInMemory.data.findIndex((item) => {
|
|
366
|
+
const itemClaims = item?.resource?.meta?.claims || {};
|
|
367
|
+
return this.resolveEntryIdentifier(itemClaims) === incomingIdentifier;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
resolveEntryIdentifier(claims) {
|
|
371
|
+
const consentIdentifier = asTrimmedString(claims[ClaimConsent.identifier]);
|
|
372
|
+
if (consentIdentifier) {
|
|
373
|
+
return `${ResourceTypesFhirR4.Consent}:${consentIdentifier}`;
|
|
374
|
+
}
|
|
375
|
+
const medicationIdentifier = asTrimmedString(claims[MedicationStatementClaim.Identifier]);
|
|
376
|
+
if (medicationIdentifier) {
|
|
377
|
+
return `${ResourceTypesFhirR4.MedicationStatement}:${medicationIdentifier}`;
|
|
378
|
+
}
|
|
379
|
+
const conditionIdentifier = asTrimmedString(claims[ConditionClaim.Identifier]);
|
|
380
|
+
if (conditionIdentifier) {
|
|
381
|
+
return `${ResourceTypesFhirR4.Condition}:${conditionIdentifier}`;
|
|
382
|
+
}
|
|
383
|
+
const allergyIdentifier = asTrimmedString(claims[AllergyIntoleranceClaim.Identifier]);
|
|
384
|
+
if (allergyIdentifier) {
|
|
385
|
+
return `${ResourceTypesFhirR4.AllergyIntolerance}:${allergyIdentifier}`;
|
|
386
|
+
}
|
|
387
|
+
const documentReferenceIdentifier = asTrimmedString(claims[DocumentReferenceClaim.Identifier]);
|
|
388
|
+
if (documentReferenceIdentifier) {
|
|
389
|
+
return `${ResourceTypesFhirR4.DocumentReference}:${documentReferenceIdentifier}`;
|
|
390
|
+
}
|
|
391
|
+
return '';
|
|
392
|
+
}
|
|
393
|
+
createBundleEntry(input) {
|
|
394
|
+
const resourceClaims = {
|
|
395
|
+
...input.claims,
|
|
396
|
+
};
|
|
397
|
+
const canonicalEntryIdentifier = this.resolveEntryCanonicalIdValue(resourceClaims);
|
|
398
|
+
return {
|
|
399
|
+
id: canonicalEntryIdentifier || undefined,
|
|
400
|
+
type: input.type || `${input.resourceType}-edit-request-v1.0`,
|
|
401
|
+
fullUrl: input.fullUrl,
|
|
402
|
+
request: input.request,
|
|
403
|
+
resource: {
|
|
404
|
+
resourceType: input.resourceType,
|
|
405
|
+
meta: {
|
|
406
|
+
claims: resourceClaims,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
resolveEntryCanonicalIdValue(claims) {
|
|
412
|
+
const consentIdentifier = asTrimmedString(claims[ClaimConsent.identifier]);
|
|
413
|
+
if (consentIdentifier) {
|
|
414
|
+
return consentIdentifier;
|
|
415
|
+
}
|
|
416
|
+
const medicationIdentifier = asTrimmedString(claims[MedicationStatementClaim.Identifier]);
|
|
417
|
+
if (medicationIdentifier) {
|
|
418
|
+
return medicationIdentifier;
|
|
419
|
+
}
|
|
420
|
+
const conditionIdentifier = asTrimmedString(claims[ConditionClaim.Identifier]);
|
|
421
|
+
if (conditionIdentifier) {
|
|
422
|
+
return conditionIdentifier;
|
|
423
|
+
}
|
|
424
|
+
const allergyIdentifier = asTrimmedString(claims[AllergyIntoleranceClaim.Identifier]);
|
|
425
|
+
if (allergyIdentifier) {
|
|
426
|
+
return allergyIdentifier;
|
|
427
|
+
}
|
|
428
|
+
const documentReferenceIdentifier = asTrimmedString(claims[DocumentReferenceClaim.Identifier]);
|
|
429
|
+
if (documentReferenceIdentifier) {
|
|
430
|
+
return documentReferenceIdentifier;
|
|
431
|
+
}
|
|
432
|
+
const communicationIdentifier = asTrimmedString(claims[CommunicationClaim.Identifier]);
|
|
433
|
+
if (communicationIdentifier) {
|
|
434
|
+
return communicationIdentifier;
|
|
435
|
+
}
|
|
436
|
+
return '';
|
|
437
|
+
}
|
|
438
|
+
assertEntryIndex(index) {
|
|
439
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.bundleInMemory.data.length) {
|
|
440
|
+
throw new Error(`Entry index out of range: ${index}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* High-level consent-access editor alias for onboarding and app-facing code.
|
|
446
|
+
*
|
|
447
|
+
* This keeps the business intent explicit for developers who are editing
|
|
448
|
+
* Consent access rules inside a Communication-carried bundle and should not
|
|
449
|
+
* need to start from the lower-level generic session name.
|
|
450
|
+
*/
|
|
451
|
+
export class ConsentAccessEditor extends CommunicationAttachedBundleSession {
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* High-level factory for consent-access editing.
|
|
455
|
+
*
|
|
456
|
+
* Prefer this name in onboarding docs when the developer intent is:
|
|
457
|
+
* "edit a Consent access bundle carried by a Communication".
|
|
458
|
+
*/
|
|
459
|
+
export function createConsentAccessEditor(options = {}) {
|
|
460
|
+
return new ConsentAccessEditor(options);
|
|
461
|
+
}
|
|
462
|
+
function ensureEntryResource(entry, mode) {
|
|
463
|
+
const resource = entry.resource;
|
|
464
|
+
if (resource && typeof resource === 'object') {
|
|
465
|
+
return resource;
|
|
466
|
+
}
|
|
467
|
+
if (mode === 'normalize') {
|
|
468
|
+
return { meta: { claims: {} } };
|
|
469
|
+
}
|
|
470
|
+
throw new Error('Active entry does not contain a valid resource object.');
|
|
471
|
+
}
|
|
472
|
+
function validateBundleLike(bundle, mode) {
|
|
473
|
+
const looksLikeBundle = bundle && bundle.resourceType === ResourceTypesFhirR4.Bundle && Array.isArray(bundle.data);
|
|
474
|
+
if (looksLikeBundle) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (mode === 'normalize') {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
throw new Error('Decoded attachment is not a valid BundleJsonApi payload.');
|
|
481
|
+
}
|
|
482
|
+
function createEmptyBundle() {
|
|
483
|
+
return {
|
|
484
|
+
resourceType: ResourceTypesFhirR4.Bundle,
|
|
485
|
+
type: 'batch',
|
|
486
|
+
data: [],
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function encodeBundleToBase64(bundle) {
|
|
490
|
+
return Buffer.from(JSON.stringify(bundle), 'utf8').toString('base64');
|
|
491
|
+
}
|
|
492
|
+
function cloneBundle(bundle) {
|
|
493
|
+
return JSON.parse(JSON.stringify(bundle));
|
|
494
|
+
}
|
|
495
|
+
function cloneEntry(entry) {
|
|
496
|
+
return JSON.parse(JSON.stringify(entry));
|
|
497
|
+
}
|
|
498
|
+
function asTrimmedString(value) {
|
|
499
|
+
if (value === undefined || value === null) {
|
|
500
|
+
return '';
|
|
501
|
+
}
|
|
502
|
+
return String(value).trim();
|
|
503
|
+
}
|
|
504
|
+
function resolveContainedDocumentsClaimKey(resourceType) {
|
|
505
|
+
if (resourceType === ResourceTypesFhirR4.Consent) {
|
|
506
|
+
return ClaimConsent.containedDocuments;
|
|
507
|
+
}
|
|
508
|
+
if (resourceType === ResourceTypesFhirR4.MedicationStatement) {
|
|
509
|
+
return MedicationStatementClaim.ContainedDocuments;
|
|
510
|
+
}
|
|
511
|
+
if (resourceType === ResourceTypesFhirR4.Condition) {
|
|
512
|
+
return ConditionClaim.ContainedDocuments;
|
|
513
|
+
}
|
|
514
|
+
if (resourceType === ResourceTypesFhirR4.AllergyIntolerance) {
|
|
515
|
+
return AllergyIntoleranceClaim.ContainedDocuments;
|
|
516
|
+
}
|
|
517
|
+
return '';
|
|
518
|
+
}
|
|
519
|
+
function resolveSubjectFromClaims(claims) {
|
|
520
|
+
return asTrimmedString(claims[ClaimConsent.subject]
|
|
521
|
+
|| claims[MedicationStatementClaim.Subject]
|
|
522
|
+
|| claims[ConditionClaim.Subject]
|
|
523
|
+
|| claims[AllergyIntoleranceClaim.Subject]
|
|
524
|
+
|| claims[AllergyIntoleranceClaim.Patient]
|
|
525
|
+
|| claims[DocumentReferenceClaim.Subject]);
|
|
526
|
+
}
|
|
527
|
+
function setIfMissing(target, key, value) {
|
|
528
|
+
if (target[key] !== undefined) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (value === undefined || value === null || String(value).trim() === '') {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
target[key] = value;
|
|
535
|
+
}
|
|
536
|
+
function runtimeUuid(prefix) {
|
|
537
|
+
const cryptoLike = globalThis;
|
|
538
|
+
if (typeof cryptoLike.crypto?.randomUUID === 'function') {
|
|
539
|
+
return cryptoLike.crypto.randomUUID();
|
|
540
|
+
}
|
|
541
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
542
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HealthcareDocumentTypes } from '../constants/healthcare';
|
|
2
2
|
import type { ParameterData } from '../models/params';
|
|
3
|
+
import { FhirParametersResource } from './fhir-search';
|
|
3
4
|
export declare const BundleDocumentRequesterKinds: Readonly<{
|
|
4
5
|
readonly Controller: "controller";
|
|
5
6
|
readonly Employee: "employee";
|
|
@@ -140,6 +141,11 @@ export declare function createSummaryOperationRequestParameters(subjectIdOrInput
|
|
|
140
141
|
* path currently stored in `Communication.content-reference`.
|
|
141
142
|
*/
|
|
142
143
|
export declare function createSummaryOperationRequestReferencePath(parameters: ReadonlyArray<ParameterData>): string;
|
|
144
|
+
/**
|
|
145
|
+
* Builds the preferred FHIR `Parameters` body for the same semantic summary
|
|
146
|
+
* search represented by `createSummaryOperationRequestReferencePath(...)`.
|
|
147
|
+
*/
|
|
148
|
+
export declare function createSummaryOperationRequestParametersResource(parameters: ReadonlyArray<ParameterData>): FhirParametersResource;
|
|
143
149
|
/**
|
|
144
150
|
* Resolves the full runtime URL to call GW CORE from the provider sector DID
|
|
145
151
|
* and the generated relative search path.
|
|
@@ -3,6 +3,7 @@ import { CommunicationCategoryCodes } from '../constants/communication.js';
|
|
|
3
3
|
import { DocumentTypeLoincOntology, HealthcareDocumentTypes } from '../constants/healthcare.js';
|
|
4
4
|
import { parseActorFromSub } from './actor.js';
|
|
5
5
|
import { getBaseUrlFromDidWeb } from './did.js';
|
|
6
|
+
import { buildFhirParametersResourceFromParameterData } from './fhir-search.js';
|
|
6
7
|
import { CommunicationClaim } from '../models/interoperable-claims/communication-claims.js';
|
|
7
8
|
import { transformCommunicationClaimsToResourceFhirR4 } from './communication-fhir-r4.js';
|
|
8
9
|
export const BundleDocumentRequesterKinds = Object.freeze({
|
|
@@ -260,6 +261,13 @@ export function createSummaryOperationRequestReferencePath(parameters) {
|
|
|
260
261
|
}
|
|
261
262
|
return `individual/org.hl7.fhir.r4/Bundle/_search?${params.filter(Boolean).join('&')}`;
|
|
262
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Builds the preferred FHIR `Parameters` body for the same semantic summary
|
|
266
|
+
* search represented by `createSummaryOperationRequestReferencePath(...)`.
|
|
267
|
+
*/
|
|
268
|
+
export function createSummaryOperationRequestParametersResource(parameters) {
|
|
269
|
+
return buildFhirParametersResourceFromParameterData(parameters);
|
|
270
|
+
}
|
|
263
271
|
/**
|
|
264
272
|
* Resolves the full runtime URL to call GW CORE from the provider sector DID
|
|
265
273
|
* and the generated relative search path.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { SearchParameterPrimitive, SearchRequestEncoding } from './fhir-search';
|
|
2
|
+
export type EmployeeClaims = Record<string, unknown>;
|
|
3
|
+
export type EmployeeSearchValue = SearchParameterPrimitive;
|
|
4
|
+
export type EmployeeDraftInput = Readonly<{
|
|
5
|
+
identifier?: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
role?: string;
|
|
8
|
+
worksFor?: string;
|
|
9
|
+
memberOf?: string;
|
|
10
|
+
memberOfOrgTaxId?: string;
|
|
11
|
+
additionalClaims?: EmployeeClaims;
|
|
12
|
+
}>;
|
|
13
|
+
export type EmployeeBatchMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
14
|
+
export type EmployeeBatchEntryInput = Readonly<{
|
|
15
|
+
method: EmployeeBatchMethod;
|
|
16
|
+
claims: EmployeeClaims;
|
|
17
|
+
resourceId?: string;
|
|
18
|
+
resourceType?: 'Employee';
|
|
19
|
+
type?: string;
|
|
20
|
+
}>;
|
|
21
|
+
export type EmployeeBatchBundleInput = Readonly<{
|
|
22
|
+
entries: readonly EmployeeBatchEntryInput[];
|
|
23
|
+
}>;
|
|
24
|
+
export type EmployeeSearchBundleInput = Readonly<{
|
|
25
|
+
claims?: Record<string, EmployeeSearchValue | undefined>;
|
|
26
|
+
method?: 'GET' | 'POST';
|
|
27
|
+
encoding?: SearchRequestEncoding;
|
|
28
|
+
resourceType?: 'Employee';
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Builds canonical `org.schema.Person.*` employee claims from semantic input.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildEmployeeClaims(input: EmployeeDraftInput): EmployeeClaims;
|
|
34
|
+
/**
|
|
35
|
+
* Builds a claims-first employee batch entry from the minimum semantic input.
|
|
36
|
+
*
|
|
37
|
+
* Callers only provide the operation method, employee claims, and optional
|
|
38
|
+
* resource id. The helper places claims in the canonical `resource.meta.claims`
|
|
39
|
+
* location and infers the business `type` internally.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildEmployeeBatchEntry(input: EmployeeBatchEntryInput): {
|
|
42
|
+
type: string;
|
|
43
|
+
request: {
|
|
44
|
+
method: EmployeeBatchMethod;
|
|
45
|
+
};
|
|
46
|
+
resource: {
|
|
47
|
+
resourceType: 'Employee';
|
|
48
|
+
id?: string;
|
|
49
|
+
meta: {
|
|
50
|
+
claims: EmployeeClaims;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Builds a canonical employee `_batch` bundle from one or more employee batch
|
|
56
|
+
* entries.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildEmployeeBatchBundle(input: EmployeeBatchBundleInput): {
|
|
59
|
+
resourceType: 'Bundle';
|
|
60
|
+
type: 'batch';
|
|
61
|
+
entry: Array<ReturnType<typeof buildEmployeeBatchEntry>>;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Builds the legacy query-string employee search target kept for compatibility
|
|
65
|
+
* with older `_search` wrappers.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildEmployeeSearchQuery(input?: EmployeeSearchBundleInput): string;
|
|
68
|
+
/**
|
|
69
|
+
* Builds a canonical employee search bundle.
|
|
70
|
+
*
|
|
71
|
+
* Defaults to `POST + Parameters`. Set `method` or `encoding` to legacy GET
|
|
72
|
+
* only when talking to older search consumers.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildEmployeeSearchBundle(input?: EmployeeSearchBundleInput): {
|
|
75
|
+
resourceType: 'Bundle';
|
|
76
|
+
type: 'batch';
|
|
77
|
+
entry: Array<{
|
|
78
|
+
request: {
|
|
79
|
+
method: 'GET' | 'POST';
|
|
80
|
+
url: string;
|
|
81
|
+
};
|
|
82
|
+
resource?: {
|
|
83
|
+
resourceType: 'Parameters';
|
|
84
|
+
parameter: Array<Record<string, unknown>>;
|
|
85
|
+
};
|
|
86
|
+
}>;
|
|
87
|
+
};
|