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