fhir-data-utils-ts 0.3.5 → 0.3.6

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.
@@ -38,6 +38,11 @@ description: Complete or audit migrations, claim catalogs, converters, builders,
38
38
  storage remain in a separately versioned imaging/storage package.
39
39
  - Preserve `Encounter.appointment`; an Appointment lifecycle transition must
40
40
  not sever the encounter from the scheduling event that originated it.
41
+ - Build laboratory panels with `buildDiagnosticReportFlatGraph(...)`.
42
+ `DiagnosticReport.code` is mandatory and names the report/panel; result
43
+ measurements are independent `Observation` entries and formatted PDF/image
44
+ evidence is an independent `DocumentReference`. Preliminary state does not
45
+ itself authorize ledger or research projection.
41
46
 
42
47
  ## Change with executable contracts
43
48
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.6 - 2026-09-26
4
+
5
+ - Add a product-neutral claims-first DiagnosticReport graph builder for coded
6
+ laboratory panels, independent Observation results and PDF/image
7
+ DocumentReference evidence.
8
+ - Correct the document builder to emit the canonical DocumentReference claim
9
+ catalog and complete list helpers for report results, performers, specimens
10
+ and graph references.
11
+
3
12
  ## 0.3.5 - 2026-09-26
4
13
 
5
14
  - Preserve the originating `Encounter.appointment` reference and expose the
package/README.md CHANGED
@@ -29,6 +29,13 @@ through `<ResourceType>.contained-reference-list`; children do not carry
29
29
  as `<ResourceType>.language`, `.narrative-status` and `.xhtml`. Native FHIR is
30
30
  materialized only by an explicit R4/R5/R6 import or export adapter.
31
31
 
32
+ Laboratory authoring uses `buildDiagnosticReportFlatGraph(...)`.
33
+ `DiagnosticReport.code` is the coded report or panel type, atomic results are
34
+ independent LOINC-coded `Observation` entries, and PDF/JPEG/PNG evidence is an
35
+ independent `DocumentReference`. Preliminary reports can be reviewed and
36
+ securely exchanged; ledger and research eligibility remain downstream policy
37
+ and normally require the confirmed final lifecycle state.
38
+
32
39
  ## Public contract
33
40
 
34
41
  The root export and stable subpaths provide:
@@ -0,0 +1,31 @@
1
+ import { type DiagnosticReportStatus } from '../constants/clinical-statuses.js';
2
+ import { type FlatClaimResourceEntry } from '../flat-claim-resource-graph.js';
3
+ export type DiagnosticReportCoding = Readonly<{
4
+ system: string;
5
+ code: string;
6
+ display?: string;
7
+ text?: string;
8
+ }>;
9
+ export type DiagnosticReportFlatInput = Readonly<{
10
+ entryId: string;
11
+ identifier: string;
12
+ status: DiagnosticReportStatus;
13
+ subjectReference: string;
14
+ issuedAt: string;
15
+ reportCode: DiagnosticReportCoding;
16
+ encounterReference?: string;
17
+ performerReferences?: readonly string[];
18
+ results?: readonly FlatClaimResourceEntry[];
19
+ attachments?: readonly FlatClaimResourceEntry[];
20
+ }>;
21
+ /**
22
+ * Builds a claims-first DiagnosticReport graph for human, animal, or One Health
23
+ * laboratory reports. `DiagnosticReport.code` identifies the report/panel;
24
+ * atomic measurements remain independent `Observation` entries and PDF/JPEG/
25
+ * PNG evidence remains independent `DocumentReference` entries.
26
+ *
27
+ * `preliminary` reports can be exchanged and reviewed, but downstream ledger
28
+ * and secondary-use policy decides whether only `final` is eligible for those
29
+ * projections. This builder does not confer authority or consent.
30
+ */
31
+ export declare function buildDiagnosticReportFlatGraph(input: DiagnosticReportFlatInput): readonly FlatClaimResourceEntry[];
@@ -0,0 +1,86 @@
1
+ // Copyright 2026 ConnectHealth.info (Connecting Solution & Applications Ltd.) under the Apache License, Version 2.0.
2
+ import { DiagnosticReportClaim } from '../claims/diagnostic-report-claims.js';
3
+ import { DiagnosticReportStatuses } from '../constants/clinical-statuses.js';
4
+ import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
5
+ import { buildFlatClaimResourceEntry, linkFlatClaimResourceEntries, } from '../flat-claim-resource-graph.js';
6
+ const DIAGNOSTIC_REPORT_STATUSES = new Set(Object.values(DiagnosticReportStatuses));
7
+ /**
8
+ * Builds a claims-first DiagnosticReport graph for human, animal, or One Health
9
+ * laboratory reports. `DiagnosticReport.code` identifies the report/panel;
10
+ * atomic measurements remain independent `Observation` entries and PDF/JPEG/
11
+ * PNG evidence remains independent `DocumentReference` entries.
12
+ *
13
+ * `preliminary` reports can be exchanged and reviewed, but downstream ledger
14
+ * and secondary-use policy decides whether only `final` is eligible for those
15
+ * projections. This builder does not confer authority or consent.
16
+ */
17
+ export function buildDiagnosticReportFlatGraph(input) {
18
+ const status = required(input.status, 'diagnostic_report_status_required');
19
+ if (!DIAGNOSTIC_REPORT_STATUSES.has(status))
20
+ throw new TypeError('diagnostic_report_status_invalid');
21
+ const claims = {
22
+ [DiagnosticReportClaim.Identifier]: required(input.identifier, 'diagnostic_report_identifier_required'),
23
+ [DiagnosticReportClaim.Status]: status,
24
+ [DiagnosticReportClaim.Subject]: required(input.subjectReference, 'diagnostic_report_subject_required'),
25
+ [DiagnosticReportClaim.Date]: isoDateTime(input.issuedAt, 'diagnostic_report_date_invalid'),
26
+ [DiagnosticReportClaim.Code]: codingToken(input.reportCode),
27
+ };
28
+ if (input.reportCode.display?.trim())
29
+ claims[DiagnosticReportClaim.CodeDisplay] = input.reportCode.display.trim();
30
+ if (input.reportCode.text?.trim())
31
+ claims[DiagnosticReportClaim.CodeText] = input.reportCode.text.trim();
32
+ if (input.encounterReference?.trim()) {
33
+ claims[DiagnosticReportClaim.Encounter] = required(input.encounterReference, 'diagnostic_report_encounter_invalid');
34
+ }
35
+ if (input.performerReferences?.length) {
36
+ claims[DiagnosticReportClaim.Performer] = referenceList(input.performerReferences, 'diagnostic_report_performer_invalid');
37
+ }
38
+ const results = [...(input.results ?? [])];
39
+ if (results.some(entry => entry.resourceType !== ResourceTypesFhirR4.Observation)) {
40
+ throw new TypeError('diagnostic_report_result_invalid');
41
+ }
42
+ const attachments = [...(input.attachments ?? [])];
43
+ if (attachments.some(entry => entry.resourceType !== ResourceTypesFhirR4.DocumentReference)) {
44
+ throw new TypeError('diagnostic_report_attachment_invalid');
45
+ }
46
+ if (results.length)
47
+ claims[DiagnosticReportClaim.Result] = results.map(entry => entry.reference).join(',');
48
+ const report = buildFlatClaimResourceEntry({
49
+ entryId: input.entryId,
50
+ resourceType: ResourceTypesFhirR4.DiagnosticReport,
51
+ claims,
52
+ });
53
+ const children = [...results, ...attachments];
54
+ const graph = [report, ...children];
55
+ return children.length
56
+ ? linkFlatClaimResourceEntries(graph, {
57
+ parentEntryId: report.entryId,
58
+ childEntryIds: children.map(entry => entry.entryId),
59
+ })
60
+ : graph;
61
+ }
62
+ function codingToken(value) {
63
+ const system = required(value.system, 'diagnostic_report_code_system_required');
64
+ const code = required(value.code, 'diagnostic_report_code_required');
65
+ if (system.includes('|') || code.includes('|') || system.includes(',') || code.includes(',')) {
66
+ throw new TypeError('diagnostic_report_code_invalid');
67
+ }
68
+ return `${system}|${code}`;
69
+ }
70
+ function referenceList(values, error) {
71
+ const normalized = [...new Set(values.map(value => required(value, error)))];
72
+ if (normalized.some(value => value.includes(',')))
73
+ throw new TypeError(error);
74
+ return normalized.join(',');
75
+ }
76
+ function isoDateTime(value, error) {
77
+ const normalized = required(value, error);
78
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(normalized) || Number.isNaN(Date.parse(normalized)))
79
+ throw new TypeError(error);
80
+ return normalized;
81
+ }
82
+ function required(value, error) {
83
+ if (typeof value !== 'string' || !value.trim())
84
+ throw new TypeError(error);
85
+ return value.trim();
86
+ }
@@ -1,2 +1,3 @@
1
+ export * from './diagnostic-report-graph.js';
1
2
  export * from './immunization-graph.js';
2
3
  export * from './immunization-reminders.js';
@@ -1,2 +1,3 @@
1
+ export * from './diagnostic-report-graph.js';
1
2
  export * from './immunization-graph.js';
2
3
  export * from './immunization-reminders.js';
@@ -8,8 +8,20 @@ export declare function getDiagnosticReportIdentifier(claims: DiagnosticReportIn
8
8
  export declare function setDiagnosticReportIdentifier(claims: DiagnosticReportInteroperableClaims, value: string): DiagnosticReportInteroperableClaims;
9
9
  export declare function getDiagnosticReportResultList(claims: DiagnosticReportInteroperableClaims): string[];
10
10
  export declare function setDiagnosticReportResultList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
11
+ export declare function addDiagnosticReportResultList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
12
+ export declare function removeDiagnosticReportResultList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
13
+ export declare function getDiagnosticReportPerformerList(claims: DiagnosticReportInteroperableClaims): string[];
14
+ export declare function setDiagnosticReportPerformerList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
15
+ export declare function addDiagnosticReportPerformerList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
16
+ export declare function removeDiagnosticReportPerformerList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
17
+ export declare function getDiagnosticReportSpecimenList(claims: DiagnosticReportInteroperableClaims): string[];
18
+ export declare function setDiagnosticReportSpecimenList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
19
+ export declare function addDiagnosticReportSpecimenList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
20
+ export declare function removeDiagnosticReportSpecimenList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
11
21
  export declare function getDiagnosticReportContainedResourceReferenceList(claims: DiagnosticReportInteroperableClaims): string[];
12
22
  export declare function setDiagnosticReportContainedResourceReferenceList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
23
+ export declare function addDiagnosticReportContainedResourceReferenceList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
24
+ export declare function removeDiagnosticReportContainedResourceReferenceList(claims: DiagnosticReportInteroperableClaims, values: string | readonly string[]): DiagnosticReportInteroperableClaims;
13
25
  /** @deprecated Use `getDiagnosticReportContainedResourceReferenceList`. */
14
26
  export declare function getDiagnosticReportContainedDocumentIdentifierList(claims: DiagnosticReportInteroperableClaims): string[];
15
27
  /** @deprecated Use `setDiagnosticReportContainedResourceReferenceList`. */
@@ -24,6 +24,36 @@ export function getDiagnosticReportResultList(claims) {
24
24
  export function setDiagnosticReportResultList(claims, values) {
25
25
  return setDiagnosticReportClaimList(claims, DiagnosticReportClaim.Result, values);
26
26
  }
27
+ export function addDiagnosticReportResultList(claims, values) {
28
+ return addDiagnosticReportClaimList(claims, DiagnosticReportClaim.Result, values);
29
+ }
30
+ export function removeDiagnosticReportResultList(claims, values) {
31
+ return removeDiagnosticReportClaimList(claims, DiagnosticReportClaim.Result, values);
32
+ }
33
+ export function getDiagnosticReportPerformerList(claims) {
34
+ return getDiagnosticReportClaimList(claims, DiagnosticReportClaim.Performer);
35
+ }
36
+ export function setDiagnosticReportPerformerList(claims, values) {
37
+ return setDiagnosticReportClaimList(claims, DiagnosticReportClaim.Performer, values);
38
+ }
39
+ export function addDiagnosticReportPerformerList(claims, values) {
40
+ return addDiagnosticReportClaimList(claims, DiagnosticReportClaim.Performer, values);
41
+ }
42
+ export function removeDiagnosticReportPerformerList(claims, values) {
43
+ return removeDiagnosticReportClaimList(claims, DiagnosticReportClaim.Performer, values);
44
+ }
45
+ export function getDiagnosticReportSpecimenList(claims) {
46
+ return getDiagnosticReportClaimList(claims, DiagnosticReportClaim.Specimen);
47
+ }
48
+ export function setDiagnosticReportSpecimenList(claims, values) {
49
+ return setDiagnosticReportClaimList(claims, DiagnosticReportClaim.Specimen, values);
50
+ }
51
+ export function addDiagnosticReportSpecimenList(claims, values) {
52
+ return addDiagnosticReportClaimList(claims, DiagnosticReportClaim.Specimen, values);
53
+ }
54
+ export function removeDiagnosticReportSpecimenList(claims, values) {
55
+ return removeDiagnosticReportClaimList(claims, DiagnosticReportClaim.Specimen, values);
56
+ }
27
57
  export function getDiagnosticReportContainedResourceReferenceList(claims) {
28
58
  const canonical = getDiagnosticReportClaimList(claims, DiagnosticReportClaim.ContainedReferenceList);
29
59
  const legacy = getDiagnosticReportClaimList(claims, DiagnosticReportClaim.ContainedDocuments);
@@ -37,6 +67,16 @@ export function setDiagnosticReportContainedResourceReferenceList(claims, values
37
67
  delete cleaned[DiagnosticReportClaim.ContainedDocuments];
38
68
  return cleaned;
39
69
  }
70
+ export function addDiagnosticReportContainedResourceReferenceList(claims, values) {
71
+ return setDiagnosticReportContainedResourceReferenceList(claims, [
72
+ ...getDiagnosticReportContainedResourceReferenceList(claims),
73
+ ...(Array.isArray(values) ? values : [values]),
74
+ ]);
75
+ }
76
+ export function removeDiagnosticReportContainedResourceReferenceList(claims, values) {
77
+ const removals = new Set(Array.isArray(values) ? values : [values]);
78
+ return setDiagnosticReportContainedResourceReferenceList(claims, getDiagnosticReportContainedResourceReferenceList(claims).filter(value => !removals.has(value)));
79
+ }
40
80
  /** @deprecated Use `getDiagnosticReportContainedResourceReferenceList`. */
41
81
  export function getDiagnosticReportContainedDocumentIdentifierList(claims) {
42
82
  return getDiagnosticReportContainedResourceReferenceList(claims);
@@ -33,31 +33,6 @@ export declare const DiagnosticReportClaim: {
33
33
  */
34
34
  readonly ContainedDocuments: "DiagnosticReport.contained-documents";
35
35
  };
36
- /**
37
- * TODO(ips-next):
38
- * Add resource-level claim helpers in `src/utils/claims-helpers-diagnostic-report.ts`
39
- * following the same contract already used by Consent/Medication/Condition/Allergy:
40
- * - `getDiagnosticReportClaimList(...)`
41
- * - `setDiagnosticReportClaimList(...)`
42
- * - `addDiagnosticReportClaimList(...)`
43
- * - `removeDiagnosticReportClaimList(...)`
44
- * - focused helpers for:
45
- * - `get/set/add/removeDiagnosticReportResultList`
46
- * - `get/set/add/removeDiagnosticReportPerformerList`
47
- * - `get/set/add/removeDiagnosticReportSpecimenList`
48
- * - `get/set/add/removeDiagnosticReportContainedResourceReferences`
49
- * - `get/setDiagnosticReportPresentedFormContentType`
50
- * - `get/setDiagnosticReportPresentedFormData`
51
- * - `get/setDiagnosticReportPresentedFormUrl`
52
- *
53
- * Keep naming aligned with canonical FHIR search parameter names
54
- * (lowercase with `-`) and reuse the existing `SearchParamNames` + contextualized
55
- * claim-key mapping pattern.
56
- *
57
- * This file is intentionally type-first only for now so GW Core and
58
- * `common-utils` can already exercise the rest of the bundle workflow without
59
- * committing to the DiagnosticReport editing surface yet.
60
- */
61
36
  export type DiagnosticReportClaimKey = typeof DiagnosticReportClaim[keyof typeof DiagnosticReportClaim];
62
37
  export declare const DiagnosticReportClaimSpecs: ClaimSpec[];
63
38
  export declare const DiagnosticReportSearchParamNames: {
@@ -95,15 +70,7 @@ export declare enum DiagnosticReportClaimsFhirApi {
95
70
  }
96
71
  export declare const DiagnosticReportSearchParamToClaimKey: Record<DiagnosticReportSearchParamName, DiagnosticReportClaimsFhirApi>;
97
72
  /**
98
- * TODO(ips-next):
99
- * Add `DiagnosticReportClaimsFhirApiExtended` only if/when the bundle editor
100
- * needs operational flat fields beyond the official search parameter surface.
101
- *
102
- * Do not invent camelCase names. Prefer official FHIR search parameter names
103
- * or explicit attachment aliases such as `presented-form-*` and
104
- * `contained-reference-list`.
105
- *
106
- * Keep `DiagnosticReport` paused at the typing/mapping layer until:
107
- * 1. `claims-helpers-diagnostic-report.ts` exists
108
- * 2. GW Core readers/tests consume the same shared claim keys
73
+ * Operational attachment and graph claims remain separate from the official
74
+ * SearchParameter catalog above. They support claims-first authoring without
75
+ * pretending that those aliases are searchable FHIR parameters.
109
76
  */
@@ -54,7 +54,7 @@ export const DiagnosticReportClaimSpecs = [
54
54
  { key: DiagnosticReportClaim.PresentedFormContentType, meaning: 'FHIR presentedForm attachment content type.', example: 'application/pdf' },
55
55
  { key: DiagnosticReportClaim.PresentedFormData, meaning: 'FHIR presentedForm inline base64 payload.', example: 'JVBERi0xLjc...' },
56
56
  { key: DiagnosticReportClaim.PresentedFormUrl, meaning: 'FHIR presentedForm external URL.', example: 'https://example.org/report.pdf' },
57
- { key: DiagnosticReportClaim.ContainedDocuments, meaning: 'CSV of linked DocumentReference identifiers stored in the same bundle.', example: 'docref-example-001,docref-example-002' },
57
+ { key: DiagnosticReportClaim.ContainedReferenceList, meaning: 'Canonical CSV of independent result and evidence resource references linked by this report.', example: 'Observation/result-1,DocumentReference/report-1' },
58
58
  ];
59
59
  export const DiagnosticReportSearchParamNames = {
60
60
  BasedOn: 'based-on',
@@ -106,15 +106,7 @@ export const DiagnosticReportSearchParamToClaimKey = {
106
106
  [DiagnosticReportSearchParamNames.Subject]: DiagnosticReportClaimsFhirApi.Subject,
107
107
  };
108
108
  /**
109
- * TODO(ips-next):
110
- * Add `DiagnosticReportClaimsFhirApiExtended` only if/when the bundle editor
111
- * needs operational flat fields beyond the official search parameter surface.
112
- *
113
- * Do not invent camelCase names. Prefer official FHIR search parameter names
114
- * or explicit attachment aliases such as `presented-form-*` and
115
- * `contained-reference-list`.
116
- *
117
- * Keep `DiagnosticReport` paused at the typing/mapping layer until:
118
- * 1. `claims-helpers-diagnostic-report.ts` exists
119
- * 2. GW Core readers/tests consume the same shared claim keys
109
+ * Operational attachment and graph claims remain separate from the official
110
+ * SearchParameter catalog above. They support claims-first authoring without
111
+ * pretending that those aliases are searchable FHIR parameters.
120
112
  */
@@ -70,16 +70,16 @@ export function normalizeLegacyContainedClaims(entry) {
70
70
  */
71
71
  export function buildDocumentReferenceFlatEntry(input) {
72
72
  const claims = {
73
- 'DocumentReference.identifier': requiredToken(input.identifier, 'document_reference_identifier_required'),
74
- 'DocumentReference.content-type': input.contentType,
75
- 'DocumentReference.contenthash': requiredToken(input.contentHash, 'document_reference_contenthash_required'),
73
+ [DocumentReferenceClaim.Identifier]: requiredToken(input.identifier, 'document_reference_identifier_required'),
74
+ [DocumentReferenceClaim.ContentType]: input.contentType,
75
+ [DocumentReferenceClaim.ContentHash]: requiredToken(input.contentHash, 'document_reference_contenthash_required'),
76
76
  };
77
77
  if (input.title?.trim())
78
- claims['DocumentReference.title'] = input.title.trim();
78
+ claims[DocumentReferenceClaim.Description] = input.title.trim();
79
79
  if (input.url?.trim())
80
- claims['DocumentReference.url'] = input.url.trim();
80
+ claims[DocumentReferenceClaim.Location] = input.url.trim();
81
81
  if (input.dataBase64?.trim())
82
- claims['DocumentReference.data'] = input.dataBase64.trim();
82
+ claims[DocumentReferenceClaim.ContentData] = input.dataBase64.trim();
83
83
  return buildFlatClaimResourceEntry({ entryId: input.entryId, resourceType: 'DocumentReference', claims });
84
84
  }
85
85
  function splitCsv(value) {
@@ -106,3 +106,4 @@ function requiredResourceType(value) {
106
106
  throw new TypeError('flat_claim_resource_type_invalid');
107
107
  return normalized;
108
108
  }
109
+ import { DocumentReferenceClaim } from './claims/document-reference-claims.js';
@@ -83,6 +83,13 @@ one native `Observation.component[]`; it does not export three independent
83
83
  native Observations. Component entries do not use `is-contained`,
84
84
  `contained-parent-reference` or `contained-reference-list`.
85
85
 
86
+ Diagnostic laboratory authoring is owned here by
87
+ `buildDiagnosticReportFlatGraph(...)`: `DiagnosticReport.code` identifies the
88
+ report/panel, independent Observation entries carry atomic results, and
89
+ independent DocumentReference entries carry PDF/image evidence. Sector roles,
90
+ confirmation, ledger eligibility, consent and research policy remain outside
91
+ this package.
92
+
86
93
  Aligned JSON arrays and the older comma-separated `Observation.component-*`
87
94
  claims are accepted only by the migration reader. No current writer emits
88
95
  either representation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhir-data-utils-ts",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "FHIR-version-neutral flat-claim and SearchParameter data utilities",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",