gdc-common-utils-ts 2.0.18 → 2.1.0
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 +0 -1
- package/dist/examples/bundle-didcomm-payload.d.ts +11 -0
- package/dist/examples/bundle-didcomm-payload.js +13 -0
- package/dist/examples/communication-attached-bundle-session.js +2 -1
- package/dist/examples/communication-didcomm-payload.d.ts +35 -0
- package/dist/examples/communication-didcomm-payload.js +38 -0
- package/dist/examples/index.d.ts +4 -0
- package/dist/examples/index.js +4 -0
- package/dist/examples/profile-manager-mem.d.ts +45 -0
- package/dist/examples/profile-manager-mem.js +29 -0
- package/dist/examples/shared.d.ts +1 -0
- package/dist/examples/shared.js +1 -0
- package/dist/examples/wallet-mem.d.ts +38 -0
- package/dist/examples/wallet-mem.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/interfaces/IProfileOutboxRepository.d.ts +36 -0
- package/dist/interfaces/IProfileOutboxRepository.js +2 -0
- package/dist/interfaces/IWallet.d.ts +74 -0
- package/dist/interfaces/IWallet.js +2 -0
- package/dist/interfaces/IWalletQueue.d.ts +30 -0
- package/dist/interfaces/IWalletQueue.js +2 -0
- package/dist/interfaces/index.d.ts +8 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/models/communication-attached-bundle-session.d.ts +193 -0
- package/dist/models/communication-attached-bundle-session.js +34 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/interoperable-claims/diagnostic-report-claims.d.ts +1 -2
- package/dist/models/interoperable-claims/diagnostic-report-claims.js +1 -2
- package/dist/models/profile-manager.d.ts +85 -0
- package/dist/models/profile-manager.js +2 -0
- package/dist/models/wallet.d.ts +59 -0
- package/dist/models/wallet.js +22 -0
- package/dist/utils/backend-message-manager-mem.d.ts +67 -0
- package/dist/utils/backend-message-manager-mem.js +227 -0
- package/dist/utils/bundle-didcomm-payload.d.ts +29 -0
- package/dist/utils/bundle-didcomm-payload.js +48 -0
- package/dist/utils/communication-attached-bundle-session-helpers.d.ts +54 -0
- package/dist/utils/communication-attached-bundle-session-helpers.js +517 -0
- package/dist/utils/communication-attached-bundle-session.d.ts +59 -302
- package/dist/utils/communication-attached-bundle-session.js +114 -839
- package/dist/utils/communication-consent-access-editor.d.ts +85 -0
- package/dist/utils/communication-consent-access-editor.js +244 -0
- package/dist/utils/communication-didcomm-payload.d.ts +33 -0
- package/dist/utils/communication-didcomm-payload.js +75 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/profile-manager-mem.d.ts +69 -0
- package/dist/utils/profile-manager-mem.js +79 -0
- package/dist/utils/profile-outbox-memory-repository.d.ts +34 -0
- package/dist/utils/profile-outbox-memory-repository.js +57 -0
- package/dist/utils/wallet-mem.d.ts +93 -0
- package/dist/utils/wallet-mem.js +277 -0
- package/dist/utils/wallet-memory-queue.d.ts +32 -0
- package/dist/utils/wallet-memory-queue.js +82 -0
- package/package.json +1 -1
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
// Copyright 2026 Conectate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
|
|
2
2
|
// Always create JSDoc, do not use strings inline in keys nor values, use types instead, and reuse the data test examples.
|
|
3
3
|
import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
|
|
4
|
-
import { getHealthcareProfessionalRolesBySector, getHealthcareRoleByClaim, HealthcareRoleFamilies, getHealthcareRolesByFamily, getHealthcareSectionsByFamily, getHealthcareSectionByCode, getHealthcareSectionFamilyByCode, HealthcareConsentPurposes, HealthcareCanonicalSectionFamilies, } from '../constants/healthcare.js';
|
|
5
|
-
import { ClaimConsent } from '../models/consent-rule.js';
|
|
6
|
-
import { AllergyIntoleranceClaim } from '../models/interoperable-claims/allergy-intolerance-claims.js';
|
|
7
4
|
import { CommunicationClaim } from '../models/interoperable-claims/communication-claims.js';
|
|
8
|
-
import { ConditionClaim } from '../models/interoperable-claims/condition-claims.js';
|
|
9
5
|
import { DocumentReferenceClaim } from '../models/interoperable-claims/document-reference-claims.js';
|
|
6
|
+
import { BundleEntryClaimsContext, } from '../models/communication-attached-bundle-session.js';
|
|
10
7
|
import { BundleQuery } from './bundle-query.js';
|
|
11
8
|
import { addClaimValues } from '../claims/claim-list-helpers.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import { ObservationClaim } from '../models/interoperable-claims/observation-claims.js';
|
|
15
|
-
export const ConsentEditorTargetKinds = Object.freeze({
|
|
16
|
-
Section: 'section',
|
|
17
|
-
ResourceType: 'resource-type',
|
|
18
|
-
});
|
|
19
|
-
export const ConsentEditorScopeCodes = Object.freeze({
|
|
20
|
-
Search: 's',
|
|
21
|
-
Read: 'r',
|
|
22
|
-
Create: 'c',
|
|
23
|
-
Update: 'u',
|
|
24
|
-
Delete: 'd',
|
|
25
|
-
});
|
|
9
|
+
import { asTrimmedString, cloneBundle, cloneEntry, cloneUnknownValue, createEmptyBundle, encodeBundleToBase64, ensureEntryResource, resolveBundleEntryCanonicalIdValue, resolveBundleEntryIdentifier, resolveContainedDocumentsClaimKey, resolveSubjectFromClaims, runtimeUuid, setIfMissing, validateBundleLike, } from './communication-attached-bundle-session-helpers.js';
|
|
10
|
+
export { BundleEntryClaimsContext, CommunicationAttachmentClaimsContext, ConsentEditorScopeCodes, ConsentEditorTargetKinds, } from '../models/communication-attached-bundle-session.js';
|
|
26
11
|
/**
|
|
27
12
|
* Communication editing session with bundle-in-memory as source of truth.
|
|
28
13
|
*
|
|
@@ -64,10 +49,18 @@ export class CommunicationAttachedBundleSession {
|
|
|
64
49
|
getActiveEntryIndex() {
|
|
65
50
|
return this.activeEntryIndex;
|
|
66
51
|
}
|
|
52
|
+
/** Returns the active entry position, or null when no entry is selected. */
|
|
53
|
+
getActiveEntryPosition() {
|
|
54
|
+
return this.activeEntryIndex;
|
|
55
|
+
}
|
|
67
56
|
/** Alias of `getActiveEntryIndex()` with entry-selection wording. */
|
|
68
57
|
getSelectedEntryIndex() {
|
|
69
58
|
return this.getActiveEntryIndex();
|
|
70
59
|
}
|
|
60
|
+
/** Alias of `getActiveEntryPosition()` with entry-selection wording. */
|
|
61
|
+
getSelectedEntryPosition() {
|
|
62
|
+
return this.getActiveEntryPosition();
|
|
63
|
+
}
|
|
71
64
|
/** Returns a deep copy of the active entry when selected. */
|
|
72
65
|
getActiveEntry() {
|
|
73
66
|
if (this.activeEntryIndex === null) {
|
|
@@ -155,11 +148,14 @@ export class CommunicationAttachedBundleSession {
|
|
|
155
148
|
this.syncAttachmentFromBundle();
|
|
156
149
|
return this;
|
|
157
150
|
}
|
|
158
|
-
/** Selects an active entry by
|
|
151
|
+
/** Selects an active entry by position or fullUrl. */
|
|
159
152
|
selectActiveEntry(selection) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
const requestedPosition = typeof selection.position === 'number'
|
|
154
|
+
? selection.position
|
|
155
|
+
: selection.index;
|
|
156
|
+
if (typeof requestedPosition === 'number') {
|
|
157
|
+
this.assertEntryIndex(requestedPosition);
|
|
158
|
+
this.activeEntryIndex = requestedPosition;
|
|
163
159
|
return this;
|
|
164
160
|
}
|
|
165
161
|
if (selection.fullUrl) {
|
|
@@ -170,7 +166,7 @@ export class CommunicationAttachedBundleSession {
|
|
|
170
166
|
this.activeEntryIndex = foundIndex;
|
|
171
167
|
return this;
|
|
172
168
|
}
|
|
173
|
-
throw new Error('selectActiveEntry requires either
|
|
169
|
+
throw new Error('selectActiveEntry requires either position or fullUrl.');
|
|
174
170
|
}
|
|
175
171
|
/** Alias of `selectActiveEntry()` with entry-selection wording. */
|
|
176
172
|
selectEntry(selection) {
|
|
@@ -200,49 +196,44 @@ export class CommunicationAttachedBundleSession {
|
|
|
200
196
|
return this;
|
|
201
197
|
}
|
|
202
198
|
/**
|
|
203
|
-
*
|
|
199
|
+
* Reusable resource-typed wrapper around `upsertActiveEntry(...)`.
|
|
204
200
|
*
|
|
205
|
-
*
|
|
201
|
+
* Resource-specific helpers should delegate here instead of re-embedding the
|
|
202
|
+
* same `resourceType/fullUrl/type/request/claims` plumbing.
|
|
206
203
|
*/
|
|
207
|
-
|
|
204
|
+
upsertActiveClaimsResourceEntry(resourceType, input) {
|
|
208
205
|
return this.upsertActiveEntry({
|
|
209
|
-
resourceType
|
|
210
|
-
claims:
|
|
206
|
+
resourceType,
|
|
207
|
+
claims: {
|
|
208
|
+
...input.claims,
|
|
209
|
+
},
|
|
211
210
|
fullUrl: input.fullUrl,
|
|
212
211
|
type: input.type,
|
|
213
212
|
request: input.request,
|
|
214
213
|
});
|
|
215
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Consent-first helper for developer onboarding.
|
|
217
|
+
*
|
|
218
|
+
* Expected keys should come from `ClaimConsent` in caller code.
|
|
219
|
+
*/
|
|
220
|
+
upsertActiveConsentEntry(input) {
|
|
221
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Consent, input);
|
|
222
|
+
}
|
|
216
223
|
/**
|
|
217
224
|
* MedicationStatement helper for IPS-in-Communication use cases.
|
|
218
225
|
*
|
|
219
226
|
* Expected keys should come from MedicationStatement claims constants.
|
|
220
227
|
*/
|
|
221
228
|
upsertActiveMedicationStatementEntry(input) {
|
|
222
|
-
return this.
|
|
223
|
-
resourceType: ResourceTypesFhirR4.MedicationStatement,
|
|
224
|
-
claims: {
|
|
225
|
-
...input.claims,
|
|
226
|
-
},
|
|
227
|
-
fullUrl: input.fullUrl,
|
|
228
|
-
type: input.type,
|
|
229
|
-
request: input.request,
|
|
230
|
-
});
|
|
229
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.MedicationStatement, input);
|
|
231
230
|
}
|
|
232
231
|
/**
|
|
233
232
|
* DocumentReference helper for bundle-contained attachments linked from
|
|
234
233
|
* other clinical resources through `*.contained-documents`.
|
|
235
234
|
*/
|
|
236
235
|
upsertActiveDocumentReferenceEntry(input) {
|
|
237
|
-
return this.
|
|
238
|
-
resourceType: ResourceTypesFhirR4.DocumentReference,
|
|
239
|
-
claims: {
|
|
240
|
-
...input.claims,
|
|
241
|
-
},
|
|
242
|
-
fullUrl: input.fullUrl,
|
|
243
|
-
type: input.type,
|
|
244
|
-
request: input.request,
|
|
245
|
-
});
|
|
236
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.DocumentReference, input);
|
|
246
237
|
}
|
|
247
238
|
/**
|
|
248
239
|
* Condition helper for IPS-in-Communication use cases.
|
|
@@ -250,15 +241,15 @@ export class CommunicationAttachedBundleSession {
|
|
|
250
241
|
* Expected keys should come from Condition claims constants.
|
|
251
242
|
*/
|
|
252
243
|
upsertActiveConditionEntry(input) {
|
|
253
|
-
return this.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
244
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Condition, input);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Appointment helper for schedule/event flows carried inside Communication.
|
|
248
|
+
*
|
|
249
|
+
* Expected keys should come from Appointment claims constants.
|
|
250
|
+
*/
|
|
251
|
+
upsertActiveAppointmentEntry(input) {
|
|
252
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Appointment, input);
|
|
262
253
|
}
|
|
263
254
|
/**
|
|
264
255
|
* Observation helper for IPS-style and sectioned bundle authoring.
|
|
@@ -266,15 +257,7 @@ export class CommunicationAttachedBundleSession {
|
|
|
266
257
|
* Expected keys should come from Observation claims constants.
|
|
267
258
|
*/
|
|
268
259
|
upsertActiveObservationEntry(input) {
|
|
269
|
-
return this.
|
|
270
|
-
resourceType: ResourceTypesFhirR4.Observation,
|
|
271
|
-
claims: {
|
|
272
|
-
...input.claims,
|
|
273
|
-
},
|
|
274
|
-
fullUrl: input.fullUrl,
|
|
275
|
-
type: input.type,
|
|
276
|
-
request: input.request,
|
|
277
|
-
});
|
|
260
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Observation, input);
|
|
278
261
|
}
|
|
279
262
|
/**
|
|
280
263
|
* AllergyIntolerance helper for IPS-in-Communication use cases.
|
|
@@ -282,33 +265,68 @@ export class CommunicationAttachedBundleSession {
|
|
|
282
265
|
* Expected keys should come from AllergyIntolerance claims constants.
|
|
283
266
|
*/
|
|
284
267
|
upsertActiveAllergyIntoleranceEntry(input) {
|
|
285
|
-
return this.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
268
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.AllergyIntolerance, input);
|
|
269
|
+
}
|
|
270
|
+
/** DiagnosticReport helper for report-in-Communication use cases. */
|
|
271
|
+
upsertActiveDiagnosticReportEntry(input) {
|
|
272
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.DiagnosticReport, input);
|
|
273
|
+
}
|
|
274
|
+
/** CarePlan helper for plan-in-Communication use cases. */
|
|
275
|
+
upsertActiveCarePlanEntry(input) {
|
|
276
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.CarePlan, input);
|
|
277
|
+
}
|
|
278
|
+
/** Procedure helper for procedure-in-Communication use cases. */
|
|
279
|
+
upsertActiveProcedureEntry(input) {
|
|
280
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Procedure, input);
|
|
281
|
+
}
|
|
282
|
+
/** Immunization helper for immunization-in-Communication use cases. */
|
|
283
|
+
upsertActiveImmunizationEntry(input) {
|
|
284
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Immunization, input);
|
|
285
|
+
}
|
|
286
|
+
/** Encounter helper for visit/event flows carried inside Communication. */
|
|
287
|
+
upsertActiveEncounterEntry(input) {
|
|
288
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Encounter, input);
|
|
289
|
+
}
|
|
290
|
+
/** Device helper for device catalog or implant/device context flows. */
|
|
291
|
+
upsertActiveDeviceEntry(input) {
|
|
292
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Device, input);
|
|
293
|
+
}
|
|
294
|
+
/** DeviceUseStatement helper for device usage flows carried inside Communication. */
|
|
295
|
+
upsertActiveDeviceUseStatementEntry(input) {
|
|
296
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.DeviceUseStatement, input);
|
|
297
|
+
}
|
|
298
|
+
/** Flag helper for alert/banner flows carried inside Communication. */
|
|
299
|
+
upsertActiveFlagEntry(input) {
|
|
300
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Flag, input);
|
|
301
|
+
}
|
|
302
|
+
/** ClinicalImpression helper for assessment flows carried inside Communication. */
|
|
303
|
+
upsertActiveClinicalImpressionEntry(input) {
|
|
304
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.ClinicalImpression, input);
|
|
305
|
+
}
|
|
306
|
+
/** Coverage helper for entitlement/insurance flows carried inside Communication. */
|
|
307
|
+
upsertActiveCoverageEntry(input) {
|
|
308
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Coverage, input);
|
|
309
|
+
}
|
|
310
|
+
/** AppointmentResponse helper for schedule-response flows carried inside Communication. */
|
|
311
|
+
upsertActiveAppointmentResponseEntry(input) {
|
|
312
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.AppointmentResponse, input);
|
|
313
|
+
}
|
|
314
|
+
/** Composition helper for document-structure flows carried inside Communication. */
|
|
315
|
+
upsertActiveCompositionEntry(input) {
|
|
316
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Composition, input);
|
|
317
|
+
}
|
|
318
|
+
/** Location helper for schedule/place flows carried inside Communication. */
|
|
319
|
+
upsertActiveLocationEntry(input) {
|
|
320
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Location, input);
|
|
321
|
+
}
|
|
322
|
+
/** Organization helper for provider/payer/department flows carried inside Communication. */
|
|
323
|
+
upsertActiveOrganizationEntry(input) {
|
|
324
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.Organization, input);
|
|
325
|
+
}
|
|
326
|
+
/** RelatedPerson helper for relationship/member flows carried inside Communication. */
|
|
327
|
+
upsertActiveRelatedPersonEntry(input) {
|
|
328
|
+
return this.upsertActiveClaimsResourceEntry(ResourceTypesFhirR4.RelatedPerson, input);
|
|
294
329
|
}
|
|
295
|
-
/**
|
|
296
|
-
* TODO(ips-next):
|
|
297
|
-
* Add `upsertActiveDiagnosticReportEntry(...)` once the shared claim helpers
|
|
298
|
-
* for `DiagnosticReport` are in place.
|
|
299
|
-
*
|
|
300
|
-
* Expected shape should mirror the existing resource helpers:
|
|
301
|
-
* - `claims` authored with `@context = org.hl7.fhir.api`
|
|
302
|
-
* - matching priority by `DiagnosticReport.identifier`
|
|
303
|
-
* - support for linked `DocumentReference` ids through
|
|
304
|
-
* `DiagnosticReport.contained-documents`
|
|
305
|
-
*
|
|
306
|
-
* Intentionally not implemented in this pass:
|
|
307
|
-
* - IPS authoring already works for the currently documented resources
|
|
308
|
-
* - GW Core can already consume bundle-contained `DocumentReference` rows
|
|
309
|
-
* - adding the DiagnosticReport editing surface now would expand the IPS
|
|
310
|
-
* contract further than intended for this release slice
|
|
311
|
-
*/
|
|
312
330
|
/**
|
|
313
331
|
* Creates or updates a linked `DocumentReference` entry and stores its
|
|
314
332
|
* identifier under the active resource `*.contained-documents` claim.
|
|
@@ -335,7 +353,7 @@ export class CommunicationAttachedBundleSession {
|
|
|
335
353
|
|| resolveSubjectFromClaims(parentClaims)
|
|
336
354
|
|| asTrimmedString(this.communicationClaims[CommunicationClaim.Subject]);
|
|
337
355
|
const documentClaims = {
|
|
338
|
-
'@context':
|
|
356
|
+
'@context': BundleEntryClaimsContext,
|
|
339
357
|
...(input.claims || {}),
|
|
340
358
|
[DocumentReferenceClaim.Identifier]: documentIdentifier,
|
|
341
359
|
};
|
|
@@ -460,29 +478,9 @@ export class CommunicationAttachedBundleSession {
|
|
|
460
478
|
resolveCurrentSubject() {
|
|
461
479
|
if (this.activeEntryIndex !== null) {
|
|
462
480
|
const claims = this.bundleInMemory.data[this.activeEntryIndex]?.resource?.meta?.claims || {};
|
|
463
|
-
const
|
|
464
|
-
if (
|
|
465
|
-
return
|
|
466
|
-
}
|
|
467
|
-
const medicationSubject = asTrimmedString(claims[MedicationStatementClaim.Subject]);
|
|
468
|
-
if (medicationSubject) {
|
|
469
|
-
return medicationSubject;
|
|
470
|
-
}
|
|
471
|
-
const conditionSubject = asTrimmedString(claims[ConditionClaim.Subject]);
|
|
472
|
-
if (conditionSubject) {
|
|
473
|
-
return conditionSubject;
|
|
474
|
-
}
|
|
475
|
-
const allergySubject = asTrimmedString(claims[AllergyIntoleranceClaim.Subject] || claims[AllergyIntoleranceClaim.Patient]);
|
|
476
|
-
if (allergySubject) {
|
|
477
|
-
return allergySubject;
|
|
478
|
-
}
|
|
479
|
-
const documentReferenceSubject = asTrimmedString(claims[DocumentReferenceClaim.Subject]);
|
|
480
|
-
if (documentReferenceSubject) {
|
|
481
|
-
return documentReferenceSubject;
|
|
482
|
-
}
|
|
483
|
-
const observationSubject = asTrimmedString(claims[ObservationClaim.Subject] || claims[ObservationClaim.Patient]);
|
|
484
|
-
if (observationSubject) {
|
|
485
|
-
return observationSubject;
|
|
481
|
+
const entrySubject = resolveSubjectFromClaims(claims);
|
|
482
|
+
if (entrySubject) {
|
|
483
|
+
return entrySubject;
|
|
486
484
|
}
|
|
487
485
|
}
|
|
488
486
|
const fromClaims = asTrimmedString(this.communicationClaims[CommunicationClaim.Subject]);
|
|
@@ -506,31 +504,7 @@ export class CommunicationAttachedBundleSession {
|
|
|
506
504
|
});
|
|
507
505
|
}
|
|
508
506
|
resolveEntryIdentifier(claims) {
|
|
509
|
-
|
|
510
|
-
if (consentIdentifier) {
|
|
511
|
-
return `${ResourceTypesFhirR4.Consent}:${consentIdentifier}`;
|
|
512
|
-
}
|
|
513
|
-
const medicationIdentifier = asTrimmedString(claims[MedicationStatementClaim.Identifier]);
|
|
514
|
-
if (medicationIdentifier) {
|
|
515
|
-
return `${ResourceTypesFhirR4.MedicationStatement}:${medicationIdentifier}`;
|
|
516
|
-
}
|
|
517
|
-
const conditionIdentifier = asTrimmedString(claims[ConditionClaim.Identifier]);
|
|
518
|
-
if (conditionIdentifier) {
|
|
519
|
-
return `${ResourceTypesFhirR4.Condition}:${conditionIdentifier}`;
|
|
520
|
-
}
|
|
521
|
-
const allergyIdentifier = asTrimmedString(claims[AllergyIntoleranceClaim.Identifier]);
|
|
522
|
-
if (allergyIdentifier) {
|
|
523
|
-
return `${ResourceTypesFhirR4.AllergyIntolerance}:${allergyIdentifier}`;
|
|
524
|
-
}
|
|
525
|
-
const documentReferenceIdentifier = asTrimmedString(claims[DocumentReferenceClaim.Identifier]);
|
|
526
|
-
if (documentReferenceIdentifier) {
|
|
527
|
-
return `${ResourceTypesFhirR4.DocumentReference}:${documentReferenceIdentifier}`;
|
|
528
|
-
}
|
|
529
|
-
const observationIdentifier = asTrimmedString(claims[ObservationClaim.Identifier]);
|
|
530
|
-
if (observationIdentifier) {
|
|
531
|
-
return `${ResourceTypesFhirR4.Observation}:${observationIdentifier}`;
|
|
532
|
-
}
|
|
533
|
-
return '';
|
|
507
|
+
return resolveBundleEntryIdentifier(claims);
|
|
534
508
|
}
|
|
535
509
|
createBundleEntry(input) {
|
|
536
510
|
const resourceClaims = {
|
|
@@ -551,35 +525,7 @@ export class CommunicationAttachedBundleSession {
|
|
|
551
525
|
};
|
|
552
526
|
}
|
|
553
527
|
resolveEntryCanonicalIdValue(claims) {
|
|
554
|
-
|
|
555
|
-
if (consentIdentifier) {
|
|
556
|
-
return consentIdentifier;
|
|
557
|
-
}
|
|
558
|
-
const medicationIdentifier = asTrimmedString(claims[MedicationStatementClaim.Identifier]);
|
|
559
|
-
if (medicationIdentifier) {
|
|
560
|
-
return medicationIdentifier;
|
|
561
|
-
}
|
|
562
|
-
const conditionIdentifier = asTrimmedString(claims[ConditionClaim.Identifier]);
|
|
563
|
-
if (conditionIdentifier) {
|
|
564
|
-
return conditionIdentifier;
|
|
565
|
-
}
|
|
566
|
-
const allergyIdentifier = asTrimmedString(claims[AllergyIntoleranceClaim.Identifier]);
|
|
567
|
-
if (allergyIdentifier) {
|
|
568
|
-
return allergyIdentifier;
|
|
569
|
-
}
|
|
570
|
-
const documentReferenceIdentifier = asTrimmedString(claims[DocumentReferenceClaim.Identifier]);
|
|
571
|
-
if (documentReferenceIdentifier) {
|
|
572
|
-
return documentReferenceIdentifier;
|
|
573
|
-
}
|
|
574
|
-
const observationIdentifier = asTrimmedString(claims[ObservationClaim.Identifier]);
|
|
575
|
-
if (observationIdentifier) {
|
|
576
|
-
return observationIdentifier;
|
|
577
|
-
}
|
|
578
|
-
const communicationIdentifier = asTrimmedString(claims[CommunicationClaim.Identifier]);
|
|
579
|
-
if (communicationIdentifier) {
|
|
580
|
-
return communicationIdentifier;
|
|
581
|
-
}
|
|
582
|
-
return '';
|
|
528
|
+
return resolveBundleEntryCanonicalIdValue(claims);
|
|
583
529
|
}
|
|
584
530
|
assertEntryIndex(index) {
|
|
585
531
|
if (!Number.isInteger(index) || index < 0 || index >= this.bundleInMemory.data.length) {
|
|
@@ -587,674 +533,3 @@ export class CommunicationAttachedBundleSession {
|
|
|
587
533
|
}
|
|
588
534
|
}
|
|
589
535
|
}
|
|
590
|
-
/**
|
|
591
|
-
* High-level consent-access editor alias for onboarding and app-facing code.
|
|
592
|
-
*
|
|
593
|
-
* This keeps the business intent explicit for developers who are editing
|
|
594
|
-
* Consent access rules inside a Communication-carried bundle and should not
|
|
595
|
-
* need to start from the lower-level generic session name.
|
|
596
|
-
*/
|
|
597
|
-
export class ConsentAccessEditor extends CommunicationAttachedBundleSession {
|
|
598
|
-
/** Returns duplicate atomic consent-rule conflicts across the current bundle. */
|
|
599
|
-
getConsentRuleDuplicateConflicts() {
|
|
600
|
-
return detectDuplicateConsentRuleConflicts(this.getBundleInMemory().data);
|
|
601
|
-
}
|
|
602
|
-
/** Returns duplicate atomic consent-rule conflicts affecting the active Consent entry. */
|
|
603
|
-
getActiveConsentRuleDuplicateConflicts() {
|
|
604
|
-
const activeEntryIndex = this.getActiveEntryIndex();
|
|
605
|
-
if (activeEntryIndex === null) {
|
|
606
|
-
return [];
|
|
607
|
-
}
|
|
608
|
-
return this.getConsentRuleDuplicateConflicts()
|
|
609
|
-
.filter((conflict) => conflict.affectedEntries.some((entry) => entry.entryIndex === activeEntryIndex));
|
|
610
|
-
}
|
|
611
|
-
/** Returns one frontend-facing editable view model for the active Consent entry. */
|
|
612
|
-
getConsentViewModel() {
|
|
613
|
-
const activeEntry = this.getActiveEntry();
|
|
614
|
-
const claims = {
|
|
615
|
-
...(activeEntry?.resource?.meta?.claims || {}),
|
|
616
|
-
};
|
|
617
|
-
return {
|
|
618
|
-
...(activeEntry?.fullUrl ? { fullUrl: activeEntry.fullUrl } : {}),
|
|
619
|
-
identifier: asTrimmedString(claims[ClaimConsent.identifier]),
|
|
620
|
-
subject: asTrimmedString(claims[ClaimConsent.subject]),
|
|
621
|
-
decision: this.getDecision(),
|
|
622
|
-
classifiedActors: this.getActorsClassified(),
|
|
623
|
-
classifiedRoles: this.getRolesClassified(),
|
|
624
|
-
classifiedPurposes: this.getPurposesClassified(),
|
|
625
|
-
classifiedTargets: this.getTargetsClassified(),
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
/** Applies one frontend-facing editable view model back into the active Consent entry. */
|
|
629
|
-
applyConsentViewModel(viewModel) {
|
|
630
|
-
this.setActiveEntryClaim(ClaimConsent.identifier, viewModel.identifier);
|
|
631
|
-
this.setActiveEntryClaim(ClaimConsent.subject, viewModel.subject);
|
|
632
|
-
this.setActiveEntryClaim(ClaimConsent.decision, viewModel.decision);
|
|
633
|
-
this.setActiveEntryClaimList(ClaimConsent.actorIdentifier, flattenClassifiedActors(viewModel.classifiedActors));
|
|
634
|
-
this.setSelectedRoles(flattenClassifiedRoles(viewModel.classifiedRoles));
|
|
635
|
-
this.setSelectedPurposes(viewModel.classifiedPurposes.map((purpose) => purpose.code));
|
|
636
|
-
const flattenedTargets = flattenClassifiedTargets(viewModel.classifiedTargets);
|
|
637
|
-
this.setSelectedCoreSections(flattenedTargets.coreSections);
|
|
638
|
-
this.setSelectedKindOfDocuments(flattenedTargets.kindOfDocuments);
|
|
639
|
-
this.setSelectedTypeOfServices(flattenedTargets.typeOfServices);
|
|
640
|
-
this.setSelectedSubjectMatterDomains(flattenedTargets.subjectMatterDomains);
|
|
641
|
-
this.setSelectedResourceTypes(flattenedTargets.resourceTypes);
|
|
642
|
-
return this;
|
|
643
|
-
}
|
|
644
|
-
/** Returns the canonical permit/deny decision from the active Consent entry. */
|
|
645
|
-
getDecision() {
|
|
646
|
-
return asTrimmedString(this.getActiveEntryClaim(ClaimConsent.decision));
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* @deprecated Use `getDecision()`.
|
|
650
|
-
* Kept as a compatibility alias for the previous helper name.
|
|
651
|
-
*/
|
|
652
|
-
getPermit() {
|
|
653
|
-
return this.getDecision();
|
|
654
|
-
}
|
|
655
|
-
/**
|
|
656
|
-
* Returns target classification derived from the current consent claim
|
|
657
|
-
* contract without altering or extending the persisted claim keys.
|
|
658
|
-
*/
|
|
659
|
-
getTargetsClassified() {
|
|
660
|
-
const claims = {
|
|
661
|
-
...(this.getActiveEntry()?.resource?.meta?.claims || {}),
|
|
662
|
-
};
|
|
663
|
-
const actionTargets = splitCsv(claims[ClaimConsent.action]).map((code) => buildClassifiedConsentTarget(ConsentEditorTargetKinds.Section, code, [ConsentEditorScopeCodes.Read]));
|
|
664
|
-
const categoryTargets = splitCsv(claims[ClaimConsent.category]).map((code) => buildClassifiedConsentTarget(ConsentEditorTargetKinds.Section, code, [ConsentEditorScopeCodes.Read], HealthcareCanonicalSectionFamilies.KindOfDocument));
|
|
665
|
-
const resourceTypes = splitCsv(claims[ClaimConsent.resourceType]).map((code) => buildClassifiedConsentTarget(ConsentEditorTargetKinds.ResourceType, code, [ConsentEditorScopeCodes.Read]));
|
|
666
|
-
return normalizeClassifiedTargets([
|
|
667
|
-
...actionTargets,
|
|
668
|
-
...categoryTargets,
|
|
669
|
-
...resourceTypes,
|
|
670
|
-
]);
|
|
671
|
-
}
|
|
672
|
-
/** Returns the catalog of core-section targets available for picker UIs. */
|
|
673
|
-
getCoreSectionOptions() {
|
|
674
|
-
return buildSectionCatalogOptions(HealthcareCanonicalSectionFamilies.CoreSection);
|
|
675
|
-
}
|
|
676
|
-
/** Returns the core-section catalog keyed by LOINC code. */
|
|
677
|
-
getCoreSectionCatalog() {
|
|
678
|
-
return getHealthcareSectionsByFamily(HealthcareCanonicalSectionFamilies.CoreSection);
|
|
679
|
-
}
|
|
680
|
-
/** Returns the catalog of kind-of-document targets available for picker UIs. */
|
|
681
|
-
getKindOfDocumentOptions() {
|
|
682
|
-
return buildSectionCatalogOptions(HealthcareCanonicalSectionFamilies.KindOfDocument);
|
|
683
|
-
}
|
|
684
|
-
/** Returns the kind-of-document catalog keyed by LOINC code. */
|
|
685
|
-
getKindOfDocumentCatalog() {
|
|
686
|
-
return getHealthcareSectionsByFamily(HealthcareCanonicalSectionFamilies.KindOfDocument);
|
|
687
|
-
}
|
|
688
|
-
/** Returns the catalog of type-of-service targets available for picker UIs. */
|
|
689
|
-
getTypeOfServiceOptions() {
|
|
690
|
-
return buildSectionCatalogOptions(HealthcareCanonicalSectionFamilies.TypeOfService);
|
|
691
|
-
}
|
|
692
|
-
/** Returns the type-of-service catalog keyed by LOINC code. */
|
|
693
|
-
getTypeOfServiceCatalog() {
|
|
694
|
-
return getHealthcareSectionsByFamily(HealthcareCanonicalSectionFamilies.TypeOfService);
|
|
695
|
-
}
|
|
696
|
-
/** Returns the catalog of subject-matter-domain targets available for picker UIs. */
|
|
697
|
-
getSubjectMatterDomainOptions() {
|
|
698
|
-
return buildSectionCatalogOptions(HealthcareCanonicalSectionFamilies.SubjectMatterDomain);
|
|
699
|
-
}
|
|
700
|
-
/** Returns the subject-matter-domain catalog keyed by LOINC code. */
|
|
701
|
-
getSubjectMatterDomainCatalog() {
|
|
702
|
-
return getHealthcareSectionsByFamily(HealthcareCanonicalSectionFamilies.SubjectMatterDomain);
|
|
703
|
-
}
|
|
704
|
-
/** Returns resource-type targets currently known in the active consent entry. */
|
|
705
|
-
getResourceTypeOptions() {
|
|
706
|
-
return this.getSelectedResourceTypes().map((code) => buildClassifiedConsentTarget(ConsentEditorTargetKinds.ResourceType, code, [ConsentEditorScopeCodes.Read]));
|
|
707
|
-
}
|
|
708
|
-
/** Returns the supported resource-type catalog. */
|
|
709
|
-
getResourceTypeCatalog() {
|
|
710
|
-
return Object.values(ResourceTypesFhirR4).filter((resourceType) => resourceType !== ResourceTypesFhirR4.Bundle);
|
|
711
|
-
}
|
|
712
|
-
/** Returns the available professional roles for one sector. */
|
|
713
|
-
getAvailableProfessionalRolesBySector(sector) {
|
|
714
|
-
return getHealthcareProfessionalRolesBySector(sector);
|
|
715
|
-
}
|
|
716
|
-
/** Returns the available HL7 relationship roles. */
|
|
717
|
-
getAvailableRelationshipRoles() {
|
|
718
|
-
return getHealthcareRolesByFamily(HealthcareRoleFamilies.PersonalRelationshipHl7);
|
|
719
|
-
}
|
|
720
|
-
/** Returns selected core-section claim values. */
|
|
721
|
-
getSelectedCoreSections() {
|
|
722
|
-
return this.getSelectedSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.CoreSection);
|
|
723
|
-
}
|
|
724
|
-
/** Returns selected kind-of-document claim values. */
|
|
725
|
-
getSelectedKindOfDocuments() {
|
|
726
|
-
return splitCsv(this.getActiveEntryClaim(ClaimConsent.category));
|
|
727
|
-
}
|
|
728
|
-
/** Returns selected type-of-service claim values. */
|
|
729
|
-
getSelectedTypeOfServices() {
|
|
730
|
-
return this.getSelectedSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.TypeOfService);
|
|
731
|
-
}
|
|
732
|
-
/** Returns selected subject-matter-domain claim values. */
|
|
733
|
-
getSelectedSubjectMatterDomains() {
|
|
734
|
-
return this.getSelectedSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.SubjectMatterDomain);
|
|
735
|
-
}
|
|
736
|
-
/** Returns selected resource-type claim values. */
|
|
737
|
-
getSelectedResourceTypes() {
|
|
738
|
-
return splitCsv(this.getActiveEntryClaim(ClaimConsent.resourceType));
|
|
739
|
-
}
|
|
740
|
-
/** Replaces selected core-section claim values while preserving other families. */
|
|
741
|
-
setSelectedCoreSections(codes) {
|
|
742
|
-
return this.setSelectedSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.CoreSection, codes);
|
|
743
|
-
}
|
|
744
|
-
/** Replaces selected kind-of-document claim values. */
|
|
745
|
-
setSelectedKindOfDocuments(codes) {
|
|
746
|
-
return this.setActiveEntryClaimList(ClaimConsent.category, codes);
|
|
747
|
-
}
|
|
748
|
-
/** Replaces selected type-of-service claim values while preserving other families. */
|
|
749
|
-
setSelectedTypeOfServices(codes) {
|
|
750
|
-
return this.setSelectedSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.TypeOfService, codes);
|
|
751
|
-
}
|
|
752
|
-
/** Replaces selected subject-matter-domain claim values while preserving other families. */
|
|
753
|
-
setSelectedSubjectMatterDomains(codes) {
|
|
754
|
-
return this.setSelectedSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.SubjectMatterDomain, codes);
|
|
755
|
-
}
|
|
756
|
-
/** Replaces selected resource-type claim values. */
|
|
757
|
-
setSelectedResourceTypes(codes) {
|
|
758
|
-
return this.setActiveEntryClaimList(ClaimConsent.resourceType, codes);
|
|
759
|
-
}
|
|
760
|
-
/** Adds core-section claim values without removing other selected targets. */
|
|
761
|
-
addCoreSections(codes) {
|
|
762
|
-
return this.addSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.CoreSection, codes);
|
|
763
|
-
}
|
|
764
|
-
/** Removes selected core-section claim values. */
|
|
765
|
-
removeCoreSections(codes) {
|
|
766
|
-
return this.removeSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.CoreSection, codes);
|
|
767
|
-
}
|
|
768
|
-
/** Adds kind-of-document claim values. */
|
|
769
|
-
addKindOfDocuments(codes) {
|
|
770
|
-
return this.addActiveEntryClaimList(ClaimConsent.category, codes);
|
|
771
|
-
}
|
|
772
|
-
/** Removes kind-of-document claim values. */
|
|
773
|
-
removeKindOfDocuments(codes) {
|
|
774
|
-
return this.removeActiveEntryClaimList(ClaimConsent.category, codes);
|
|
775
|
-
}
|
|
776
|
-
/** Adds type-of-service claim values without removing other selected targets. */
|
|
777
|
-
addTypeOfServices(codes) {
|
|
778
|
-
return this.addSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.TypeOfService, codes);
|
|
779
|
-
}
|
|
780
|
-
/** Removes selected type-of-service claim values. */
|
|
781
|
-
removeTypeOfServices(codes) {
|
|
782
|
-
return this.removeSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.TypeOfService, codes);
|
|
783
|
-
}
|
|
784
|
-
/** Adds subject-matter-domain claim values without removing other selected targets. */
|
|
785
|
-
addSubjectMatterDomains(codes) {
|
|
786
|
-
return this.addSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.SubjectMatterDomain, codes);
|
|
787
|
-
}
|
|
788
|
-
/** Removes selected subject-matter-domain claim values. */
|
|
789
|
-
removeSubjectMatterDomains(codes) {
|
|
790
|
-
return this.removeSectionCodesByFamily(ClaimConsent.action, HealthcareCanonicalSectionFamilies.SubjectMatterDomain, codes);
|
|
791
|
-
}
|
|
792
|
-
/** Adds resource-type claim values. */
|
|
793
|
-
addResourceTypes(codes) {
|
|
794
|
-
return this.addActiveEntryClaimList(ClaimConsent.resourceType, codes);
|
|
795
|
-
}
|
|
796
|
-
/** Removes resource-type claim values. */
|
|
797
|
-
removeResourceTypes(codes) {
|
|
798
|
-
return this.removeActiveEntryClaimList(ClaimConsent.resourceType, codes);
|
|
799
|
-
}
|
|
800
|
-
/** Returns purposes as explicit classified items. */
|
|
801
|
-
getPurposesClassified() {
|
|
802
|
-
return this.getSelectedPurposes().map((code) => ({
|
|
803
|
-
code,
|
|
804
|
-
display: resolveConsentPurposeDisplay(code),
|
|
805
|
-
}));
|
|
806
|
-
}
|
|
807
|
-
/** Returns selected purpose claim values. */
|
|
808
|
-
getSelectedPurposes() {
|
|
809
|
-
return splitCsv(this.getActiveEntryClaim(ClaimConsent.purpose));
|
|
810
|
-
}
|
|
811
|
-
/** Replaces selected purpose claim values. */
|
|
812
|
-
setSelectedPurposes(codes) {
|
|
813
|
-
return this.setActiveEntryClaimList(ClaimConsent.purpose, codes);
|
|
814
|
-
}
|
|
815
|
-
/** Adds purpose claim values. */
|
|
816
|
-
addPurposes(codes) {
|
|
817
|
-
return this.addActiveEntryClaimList(ClaimConsent.purpose, codes);
|
|
818
|
-
}
|
|
819
|
-
/** Removes purpose claim values. */
|
|
820
|
-
removePurposes(codes) {
|
|
821
|
-
return this.removeActiveEntryClaimList(ClaimConsent.purpose, codes);
|
|
822
|
-
}
|
|
823
|
-
/** Returns selected actor roles as explicit classified items. */
|
|
824
|
-
getRolesClassified() {
|
|
825
|
-
const classified = {
|
|
826
|
-
professional: [],
|
|
827
|
-
relationship: [],
|
|
828
|
-
legalRepresentative: [],
|
|
829
|
-
other: [],
|
|
830
|
-
};
|
|
831
|
-
for (const roleToken of this.getSelectedRoles()) {
|
|
832
|
-
const role = getHealthcareRoleByClaim(roleToken);
|
|
833
|
-
if (!role) {
|
|
834
|
-
classified.other.push({
|
|
835
|
-
codingSystem: '',
|
|
836
|
-
code: roleToken,
|
|
837
|
-
display: roleToken,
|
|
838
|
-
});
|
|
839
|
-
continue;
|
|
840
|
-
}
|
|
841
|
-
const nextRole = {
|
|
842
|
-
family: role.family,
|
|
843
|
-
codingSystem: role.codingSystem,
|
|
844
|
-
code: role.code,
|
|
845
|
-
display: role.titleEn,
|
|
846
|
-
definition: role.definition,
|
|
847
|
-
};
|
|
848
|
-
if (role.family === HealthcareRoleFamilies.ProfessionalOccupationIsco08) {
|
|
849
|
-
classified.professional.push(nextRole);
|
|
850
|
-
}
|
|
851
|
-
else if (role.family === HealthcareRoleFamilies.PersonalRelationshipHl7) {
|
|
852
|
-
classified.relationship.push(nextRole);
|
|
853
|
-
}
|
|
854
|
-
else if (role.family === HealthcareRoleFamilies.LegalRepresentativeHl7) {
|
|
855
|
-
classified.legalRepresentative.push(nextRole);
|
|
856
|
-
}
|
|
857
|
-
else {
|
|
858
|
-
classified.other.push(nextRole);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return classified;
|
|
862
|
-
}
|
|
863
|
-
/** Returns selected actor-role claim values. */
|
|
864
|
-
getSelectedRoles() {
|
|
865
|
-
return splitCsv(this.getActiveEntryClaim(ClaimConsent.actorRole));
|
|
866
|
-
}
|
|
867
|
-
/** Replaces selected actor-role claim values. */
|
|
868
|
-
setSelectedRoles(codes) {
|
|
869
|
-
return this.setActiveEntryClaimList(ClaimConsent.actorRole, codes);
|
|
870
|
-
}
|
|
871
|
-
/** Adds actor-role claim values. */
|
|
872
|
-
addRoles(codes) {
|
|
873
|
-
return this.addActiveEntryClaimList(ClaimConsent.actorRole, codes);
|
|
874
|
-
}
|
|
875
|
-
/** Removes actor-role claim values. */
|
|
876
|
-
removeRoles(codes) {
|
|
877
|
-
return this.removeActiveEntryClaimList(ClaimConsent.actorRole, codes);
|
|
878
|
-
}
|
|
879
|
-
/** Returns consent actors grouped by jurisdiction, organization, and user. */
|
|
880
|
-
getActorsClassified() {
|
|
881
|
-
const claims = {
|
|
882
|
-
...(this.getActiveEntry()?.resource?.meta?.claims || {}),
|
|
883
|
-
};
|
|
884
|
-
const actorTokens = splitCsv(claims[ClaimConsent.actorIdentifier]);
|
|
885
|
-
const actorRole = parseConsentActorRole(splitCsv(claims[ClaimConsent.actorRole])[0] || '');
|
|
886
|
-
const jurisdictions = new Map();
|
|
887
|
-
const organizations = new Map();
|
|
888
|
-
const users = new Map();
|
|
889
|
-
for (const token of actorTokens) {
|
|
890
|
-
if (looksLikeJurisdictionToken(token)) {
|
|
891
|
-
jurisdictions.set(token, { code: token, display: token });
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
if (looksLikeEmailToken(token)) {
|
|
895
|
-
users.set(`email:${token}`, {
|
|
896
|
-
email: token,
|
|
897
|
-
...(actorRole ? { role: actorRole } : {}),
|
|
898
|
-
});
|
|
899
|
-
continue;
|
|
900
|
-
}
|
|
901
|
-
if (looksLikePhoneToken(token)) {
|
|
902
|
-
users.set(`phone:${token}`, {
|
|
903
|
-
phone: token,
|
|
904
|
-
...(actorRole ? { role: actorRole } : {}),
|
|
905
|
-
});
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
908
|
-
const organization = parseDidWebOrganizationToken(token);
|
|
909
|
-
if (organization) {
|
|
910
|
-
organizations.set(organization.domain, organization);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
return {
|
|
914
|
-
jurisdictions: Array.from(jurisdictions.values()),
|
|
915
|
-
organizations: Array.from(organizations.values()),
|
|
916
|
-
users: Array.from(users.values()),
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
getSelectedSectionCodesByFamily(claimKey, family) {
|
|
920
|
-
return splitCsv(this.getActiveEntryClaim(claimKey))
|
|
921
|
-
.filter((code) => getHealthcareSectionFamilyByCode(code) === family);
|
|
922
|
-
}
|
|
923
|
-
setSelectedSectionCodesByFamily(claimKey, family, codes) {
|
|
924
|
-
const preserved = splitCsv(this.getActiveEntryClaim(claimKey))
|
|
925
|
-
.filter((code) => getHealthcareSectionFamilyByCode(code) !== family);
|
|
926
|
-
return this.setActiveEntryClaimList(claimKey, [...preserved, ...codes]);
|
|
927
|
-
}
|
|
928
|
-
addSectionCodesByFamily(claimKey, family, codes) {
|
|
929
|
-
const next = [...this.getSelectedSectionCodesByFamily(claimKey, family), ...codes];
|
|
930
|
-
return this.setSelectedSectionCodesByFamily(claimKey, family, next);
|
|
931
|
-
}
|
|
932
|
-
removeSectionCodesByFamily(claimKey, family, codes) {
|
|
933
|
-
const codesToRemove = new Set(normalizeCsvValues(codes));
|
|
934
|
-
const next = this.getSelectedSectionCodesByFamily(claimKey, family)
|
|
935
|
-
.filter((code) => !codesToRemove.has(code));
|
|
936
|
-
return this.setSelectedSectionCodesByFamily(claimKey, family, next);
|
|
937
|
-
}
|
|
938
|
-
setActiveEntryClaimList(key, values) {
|
|
939
|
-
const normalized = normalizeCsvValues(values);
|
|
940
|
-
if (normalized.length === 0) {
|
|
941
|
-
return this.removeActiveEntryClaim(key);
|
|
942
|
-
}
|
|
943
|
-
return this.setActiveEntryClaim(key, normalized.join(CSV_SEPARATOR));
|
|
944
|
-
}
|
|
945
|
-
addActiveEntryClaimList(key, values) {
|
|
946
|
-
const next = normalizeCsvValues([
|
|
947
|
-
...splitCsv(this.getActiveEntryClaim(key)),
|
|
948
|
-
...values,
|
|
949
|
-
]);
|
|
950
|
-
return this.setActiveEntryClaimList(key, next);
|
|
951
|
-
}
|
|
952
|
-
removeActiveEntryClaimList(key, values) {
|
|
953
|
-
const valuesToRemove = new Set(normalizeCsvValues(values));
|
|
954
|
-
const next = splitCsv(this.getActiveEntryClaim(key))
|
|
955
|
-
.filter((value) => !valuesToRemove.has(value));
|
|
956
|
-
return this.setActiveEntryClaimList(key, next);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
/**
|
|
960
|
-
* High-level factory for consent-access editing.
|
|
961
|
-
*
|
|
962
|
-
* Prefer this name in onboarding docs when the developer intent is:
|
|
963
|
-
* "edit a Consent access bundle carried by a Communication".
|
|
964
|
-
*/
|
|
965
|
-
export function createConsentAccessEditor(options = {}) {
|
|
966
|
-
return new ConsentAccessEditor(options);
|
|
967
|
-
}
|
|
968
|
-
function ensureEntryResource(entry, mode) {
|
|
969
|
-
const resource = entry.resource;
|
|
970
|
-
if (resource && typeof resource === 'object') {
|
|
971
|
-
return resource;
|
|
972
|
-
}
|
|
973
|
-
if (mode === 'normalize') {
|
|
974
|
-
return { meta: { claims: {} } };
|
|
975
|
-
}
|
|
976
|
-
throw new Error('Active entry does not contain a valid resource object.');
|
|
977
|
-
}
|
|
978
|
-
function validateBundleLike(bundle, mode) {
|
|
979
|
-
const looksLikeBundle = bundle && bundle.resourceType === ResourceTypesFhirR4.Bundle && Array.isArray(bundle.data);
|
|
980
|
-
if (looksLikeBundle) {
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
if (mode === 'normalize') {
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
throw new Error('Decoded attachment is not a valid BundleJsonApi payload.');
|
|
987
|
-
}
|
|
988
|
-
function createEmptyBundle() {
|
|
989
|
-
return {
|
|
990
|
-
resourceType: ResourceTypesFhirR4.Bundle,
|
|
991
|
-
type: 'batch',
|
|
992
|
-
data: [],
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
function encodeBundleToBase64(bundle) {
|
|
996
|
-
return Buffer.from(JSON.stringify(bundle), 'utf8').toString('base64');
|
|
997
|
-
}
|
|
998
|
-
function cloneBundle(bundle) {
|
|
999
|
-
return JSON.parse(JSON.stringify(bundle));
|
|
1000
|
-
}
|
|
1001
|
-
function cloneEntry(entry) {
|
|
1002
|
-
return JSON.parse(JSON.stringify(entry));
|
|
1003
|
-
}
|
|
1004
|
-
function cloneUnknownValue(value) {
|
|
1005
|
-
if (value === undefined) {
|
|
1006
|
-
return value;
|
|
1007
|
-
}
|
|
1008
|
-
return JSON.parse(JSON.stringify(value));
|
|
1009
|
-
}
|
|
1010
|
-
function asTrimmedString(value) {
|
|
1011
|
-
if (value === undefined || value === null) {
|
|
1012
|
-
return '';
|
|
1013
|
-
}
|
|
1014
|
-
return String(value).trim();
|
|
1015
|
-
}
|
|
1016
|
-
function resolveContainedDocumentsClaimKey(resourceType) {
|
|
1017
|
-
if (resourceType === ResourceTypesFhirR4.Consent) {
|
|
1018
|
-
return ClaimConsent.containedDocuments;
|
|
1019
|
-
}
|
|
1020
|
-
if (resourceType === ResourceTypesFhirR4.MedicationStatement) {
|
|
1021
|
-
return MedicationStatementClaim.ContainedDocuments;
|
|
1022
|
-
}
|
|
1023
|
-
if (resourceType === ResourceTypesFhirR4.Condition) {
|
|
1024
|
-
return ConditionClaim.ContainedDocuments;
|
|
1025
|
-
}
|
|
1026
|
-
if (resourceType === ResourceTypesFhirR4.AllergyIntolerance) {
|
|
1027
|
-
return AllergyIntoleranceClaim.ContainedDocuments;
|
|
1028
|
-
}
|
|
1029
|
-
return '';
|
|
1030
|
-
}
|
|
1031
|
-
function resolveSubjectFromClaims(claims) {
|
|
1032
|
-
return asTrimmedString(claims[ClaimConsent.subject]
|
|
1033
|
-
|| claims[MedicationStatementClaim.Subject]
|
|
1034
|
-
|| claims[ConditionClaim.Subject]
|
|
1035
|
-
|| claims[AllergyIntoleranceClaim.Subject]
|
|
1036
|
-
|| claims[AllergyIntoleranceClaim.Patient]
|
|
1037
|
-
|| claims[DocumentReferenceClaim.Subject]);
|
|
1038
|
-
}
|
|
1039
|
-
function setIfMissing(target, key, value) {
|
|
1040
|
-
if (target[key] !== undefined) {
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
if (value === undefined || value === null || String(value).trim() === '') {
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
target[key] = value;
|
|
1047
|
-
}
|
|
1048
|
-
function runtimeUuid(prefix) {
|
|
1049
|
-
const cryptoLike = globalThis;
|
|
1050
|
-
if (typeof cryptoLike.crypto?.randomUUID === 'function') {
|
|
1051
|
-
return cryptoLike.crypto.randomUUID();
|
|
1052
|
-
}
|
|
1053
|
-
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1054
|
-
}
|
|
1055
|
-
const CSV_SEPARATOR = ',';
|
|
1056
|
-
const DID_WEB_PREFIX = 'did:web:';
|
|
1057
|
-
const PHONE_PREFIX = 'tel:';
|
|
1058
|
-
function splitCsv(value) {
|
|
1059
|
-
return normalizeCsvValues(String(value || '').split(CSV_SEPARATOR));
|
|
1060
|
-
}
|
|
1061
|
-
function normalizeCsvValues(values) {
|
|
1062
|
-
return Array.from(new Set(values
|
|
1063
|
-
.map((item) => String(item || '').trim())
|
|
1064
|
-
.filter(Boolean)));
|
|
1065
|
-
}
|
|
1066
|
-
function buildClassifiedConsentTarget(kind, code, scopeCodes, sectionFamily) {
|
|
1067
|
-
const normalizedCode = String(code || '').trim();
|
|
1068
|
-
return {
|
|
1069
|
-
target: {
|
|
1070
|
-
kind,
|
|
1071
|
-
code: normalizedCode,
|
|
1072
|
-
display: resolveConsentTargetDisplay(kind, normalizedCode),
|
|
1073
|
-
...(kind === ConsentEditorTargetKinds.Section
|
|
1074
|
-
? { sectionFamily: sectionFamily || getHealthcareSectionFamilyByCode(normalizedCode) }
|
|
1075
|
-
: {}),
|
|
1076
|
-
},
|
|
1077
|
-
scopes: normalizeScopeCodes(scopeCodes).map((scopeCode) => ({
|
|
1078
|
-
code: scopeCode,
|
|
1079
|
-
display: resolveConsentScopeDisplay(scopeCode),
|
|
1080
|
-
})),
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
function buildSectionCatalogOptions(family) {
|
|
1084
|
-
return Object.values(getHealthcareSectionsByFamily(family)).map((descriptor) => buildClassifiedConsentTarget(ConsentEditorTargetKinds.Section, descriptor.attributeValue, [ConsentEditorScopeCodes.Read]));
|
|
1085
|
-
}
|
|
1086
|
-
function normalizeScopeCodes(scopeCodes) {
|
|
1087
|
-
const values = Array.from(new Set(scopeCodes.map((scopeCode) => String(scopeCode).trim()).filter(Boolean)));
|
|
1088
|
-
return values.filter((scopeCode) => Object.values(ConsentEditorScopeCodes).includes(scopeCode));
|
|
1089
|
-
}
|
|
1090
|
-
function normalizeClassifiedTargets(targets) {
|
|
1091
|
-
const result = new Map();
|
|
1092
|
-
for (const target of targets) {
|
|
1093
|
-
const normalized = buildClassifiedConsentTarget(target.target.kind, String(target.target.code || '').trim(), target.scopes.map((scope) => scope.code), target.target.sectionFamily);
|
|
1094
|
-
const key = `${normalized.target.kind}:${normalized.target.code}`;
|
|
1095
|
-
result.set(key, normalized);
|
|
1096
|
-
}
|
|
1097
|
-
return Array.from(result.values());
|
|
1098
|
-
}
|
|
1099
|
-
function resolveConsentTargetDisplay(kind, code) {
|
|
1100
|
-
if (kind === ConsentEditorTargetKinds.ResourceType) {
|
|
1101
|
-
return code;
|
|
1102
|
-
}
|
|
1103
|
-
if (kind === ConsentEditorTargetKinds.Section) {
|
|
1104
|
-
const loincCode = code.includes('|') ? code.split('|').slice(-1)[0] : code;
|
|
1105
|
-
return getHealthcareSectionByCode(loincCode)?.titleEn;
|
|
1106
|
-
}
|
|
1107
|
-
return undefined;
|
|
1108
|
-
}
|
|
1109
|
-
function resolveConsentScopeDisplay(scopeCode) {
|
|
1110
|
-
if (scopeCode === ConsentEditorScopeCodes.Search)
|
|
1111
|
-
return 'search';
|
|
1112
|
-
if (scopeCode === ConsentEditorScopeCodes.Read)
|
|
1113
|
-
return 'read';
|
|
1114
|
-
if (scopeCode === ConsentEditorScopeCodes.Create)
|
|
1115
|
-
return 'create';
|
|
1116
|
-
if (scopeCode === ConsentEditorScopeCodes.Update)
|
|
1117
|
-
return 'update';
|
|
1118
|
-
return 'delete';
|
|
1119
|
-
}
|
|
1120
|
-
function resolveConsentPurposeDisplay(code) {
|
|
1121
|
-
const normalized = String(code || '').trim();
|
|
1122
|
-
if (normalized === HealthcareConsentPurposes.Treatment) {
|
|
1123
|
-
return 'Treatment';
|
|
1124
|
-
}
|
|
1125
|
-
if (normalized === HealthcareConsentPurposes.EmergencyTreatment) {
|
|
1126
|
-
return 'Emergency treatment';
|
|
1127
|
-
}
|
|
1128
|
-
if (normalized === HealthcareConsentPurposes.CareManagement) {
|
|
1129
|
-
return 'Care management';
|
|
1130
|
-
}
|
|
1131
|
-
if (normalized === HealthcareConsentPurposes.Operations) {
|
|
1132
|
-
return 'Operations';
|
|
1133
|
-
}
|
|
1134
|
-
if (normalized === HealthcareConsentPurposes.PatientAdministration) {
|
|
1135
|
-
return 'Patient administration';
|
|
1136
|
-
}
|
|
1137
|
-
if (normalized === HealthcareConsentPurposes.RecordsManagement) {
|
|
1138
|
-
return 'Records management';
|
|
1139
|
-
}
|
|
1140
|
-
return normalized;
|
|
1141
|
-
}
|
|
1142
|
-
function looksLikeEmailToken(value) {
|
|
1143
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || '').trim().toLowerCase());
|
|
1144
|
-
}
|
|
1145
|
-
function looksLikePhoneToken(value) {
|
|
1146
|
-
return String(value || '').trim().startsWith(PHONE_PREFIX);
|
|
1147
|
-
}
|
|
1148
|
-
function looksLikeJurisdictionToken(value) {
|
|
1149
|
-
return /^[A-Z]{2}([\-:][A-Z0-9]+)?$/.test(String(value || '').trim());
|
|
1150
|
-
}
|
|
1151
|
-
function parseDidWebOrganizationToken(value) {
|
|
1152
|
-
const normalized = String(value || '').trim();
|
|
1153
|
-
if (!normalized.startsWith(DID_WEB_PREFIX)) {
|
|
1154
|
-
return undefined;
|
|
1155
|
-
}
|
|
1156
|
-
const segments = normalized.slice(DID_WEB_PREFIX.length).split(':').filter(Boolean);
|
|
1157
|
-
if (segments.length === 0) {
|
|
1158
|
-
return undefined;
|
|
1159
|
-
}
|
|
1160
|
-
const [domain, ...departmentSegments] = segments;
|
|
1161
|
-
return {
|
|
1162
|
-
domain,
|
|
1163
|
-
display: domain,
|
|
1164
|
-
departments: departmentSegments.map((segment) => ({
|
|
1165
|
-
code: segment,
|
|
1166
|
-
display: segment,
|
|
1167
|
-
})),
|
|
1168
|
-
locations: [],
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
function parseConsentActorRole(value) {
|
|
1172
|
-
const normalized = String(value || '').trim();
|
|
1173
|
-
if (!normalized) {
|
|
1174
|
-
return undefined;
|
|
1175
|
-
}
|
|
1176
|
-
const healthcareRole = getHealthcareRoleByClaim(normalized);
|
|
1177
|
-
if (healthcareRole) {
|
|
1178
|
-
return {
|
|
1179
|
-
codingSystem: healthcareRole.codingSystem,
|
|
1180
|
-
code: healthcareRole.code,
|
|
1181
|
-
display: healthcareRole.titleEn,
|
|
1182
|
-
};
|
|
1183
|
-
}
|
|
1184
|
-
const separatorIndex = normalized.indexOf('|');
|
|
1185
|
-
if (separatorIndex < 0) {
|
|
1186
|
-
return {
|
|
1187
|
-
codingSystem: '',
|
|
1188
|
-
code: normalized,
|
|
1189
|
-
display: normalized,
|
|
1190
|
-
};
|
|
1191
|
-
}
|
|
1192
|
-
return {
|
|
1193
|
-
codingSystem: normalized.slice(0, separatorIndex),
|
|
1194
|
-
code: normalized.slice(separatorIndex + 1),
|
|
1195
|
-
display: normalized,
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1198
|
-
function flattenClassifiedActors(classifiedActors) {
|
|
1199
|
-
return normalizeCsvValues([
|
|
1200
|
-
...classifiedActors.jurisdictions.map((jurisdiction) => jurisdiction.code),
|
|
1201
|
-
...classifiedActors.organizations.map(serializeClassifiedOrganization),
|
|
1202
|
-
...classifiedActors.users.flatMap((user) => [user.email, user.phone]),
|
|
1203
|
-
]);
|
|
1204
|
-
}
|
|
1205
|
-
function serializeClassifiedOrganization(organization) {
|
|
1206
|
-
const domain = String(organization.domain || '').trim();
|
|
1207
|
-
const departments = organization.departments
|
|
1208
|
-
.map((department) => String(department.code || '').trim())
|
|
1209
|
-
.filter(Boolean);
|
|
1210
|
-
return [DID_WEB_PREFIX.replace(/:$/, ''), domain, ...departments].join(':');
|
|
1211
|
-
}
|
|
1212
|
-
function flattenClassifiedRoles(classifiedRoles) {
|
|
1213
|
-
return normalizeCsvValues([
|
|
1214
|
-
...classifiedRoles.professional.map(serializeClassifiedRole),
|
|
1215
|
-
...classifiedRoles.relationship.map(serializeClassifiedRole),
|
|
1216
|
-
...classifiedRoles.legalRepresentative.map(serializeClassifiedRole),
|
|
1217
|
-
...classifiedRoles.other.map(serializeClassifiedRole),
|
|
1218
|
-
]);
|
|
1219
|
-
}
|
|
1220
|
-
function serializeClassifiedRole(role) {
|
|
1221
|
-
const descriptor = getHealthcareRoleByClaim(`${String(role.codingSystem || '').trim()}|${String(role.code || '').trim()}`);
|
|
1222
|
-
if (descriptor) {
|
|
1223
|
-
return descriptor.claim;
|
|
1224
|
-
}
|
|
1225
|
-
return String(role.code || '').trim() || String(role.display || '').trim();
|
|
1226
|
-
}
|
|
1227
|
-
function flattenClassifiedTargets(classifiedTargets) {
|
|
1228
|
-
const coreSections = [];
|
|
1229
|
-
const kindOfDocuments = [];
|
|
1230
|
-
const typeOfServices = [];
|
|
1231
|
-
const subjectMatterDomains = [];
|
|
1232
|
-
const resourceTypes = [];
|
|
1233
|
-
for (const entry of normalizeClassifiedTargets(classifiedTargets)) {
|
|
1234
|
-
if (entry.target.kind === ConsentEditorTargetKinds.ResourceType) {
|
|
1235
|
-
resourceTypes.push(entry.target.code);
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
const family = entry.target.sectionFamily || getHealthcareSectionFamilyByCode(entry.target.code);
|
|
1239
|
-
if (family === HealthcareCanonicalSectionFamilies.KindOfDocument) {
|
|
1240
|
-
kindOfDocuments.push(entry.target.code);
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
if (family === HealthcareCanonicalSectionFamilies.TypeOfService) {
|
|
1244
|
-
typeOfServices.push(entry.target.code);
|
|
1245
|
-
continue;
|
|
1246
|
-
}
|
|
1247
|
-
if (family === HealthcareCanonicalSectionFamilies.SubjectMatterDomain) {
|
|
1248
|
-
subjectMatterDomains.push(entry.target.code);
|
|
1249
|
-
continue;
|
|
1250
|
-
}
|
|
1251
|
-
coreSections.push(entry.target.code);
|
|
1252
|
-
}
|
|
1253
|
-
return {
|
|
1254
|
-
coreSections: normalizeCsvValues(coreSections),
|
|
1255
|
-
kindOfDocuments: normalizeCsvValues(kindOfDocuments),
|
|
1256
|
-
typeOfServices: normalizeCsvValues(typeOfServices),
|
|
1257
|
-
subjectMatterDomains: normalizeCsvValues(subjectMatterDomains),
|
|
1258
|
-
resourceTypes: normalizeCsvValues(resourceTypes),
|
|
1259
|
-
};
|
|
1260
|
-
}
|