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.
Files changed (57) hide show
  1. package/README.md +0 -1
  2. package/dist/examples/bundle-didcomm-payload.d.ts +11 -0
  3. package/dist/examples/bundle-didcomm-payload.js +13 -0
  4. package/dist/examples/communication-attached-bundle-session.js +2 -1
  5. package/dist/examples/communication-didcomm-payload.d.ts +35 -0
  6. package/dist/examples/communication-didcomm-payload.js +38 -0
  7. package/dist/examples/index.d.ts +4 -0
  8. package/dist/examples/index.js +4 -0
  9. package/dist/examples/profile-manager-mem.d.ts +45 -0
  10. package/dist/examples/profile-manager-mem.js +29 -0
  11. package/dist/examples/shared.d.ts +1 -0
  12. package/dist/examples/shared.js +1 -0
  13. package/dist/examples/wallet-mem.d.ts +38 -0
  14. package/dist/examples/wallet-mem.js +40 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +1 -0
  17. package/dist/interfaces/IProfileOutboxRepository.d.ts +36 -0
  18. package/dist/interfaces/IProfileOutboxRepository.js +2 -0
  19. package/dist/interfaces/IWallet.d.ts +74 -0
  20. package/dist/interfaces/IWallet.js +2 -0
  21. package/dist/interfaces/IWalletQueue.d.ts +30 -0
  22. package/dist/interfaces/IWalletQueue.js +2 -0
  23. package/dist/interfaces/index.d.ts +8 -0
  24. package/dist/interfaces/index.js +8 -0
  25. package/dist/models/communication-attached-bundle-session.d.ts +193 -0
  26. package/dist/models/communication-attached-bundle-session.js +34 -0
  27. package/dist/models/index.d.ts +3 -0
  28. package/dist/models/index.js +3 -0
  29. package/dist/models/interoperable-claims/diagnostic-report-claims.d.ts +1 -2
  30. package/dist/models/interoperable-claims/diagnostic-report-claims.js +1 -2
  31. package/dist/models/profile-manager.d.ts +85 -0
  32. package/dist/models/profile-manager.js +2 -0
  33. package/dist/models/wallet.d.ts +59 -0
  34. package/dist/models/wallet.js +22 -0
  35. package/dist/utils/backend-message-manager-mem.d.ts +67 -0
  36. package/dist/utils/backend-message-manager-mem.js +227 -0
  37. package/dist/utils/bundle-didcomm-payload.d.ts +29 -0
  38. package/dist/utils/bundle-didcomm-payload.js +48 -0
  39. package/dist/utils/communication-attached-bundle-session-helpers.d.ts +54 -0
  40. package/dist/utils/communication-attached-bundle-session-helpers.js +517 -0
  41. package/dist/utils/communication-attached-bundle-session.d.ts +59 -302
  42. package/dist/utils/communication-attached-bundle-session.js +114 -839
  43. package/dist/utils/communication-consent-access-editor.d.ts +85 -0
  44. package/dist/utils/communication-consent-access-editor.js +244 -0
  45. package/dist/utils/communication-didcomm-payload.d.ts +33 -0
  46. package/dist/utils/communication-didcomm-payload.js +75 -0
  47. package/dist/utils/index.d.ts +8 -0
  48. package/dist/utils/index.js +8 -0
  49. package/dist/utils/profile-manager-mem.d.ts +69 -0
  50. package/dist/utils/profile-manager-mem.js +79 -0
  51. package/dist/utils/profile-outbox-memory-repository.d.ts +34 -0
  52. package/dist/utils/profile-outbox-memory-repository.js +57 -0
  53. package/dist/utils/wallet-mem.d.ts +93 -0
  54. package/dist/utils/wallet-mem.js +277 -0
  55. package/dist/utils/wallet-memory-queue.d.ts +32 -0
  56. package/dist/utils/wallet-memory-queue.js +82 -0
  57. 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 { detectDuplicateConsentRuleConflicts, } from './consent-duplicate-rules.js';
13
- import { MedicationStatementClaim, } from '../models/interoperable-claims/medication-statement-claims.js';
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 index or fullUrl. */
151
+ /** Selects an active entry by position or fullUrl. */
159
152
  selectActiveEntry(selection) {
160
- if (typeof selection.index === 'number') {
161
- this.assertEntryIndex(selection.index);
162
- this.activeEntryIndex = selection.index;
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 index or fullUrl.');
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
- * Consent-first helper for developer onboarding.
199
+ * Reusable resource-typed wrapper around `upsertActiveEntry(...)`.
204
200
  *
205
- * Expected keys should come from `ClaimConsent` in caller code.
201
+ * Resource-specific helpers should delegate here instead of re-embedding the
202
+ * same `resourceType/fullUrl/type/request/claims` plumbing.
206
203
  */
207
- upsertActiveConsentEntry(input) {
204
+ upsertActiveClaimsResourceEntry(resourceType, input) {
208
205
  return this.upsertActiveEntry({
209
- resourceType: ResourceTypesFhirR4.Consent,
210
- claims: input.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.upsertActiveEntry({
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.upsertActiveEntry({
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.upsertActiveEntry({
254
- resourceType: ResourceTypesFhirR4.Condition,
255
- claims: {
256
- ...input.claims,
257
- },
258
- fullUrl: input.fullUrl,
259
- type: input.type,
260
- request: input.request,
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.upsertActiveEntry({
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.upsertActiveEntry({
286
- resourceType: ResourceTypesFhirR4.AllergyIntolerance,
287
- claims: {
288
- ...input.claims,
289
- },
290
- fullUrl: input.fullUrl,
291
- type: input.type,
292
- request: input.request,
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': 'org.hl7.fhir.api',
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 consentSubject = asTrimmedString(claims[ClaimConsent.subject]);
464
- if (consentSubject) {
465
- return consentSubject;
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
- const consentIdentifier = asTrimmedString(claims[ClaimConsent.identifier]);
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
- const consentIdentifier = asTrimmedString(claims[ClaimConsent.identifier]);
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
- }