gdc-common-utils-ts 2.0.6 → 2.0.8

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.
@@ -29,6 +29,28 @@ export type ClinicalResourceExpandedView = Readonly<{
29
29
  xhtml?: string;
30
30
  notes: string[];
31
31
  }>;
32
+ export type ClinicalResourceLike = Readonly<{
33
+ resourceType?: string;
34
+ text?: {
35
+ div?: unknown;
36
+ };
37
+ meta?: {
38
+ claims?: Record<string, unknown>;
39
+ };
40
+ note?: unknown;
41
+ code?: unknown;
42
+ valueQuantity?: unknown;
43
+ [key: string]: unknown;
44
+ }>;
45
+ export type LocalTextAndIntDisplay = Readonly<{
46
+ localText?: string;
47
+ internationalDisplay?: string;
48
+ combined?: string;
49
+ }>;
50
+ export type NarrativeResult = Readonly<{
51
+ xhtml?: string;
52
+ source: 'resource.text.div' | 'derived-from-claims' | 'missing';
53
+ }>;
32
54
  export type ClinicalResourceEntryLike = Readonly<{
33
55
  fullUrl?: string;
34
56
  type?: string;
@@ -65,3 +87,18 @@ export declare function toClinicalResourceExpandedView(entry: ClinicalResourceEn
65
87
  * Maps all entries from a Bundle into expanded views.
66
88
  */
67
89
  export declare function toClinicalResourceExpandedViews(bundle: ClinicalResourceBundleLike): ClinicalResourceExpandedView[];
90
+ /**
91
+ * Returns the most useful local text plus international display pair that can
92
+ * be inferred from one FHIR-like resource and its `meta.claims`.
93
+ */
94
+ export declare function getLocalTextAndIntDisplay(resource: ClinicalResourceLike): LocalTextAndIntDisplay;
95
+ /**
96
+ * Returns XHTML narrative for one FHIR-like resource, preferring
97
+ * `resource.text.div` and otherwise deriving a deterministic fallback from
98
+ * canonical `meta.claims`.
99
+ */
100
+ export declare function getXhtmlOrDerived(resource: ClinicalResourceLike): string | undefined;
101
+ /**
102
+ * Returns XHTML plus the source used to obtain it.
103
+ */
104
+ export declare function getNarrative(resource: ClinicalResourceLike): NarrativeResult;
@@ -4,7 +4,9 @@ import { ClaimConsent } from '../models/consent-rule.js';
4
4
  import { AllergyIntoleranceClaim, AllergyIntoleranceClaimsFhirApi, } from '../models/interoperable-claims/allergy-intolerance-claims.js';
5
5
  import { CommunicationClaim } from '../models/interoperable-claims/communication-claims.js';
6
6
  import { ConditionClaim, ConditionClaimsFhirApi, } from '../models/interoperable-claims/condition-claims.js';
7
- import { MedicationStatementClaim, MedicationStatementClaimsFhirApi, } from '../models/interoperable-claims/medication-statement-claims.js';
7
+ import { ImmunizationClaim } from '../models/interoperable-claims/immunization-claims.js';
8
+ import { MedicationStatementClaim, MedicationStatementClaimsFhirApi, MedicationStatementClaimsFhirApiExtended, } from '../models/interoperable-claims/medication-statement-claims.js';
9
+ import { ObservationClaim } from '../models/interoperable-claims/observation-claims.js';
8
10
  const CONSENT_ACTOR_REFERENCE_CLAIM = 'Consent.actor-reference';
9
11
  const GENERIC_CREATOR_CLAIM_SUFFIX = '.creator';
10
12
  const GENERIC_PERFORMER_CLAIM_SUFFIX = '.performer';
@@ -70,6 +72,73 @@ export function toClinicalResourceExpandedView(entry) {
70
72
  export function toClinicalResourceExpandedViews(bundle) {
71
73
  return readBundleEntries(bundle).map((entry) => toClinicalResourceExpandedView(entry));
72
74
  }
75
+ /**
76
+ * Returns the most useful local text plus international display pair that can
77
+ * be inferred from one FHIR-like resource and its `meta.claims`.
78
+ */
79
+ export function getLocalTextAndIntDisplay(resource) {
80
+ const claims = asRecord(resource?.meta?.claims);
81
+ const resourceType = resolveResourceType({ resource }, claims);
82
+ const localText = firstDefinedText([
83
+ resolveResourceCodeText(resource),
84
+ findBySuffix(claims, '.code-text'),
85
+ findBySuffix(claims, '.medication-text'),
86
+ findBySuffix(claims, '.vaccine-code-text'),
87
+ findBySuffix(claims, '.value-concept-text'),
88
+ ]) || resolveTitle(resourceType, claims);
89
+ const internationalDisplay = firstDefinedText([
90
+ resolveResourceCodeDisplay(resource),
91
+ findBySuffix(claims, '.code-display'),
92
+ findBySuffix(claims, '.vaccine-code-display'),
93
+ findBySuffix(claims, '.value-concept-display'),
94
+ ]);
95
+ const combined = buildCombinedLabel(localText, internationalDisplay);
96
+ return {
97
+ ...(localText ? { localText } : {}),
98
+ ...(internationalDisplay ? { internationalDisplay } : {}),
99
+ ...(combined ? { combined } : {}),
100
+ };
101
+ }
102
+ /**
103
+ * Returns XHTML narrative for one FHIR-like resource, preferring
104
+ * `resource.text.div` and otherwise deriving a deterministic fallback from
105
+ * canonical `meta.claims`.
106
+ */
107
+ export function getXhtmlOrDerived(resource) {
108
+ return getNarrative(resource).xhtml;
109
+ }
110
+ /**
111
+ * Returns XHTML plus the source used to obtain it.
112
+ */
113
+ export function getNarrative(resource) {
114
+ const fromFhirNarrative = trimValue(asRecord(resource?.text).div);
115
+ if (fromFhirNarrative) {
116
+ return {
117
+ xhtml: fromFhirNarrative,
118
+ source: 'resource.text.div',
119
+ };
120
+ }
121
+ const claims = asRecord(resource?.meta?.claims);
122
+ const fromSpecificClaim = findBySuffix(claims, '.xhtml')
123
+ || findBySuffix(claims, '.text-div');
124
+ if (fromSpecificClaim) {
125
+ return {
126
+ xhtml: fromSpecificClaim,
127
+ source: 'derived-from-claims',
128
+ };
129
+ }
130
+ const resourceType = resolveResourceType({ resource }, claims);
131
+ const lines = buildNarrativeLines(resourceType, claims, resource);
132
+ if (lines.length === 0) {
133
+ return {
134
+ source: 'missing',
135
+ };
136
+ }
137
+ return {
138
+ xhtml: `<div xmlns="http://www.w3.org/1999/xhtml">${lines.map((line) => `<p>${escapeHtml(line)}</p>`).join('')}</div>`,
139
+ source: 'derived-from-claims',
140
+ };
141
+ }
73
142
  function readClaims(entry) {
74
143
  const resourceClaims = asRecord(entry?.resource?.meta?.claims);
75
144
  const legacyClaims = asRecord(entry?.meta?.claims);
@@ -284,13 +353,16 @@ function resolveActors(resourceType, claims) {
284
353
  return out;
285
354
  }
286
355
  function resolveXhtml(entry, claims) {
287
- const fromFhirNarrative = trimValue(asRecord(entry?.resource?.text).div);
288
- if (fromFhirNarrative) {
289
- return fromFhirNarrative;
290
- }
291
- const fromSpecificClaim = findBySuffix(claims, '.xhtml')
292
- || findBySuffix(claims, '.text-div');
293
- return fromSpecificClaim || undefined;
356
+ if (!entry?.resource) {
357
+ return undefined;
358
+ }
359
+ const mergedResource = {
360
+ ...entry.resource,
361
+ meta: {
362
+ claims,
363
+ },
364
+ };
365
+ return getXhtmlOrDerived(mergedResource);
294
366
  }
295
367
  function resolveNotes(entry, resourceType, claims) {
296
368
  const notesFromResource = readFhirNoteArray(entry);
@@ -379,6 +451,211 @@ function findBySuffix(claims, keySuffix) {
379
451
  }
380
452
  return undefined;
381
453
  }
454
+ function resolveResourceCodeText(resource) {
455
+ return trimValue(asRecord(resource?.code).text) || undefined;
456
+ }
457
+ function resolveResourceCodeDisplay(resource) {
458
+ const coding = asArray(asRecord(resource?.code).coding);
459
+ for (const item of coding) {
460
+ const display = trimValue(asRecord(item).display);
461
+ if (display) {
462
+ return display;
463
+ }
464
+ }
465
+ return undefined;
466
+ }
467
+ function firstDefinedText(values) {
468
+ for (const value of values) {
469
+ const normalized = trimValue(value);
470
+ if (normalized) {
471
+ return normalized;
472
+ }
473
+ }
474
+ return undefined;
475
+ }
476
+ function buildCombinedLabel(localText, internationalDisplay) {
477
+ const local = trimValue(localText);
478
+ const intl = trimValue(internationalDisplay);
479
+ if (local && intl && local !== intl) {
480
+ return `${local} (${intl})`;
481
+ }
482
+ return local || intl || undefined;
483
+ }
484
+ function buildNarrativeLines(resourceType, claims, resource) {
485
+ const lines = [];
486
+ const label = getLocalTextAndIntDisplay(resource).combined || resolveTitle(resourceType, claims) || resourceType;
487
+ if (label) {
488
+ lines.push(label);
489
+ }
490
+ const date = resolveDate(resourceType, claims);
491
+ if (date) {
492
+ lines.push(`Date: ${date}`);
493
+ }
494
+ const periodStart = resolvePeriodStart(resourceType, claims);
495
+ const periodEnd = resolvePeriodEnd(resourceType, claims);
496
+ if (periodStart) {
497
+ lines.push(`Start: ${periodStart}`);
498
+ if (periodEnd) {
499
+ lines.push(`End: ${periodEnd}`);
500
+ }
501
+ }
502
+ appendFamilySpecificNarrativeLines(lines, resourceType, claims, resource);
503
+ return uniqueTokens(lines);
504
+ }
505
+ function appendFamilySpecificNarrativeLines(lines, resourceType, claims, resource) {
506
+ if (resourceType === ResourceTypesFhirR4.AllergyIntolerance) {
507
+ pushLine(lines, 'Clinical status', firstClaimValue(claims, [
508
+ AllergyIntoleranceClaim.ClinicalStatus,
509
+ AllergyIntoleranceClaimsFhirApi.ClinicalStatus,
510
+ ]));
511
+ pushLine(lines, 'Verification status', firstClaimValue(claims, [
512
+ AllergyIntoleranceClaim.VerificationStatus,
513
+ AllergyIntoleranceClaimsFhirApi.VerificationStatus,
514
+ ]));
515
+ pushLine(lines, 'Criticality', firstClaimValue(claims, [
516
+ AllergyIntoleranceClaim.Criticality,
517
+ AllergyIntoleranceClaimsFhirApi.Criticality,
518
+ ]));
519
+ pushLine(lines, 'Category', firstClaimCsvValue(claims, [
520
+ AllergyIntoleranceClaim.Category,
521
+ AllergyIntoleranceClaimsFhirApi.Category,
522
+ ]));
523
+ return;
524
+ }
525
+ if (resourceType === ResourceTypesFhirR4.Condition) {
526
+ pushLine(lines, 'Clinical status', firstClaimValue(claims, [
527
+ ConditionClaim.ClinicalStatus,
528
+ ConditionClaimsFhirApi.ClinicalStatus,
529
+ ]));
530
+ pushLine(lines, 'Verification status', firstClaimValue(claims, [
531
+ ConditionClaim.VerificationStatus,
532
+ ConditionClaimsFhirApi.VerificationStatus,
533
+ ]));
534
+ pushLine(lines, 'Severity', firstClaimValue(claims, [
535
+ ConditionClaim.Severity,
536
+ ConditionClaimsFhirApi.Severity,
537
+ ]));
538
+ pushLine(lines, 'Category', firstClaimCsvValue(claims, [
539
+ ConditionClaim.Category,
540
+ ConditionClaimsFhirApi.Category,
541
+ ]));
542
+ return;
543
+ }
544
+ if (resourceType === ResourceTypesFhirR4.MedicationStatement) {
545
+ pushLine(lines, 'Status', firstClaimValue(claims, [
546
+ MedicationStatementClaim.Status,
547
+ MedicationStatementClaimsFhirApi.Status,
548
+ ]));
549
+ pushLine(lines, 'Dose', buildQuantityLabel(firstDefinedText([
550
+ normalizeNumericValue(claims[MedicationStatementClaimsFhirApiExtended.DoseQuantityValue]),
551
+ normalizeNumericValue(claims['MedicationStatement.dose-quantity-value']),
552
+ ]), firstDefinedText([
553
+ trimValue(claims[MedicationStatementClaimsFhirApiExtended.DoseQuantityUnit]),
554
+ trimValue(claims['MedicationStatement.dose-quantity-unit']),
555
+ ])));
556
+ pushLine(lines, 'Timing', buildMedicationTimingLabel(claims));
557
+ pushLine(lines, 'Note', firstClaimValue(claims, [MedicationStatementClaim.Note]));
558
+ return;
559
+ }
560
+ if (resourceType === ResourceTypesFhirR4.Immunization) {
561
+ pushLine(lines, 'Status', trimValue(claims[ImmunizationClaim.Status]));
562
+ pushLine(lines, 'Vaccine', firstDefinedText([
563
+ findBySuffix(claims, '.vaccine-code-text'),
564
+ findBySuffix(claims, '.vaccine-code-display'),
565
+ trimValue(claims[ImmunizationClaim.VaccineCode]),
566
+ ]));
567
+ pushLine(lines, 'Performer', trimValue(claims[ImmunizationClaim.Performer]));
568
+ pushLine(lines, 'Note', trimValue(claims[ImmunizationClaim.Note]));
569
+ return;
570
+ }
571
+ if (resourceType === ResourceTypesFhirR4.Observation) {
572
+ appendObservationNarrativeLines(lines, claims, resource);
573
+ }
574
+ }
575
+ function appendObservationNarrativeLines(lines, claims, resource) {
576
+ const codeValue = firstDefinedText([
577
+ trimValue(claims[ObservationClaim.CodeValue]),
578
+ splitTokenCode(claims[ObservationClaim.Code]),
579
+ ]);
580
+ const systolic = normalizeNumericValue(claims[ObservationClaim.BloodPressureSystolicNumber]);
581
+ const diastolic = normalizeNumericValue(claims[ObservationClaim.BloodPressureDiastolicNumber]);
582
+ const unit = firstDefinedText([
583
+ trimValue(claims[ObservationClaim.ValueQuantityUnit]),
584
+ resolveObservationUnitFromResource(resource),
585
+ ]);
586
+ if (codeValue === '85354-9' || systolic || diastolic) {
587
+ if (systolic) {
588
+ pushLine(lines, 'Systolic', buildQuantityLabel(systolic, unit));
589
+ }
590
+ if (diastolic) {
591
+ pushLine(lines, 'Diastolic', buildQuantityLabel(diastolic, unit));
592
+ }
593
+ return;
594
+ }
595
+ const numericValue = normalizeNumericValue(claims[ObservationClaim.ValueQuantityNumber]);
596
+ if (numericValue || unit) {
597
+ pushLine(lines, 'Value', buildQuantityLabel(numericValue, unit));
598
+ }
599
+ pushLine(lines, 'Note', trimValue(claims[ObservationClaim.Note]));
600
+ }
601
+ function buildMedicationTimingLabel(claims) {
602
+ const frequency = firstDefinedText([
603
+ normalizeNumericValue(claims[MedicationStatementClaimsFhirApiExtended.TimingFrequency]),
604
+ normalizeNumericValue(claims['MedicationStatement.timing-frequency']),
605
+ ]);
606
+ const period = firstDefinedText([
607
+ normalizeNumericValue(claims[MedicationStatementClaimsFhirApiExtended.TimingPeriod]),
608
+ normalizeNumericValue(claims['MedicationStatement.timing-period']),
609
+ ]);
610
+ const unit = firstDefinedText([
611
+ trimValue(claims[MedicationStatementClaimsFhirApiExtended.TimingPeriodUnit]),
612
+ trimValue(claims['MedicationStatement.timing-period-unit']),
613
+ ]);
614
+ if (!frequency && !period && !unit) {
615
+ return undefined;
616
+ }
617
+ return [frequency ? `${frequency}x` : undefined, period ? `every ${period}` : undefined, unit].filter(Boolean).join(' ');
618
+ }
619
+ function buildQuantityLabel(value, unit) {
620
+ const normalizedValue = trimValue(value);
621
+ const normalizedUnit = trimValue(unit);
622
+ if (normalizedValue && normalizedUnit) {
623
+ return `${normalizedValue} ${normalizedUnit}`;
624
+ }
625
+ return normalizedValue || normalizedUnit || undefined;
626
+ }
627
+ function resolveObservationUnitFromResource(resource) {
628
+ const valueQuantity = asRecord(resource?.valueQuantity);
629
+ return trimValue(valueQuantity.unit) || trimValue(valueQuantity.code) || undefined;
630
+ }
631
+ function normalizeNumericValue(value) {
632
+ const normalized = trimValue(value);
633
+ return normalized || undefined;
634
+ }
635
+ function splitTokenCode(value) {
636
+ const normalized = trimValue(value);
637
+ if (!normalized) {
638
+ return undefined;
639
+ }
640
+ if (!normalized.includes('|')) {
641
+ return normalized;
642
+ }
643
+ const parts = normalized.split('|');
644
+ return trimValue(parts[parts.length - 1]) || undefined;
645
+ }
646
+ function pushLine(lines, label, value) {
647
+ const normalized = trimValue(value);
648
+ if (!normalized) {
649
+ return;
650
+ }
651
+ lines.push(`${label}: ${normalized}`);
652
+ }
653
+ function escapeHtml(value) {
654
+ return value
655
+ .replaceAll('&', '&amp;')
656
+ .replaceAll('<', '&lt;')
657
+ .replaceAll('>', '&gt;');
658
+ }
382
659
  function pushActor(out, actor) {
383
660
  const identifier = trimValue(actor.identifier);
384
661
  if (!identifier) {
@@ -414,3 +691,6 @@ function asRecord(value) {
414
691
  }
415
692
  return value;
416
693
  }
694
+ function asArray(value) {
695
+ return Array.isArray(value) ? value : [];
696
+ }
@@ -90,7 +90,7 @@ export declare const LEGAL_ORGANIZATION_ONBOARDING_JSON_SCHEMA: {
90
90
  * canonical `identifier.value`
91
91
  *
92
92
  * The result shape is intentionally assistant-friendly:
93
- * - `missingClaims` tells UI/voice flows what is still required
93
+ * - `missingClaims` tells UI/application flows what is still required
94
94
  * - `normalizedClaims` shows the post-derivation claim set
95
95
  * - `derived` explains which values were filled automatically
96
96
  */
@@ -67,7 +67,7 @@ function normalizeOptionalString(value) {
67
67
  * canonical `identifier.value`
68
68
  *
69
69
  * The result shape is intentionally assistant-friendly:
70
- * - `missingClaims` tells UI/voice flows what is still required
70
+ * - `missingClaims` tells UI/application flows what is still required
71
71
  * - `normalizedClaims` shows the post-derivation claim set
72
72
  * - `derived` explains which values were filled automatically
73
73
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },