fhir-data-utils-ts 0.3.3 → 0.3.5

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 (32) hide show
  1. package/.codex/skills/govern-fhir-data-utilities/SKILL.md +9 -0
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +26 -0
  4. package/dist/claim-helpers/claims-helpers-encounter.d.ts +2 -0
  5. package/dist/claim-helpers/claims-helpers-encounter.js +6 -0
  6. package/dist/claim-helpers/claims-helpers-imaging-study.d.ts +8 -0
  7. package/dist/claim-helpers/claims-helpers-imaging-study.js +20 -0
  8. package/dist/claim-helpers/index.d.ts +1 -0
  9. package/dist/claim-helpers/index.js +1 -0
  10. package/dist/claims/communication-claims.d.ts +4 -0
  11. package/dist/claims/communication-claims.js +4 -0
  12. package/dist/claims/encounter-claims.d.ts +13 -0
  13. package/dist/claims/encounter-claims.js +15 -0
  14. package/dist/claims/imaging-study-claims.d.ts +25 -0
  15. package/dist/claims/imaging-study-claims.js +43 -0
  16. package/dist/claims/index.d.ts +1 -0
  17. package/dist/claims/index.js +1 -0
  18. package/dist/constants/fhir-code-systems.d.ts +2 -0
  19. package/dist/constants/fhir-code-systems.js +2 -0
  20. package/dist/constants/fhir-resource-types.d.ts +2 -0
  21. package/dist/constants/fhir-resource-types.js +2 -0
  22. package/dist/converters/convert-encounter.js +5 -1
  23. package/dist/converters/convert-imaging-study.d.ts +4 -0
  24. package/dist/converters/convert-imaging-study.js +82 -0
  25. package/dist/converters/index.d.ts +1 -0
  26. package/dist/converters/index.js +1 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/subscription-r5.d.ts +150 -0
  30. package/dist/subscription-r5.js +202 -0
  31. package/docs/UHC_FHIR_UTILS_MIGRATION.md +18 -0
  32. package/package.json +5 -1
@@ -29,6 +29,15 @@ description: Complete or audit migrations, claim catalogs, converters, builders,
29
29
  projection. Do not invent resource-specific scalar aliases for indexing.
30
30
  - Keep Group membership and ResearchStudy associated parties descriptive;
31
31
  neither is a Consent, SMART scope or authorization grant.
32
+ - Keep native R5 SubscriptionTopic/Subscription configuration and notification
33
+ Bundle parsing separate from the claims-first Communication being delivered.
34
+ This package owns their reusable types and validation, never HTTP delivery,
35
+ subscriber persistence, authentication or product notification policy.
36
+ - Keep ImagingStudy searchable metadata, clinical references and protected
37
+ endpoint references in neutral flat claims. DICOM parsing, pixels and object
38
+ storage remain in a separately versioned imaging/storage package.
39
+ - Preserve `Encounter.appointment`; an Appointment lifecycle transition must
40
+ not sever the encounter from the scheduling event that originated it.
32
41
 
33
42
  ## Change with executable contracts
34
43
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.5 - 2026-09-26
4
+
5
+ - Preserve the originating `Encounter.appointment` reference and expose the
6
+ canonical FHIR R4 Encounter lifecycle vocabulary.
7
+ - Add product-neutral ImagingStudy flat claims, helpers and R4 projection for
8
+ searchable clinical metadata and protected endpoint references without
9
+ embedding DICOM payloads.
10
+ - Cover Encounter and ImagingStudy with rich round-trip, catalog, helper,
11
+ documentation and package-surface contracts.
12
+
13
+ ## 0.3.4 - 2026-09-23
14
+
15
+ - Add native FHIR R5 `SubscriptionTopic` and HTTPS rest-hook `Subscription`
16
+ builders plus the official resource-specific SearchParameter catalogs.
17
+ - Add a strict `subscription-notification` Bundle parser that resolves each
18
+ advertised full-resource event focus and distinguishes handshake events.
19
+ - Add the claims-first terminal-job `Communication` contract used by DataConv
20
+ and professional BFFs without introducing transport, storage or product
21
+ policy into the neutral package.
22
+
3
23
  ## 0.3.3 - 2026-09-23
4
24
 
5
25
  - Define product-neutral coding-review families and pending-proposal helpers.
package/README.md CHANGED
@@ -42,6 +42,9 @@ The root export and stable subpaths provide:
42
42
  RelatedPerson/telecom helpers and type-only DICOMDIR tree contract;
43
43
  - cross-sector terminology, Immunization graphs/reminders, and FHIR R5 Group
44
44
  and ResearchStudy contracts previously located only in SOS or Vet packages.
45
+ - native FHIR R5 SubscriptionTopic/Subscription configuration builders,
46
+ official search catalogs and a strict notification-Bundle parser; terminal
47
+ asynchronous job messages remain claims-first Communication resources.
45
48
 
46
49
  Claim catalogs and graph builders are deliberately different surfaces.
47
50
  `fhir-data-utils-ts/claims` owns canonical storage names and their specs;
@@ -50,6 +53,13 @@ those catalogs. The old `/immunization-flat-claims` and
50
53
  `/immunization-reminder-plan` paths are deprecated compatibility re-exports;
51
54
  new code imports `/builders` or `/builders/immunization`.
52
55
 
56
+ Encounter projection preserves the originating `Appointment` reference and
57
+ uses the canonical R4 lifecycle (`in-progress` while care is underway,
58
+ `finished` when it ends). ImagingStudy projection stores searchable study
59
+ metadata and protected endpoint references only. DICOM instances and pixel
60
+ payloads remain outside this neutral package and are never embedded as flat
61
+ claims.
62
+
53
63
  No consumer should copy claim paths or rebuild these structures with literals.
54
64
  Use `fhir-data-utils-ts/claims`, `/converters`, `/fhir-bundle`,
55
65
  `/fhir-search-parameters`, `/clinical-terminology`, `/group-r5` or
@@ -111,6 +121,22 @@ The package exports builders, traversal/confirmation helpers and
111
121
  `CODING_REVIEW_EXAMPLES`. Tests and consumers reuse them instead of duplicating
112
122
  claim strings, terminology identifiers, codes or UUIDs.
113
123
 
124
+ ## R5 asynchronous notifications
125
+
126
+ `fhir-data-utils-ts/subscription-r5` separates three boundaries:
127
+
128
+ - `SubscriptionTopic` and `Subscription` are native R5 configuration resources;
129
+ - the job-completion `Communication` is canonical flat-claim business data;
130
+ - `Bundle[type=subscription-notification]` is parsed only at the receiving
131
+ rest-hook boundary.
132
+
133
+ The shared package owns types, builders, official SearchParameter metadata and
134
+ validation. DataConv owns emitting the terminal Communication, GW owns durable
135
+ subscription delivery, and each BFF owns authenticated inbox persistence and
136
+ browser/email/push fan-out. Tenant-level Communication subscriptions initially
137
+ use no server-side filter; the receiving BFF routes the minimal full resource by
138
+ its canonical `Communication.recipient` claim.
139
+
114
140
  ## Verification gate
115
141
 
116
142
  `npm run check` runs strict type checking, all contract tests and built-in
@@ -8,3 +8,5 @@ export declare function getEncounterIdentifier(claims: EncounterInteroperableCla
8
8
  export declare function setEncounterIdentifier(claims: EncounterInteroperableClaims, value: string): EncounterInteroperableClaims;
9
9
  export declare function getEncounterParticipantList(claims: EncounterInteroperableClaims): string[];
10
10
  export declare function setEncounterParticipantList(claims: EncounterInteroperableClaims, values: string | readonly string[]): EncounterInteroperableClaims;
11
+ export declare function getEncounterAppointmentList(claims: EncounterInteroperableClaims): string[];
12
+ export declare function setEncounterAppointmentList(claims: EncounterInteroperableClaims, values: string | readonly string[]): EncounterInteroperableClaims;
@@ -24,3 +24,9 @@ export function getEncounterParticipantList(claims) {
24
24
  export function setEncounterParticipantList(claims, values) {
25
25
  return setEncounterClaimList(claims, EncounterClaim.Participant, values);
26
26
  }
27
+ export function getEncounterAppointmentList(claims) {
28
+ return getEncounterClaimList(claims, EncounterClaim.Appointment);
29
+ }
30
+ export function setEncounterAppointmentList(claims, values) {
31
+ return setEncounterClaimList(claims, EncounterClaim.Appointment, values);
32
+ }
@@ -0,0 +1,8 @@
1
+ import { type GenericInteroperableClaims } from './claim-list-helpers.js';
2
+ export type ImagingStudyInteroperableClaims = GenericInteroperableClaims;
3
+ export declare function getImagingStudyClaimList(claims: ImagingStudyInteroperableClaims, claimKey: string): string[];
4
+ export declare function setImagingStudyClaimList(claims: ImagingStudyInteroperableClaims, claimKey: string, values: string | readonly string[]): ImagingStudyInteroperableClaims;
5
+ export declare function addImagingStudyClaimList(claims: ImagingStudyInteroperableClaims, claimKey: string, values: string | readonly string[]): ImagingStudyInteroperableClaims;
6
+ export declare function removeImagingStudyClaimList(claims: ImagingStudyInteroperableClaims, claimKey: string, values: string | readonly string[]): ImagingStudyInteroperableClaims;
7
+ export declare function getImagingStudyIdentifier(claims: ImagingStudyInteroperableClaims): string;
8
+ export declare function setImagingStudyIdentifier(claims: ImagingStudyInteroperableClaims, value: string): ImagingStudyInteroperableClaims;
@@ -0,0 +1,20 @@
1
+ import { ImagingStudyClaim } from '../claims/imaging-study-claims.js';
2
+ import { addClaimValues, getClaimValues, normalizeClaimScalar, removeClaimValues, setClaimValues, } from './claim-list-helpers.js';
3
+ export function getImagingStudyClaimList(claims, claimKey) {
4
+ return getClaimValues(claims, claimKey);
5
+ }
6
+ export function setImagingStudyClaimList(claims, claimKey, values) {
7
+ return setClaimValues(claims, claimKey, values);
8
+ }
9
+ export function addImagingStudyClaimList(claims, claimKey, values) {
10
+ return addClaimValues(claims, claimKey, values);
11
+ }
12
+ export function removeImagingStudyClaimList(claims, claimKey, values) {
13
+ return removeClaimValues(claims, claimKey, values);
14
+ }
15
+ export function getImagingStudyIdentifier(claims) {
16
+ return normalizeClaimScalar(claims[ImagingStudyClaim.Identifier]);
17
+ }
18
+ export function setImagingStudyIdentifier(claims, value) {
19
+ return { ...claims, [ImagingStudyClaim.Identifier]: normalizeClaimScalar(value) };
20
+ }
@@ -15,6 +15,7 @@ export * from './claims-helpers-document-reference.js';
15
15
  export * from './claims-helpers-encounter.js';
16
16
  export * from './claims-helpers-flag.js';
17
17
  export * from './claims-helpers-immunization.js';
18
+ export * from './claims-helpers-imaging-study.js';
18
19
  export * from './claims-helpers-location.js';
19
20
  export * from './claims-helpers-medication-statement.js';
20
21
  export * from './claims-helpers-observation.js';
@@ -15,6 +15,7 @@ export * from './claims-helpers-document-reference.js';
15
15
  export * from './claims-helpers-encounter.js';
16
16
  export * from './claims-helpers-flag.js';
17
17
  export * from './claims-helpers-immunization.js';
18
+ export * from './claims-helpers-imaging-study.js';
18
19
  export * from './claims-helpers-location.js';
19
20
  export * from './claims-helpers-medication-statement.js';
20
21
  export * from './claims-helpers-observation.js';
@@ -15,6 +15,8 @@ export declare const CommunicationClaim: {
15
15
  readonly Status: "Communication.status";
16
16
  /** Stable communication identifier. */
17
17
  readonly Identifier: "Communication.identifier";
18
+ /** Request or Task that initiated this Communication. */
19
+ readonly BasedOn: "Communication.based-on";
18
20
  /** Subject identifier (typically `did:web`). */
19
21
  readonly Subject: "Communication.subject";
20
22
  /** Recipient identifier(s); CSV allowed. */
@@ -27,6 +29,8 @@ export declare const CommunicationClaim: {
27
29
  readonly NoteText: "Communication.note-text";
28
30
  /** Communication topic code (`system|code`). */
29
31
  readonly Topic: "Communication.topic";
32
+ /** Resource references that provide the Communication context; CSV allowed. */
33
+ readonly About: "Communication.about";
30
34
  /** Narrative text (short summary or body). */
31
35
  readonly Text: "Communication.text";
32
36
  /** Related resource references; CSV allowed. */
@@ -18,6 +18,8 @@ export const CommunicationClaim = {
18
18
  Status: 'Communication.status',
19
19
  /** Stable communication identifier. */
20
20
  Identifier: 'Communication.identifier',
21
+ /** Request or Task that initiated this Communication. */
22
+ BasedOn: 'Communication.based-on',
21
23
  /** Subject identifier (typically `did:web`). */
22
24
  Subject: 'Communication.subject',
23
25
  /** Recipient identifier(s); CSV allowed. */
@@ -30,6 +32,8 @@ export const CommunicationClaim = {
30
32
  NoteText: 'Communication.note-text',
31
33
  /** Communication topic code (`system|code`). */
32
34
  Topic: 'Communication.topic',
35
+ /** Resource references that provide the Communication context; CSV allowed. */
36
+ About: 'Communication.about',
33
37
  /** Narrative text (short summary or body). */
34
38
  Text: 'Communication.text',
35
39
  /** Related resource references; CSV allowed. */
@@ -1,6 +1,7 @@
1
1
  import type { ClaimSpec } from './claim-types.js';
2
2
  export declare const EncounterClaim: {
3
3
  readonly Identifier: "Encounter.identifier";
4
+ readonly Appointment: "Encounter.appointment";
4
5
  readonly Status: "Encounter.status";
5
6
  readonly Class: "Encounter.class";
6
7
  readonly Type: "Encounter.type";
@@ -14,5 +15,17 @@ export declare const EncounterClaim: {
14
15
  readonly Diagnosis: "Encounter.diagnosis";
15
16
  readonly Location: "Encounter.location";
16
17
  };
18
+ /** Canonical FHIR R4 Encounter lifecycle codes. */
19
+ export declare enum EncounterStatus {
20
+ Planned = "planned",
21
+ Arrived = "arrived",
22
+ Triaged = "triaged",
23
+ InProgress = "in-progress",
24
+ OnLeave = "onleave",
25
+ Finished = "finished",
26
+ Cancelled = "cancelled",
27
+ EnteredInError = "entered-in-error",
28
+ Unknown = "unknown"
29
+ }
17
30
  export type EncounterClaimKey = typeof EncounterClaim[keyof typeof EncounterClaim];
18
31
  export declare const EncounterClaimSpecs: ClaimSpec[];
@@ -2,6 +2,7 @@
2
2
  // File: src/models/interoperable-claims/encounter-claims.ts
3
3
  export const EncounterClaim = {
4
4
  Identifier: 'Encounter.identifier',
5
+ Appointment: 'Encounter.appointment',
5
6
  Status: 'Encounter.status',
6
7
  Class: 'Encounter.class',
7
8
  Type: 'Encounter.type',
@@ -15,8 +16,22 @@ export const EncounterClaim = {
15
16
  Diagnosis: 'Encounter.diagnosis',
16
17
  Location: 'Encounter.location',
17
18
  };
19
+ /** Canonical FHIR R4 Encounter lifecycle codes. */
20
+ export var EncounterStatus;
21
+ (function (EncounterStatus) {
22
+ EncounterStatus["Planned"] = "planned";
23
+ EncounterStatus["Arrived"] = "arrived";
24
+ EncounterStatus["Triaged"] = "triaged";
25
+ EncounterStatus["InProgress"] = "in-progress";
26
+ EncounterStatus["OnLeave"] = "onleave";
27
+ EncounterStatus["Finished"] = "finished";
28
+ EncounterStatus["Cancelled"] = "cancelled";
29
+ EncounterStatus["EnteredInError"] = "entered-in-error";
30
+ EncounterStatus["Unknown"] = "unknown";
31
+ })(EncounterStatus || (EncounterStatus = {}));
18
32
  export const EncounterClaimSpecs = [
19
33
  { key: EncounterClaim.Identifier, meaning: 'Business identifier.', example: 'enc-001' },
34
+ { key: EncounterClaim.Appointment, meaning: 'Originating appointment references (CSV).', example: 'Appointment/appointment-1' },
20
35
  { key: EncounterClaim.Status, meaning: 'Encounter status.', example: 'finished' },
21
36
  { key: EncounterClaim.Class, meaning: 'Encounter class token.', example: 'http://terminology.hl7.org/CodeSystem/v3-ActCode|AMB' },
22
37
  { key: EncounterClaim.Type, meaning: 'Encounter type token.', example: 'http://snomed.info/sct|185349003' },
@@ -0,0 +1,25 @@
1
+ import type { ClaimSpec } from './claim-types.js';
2
+ /** Product-neutral flat claims for searchable FHIR R4 ImagingStudy metadata. */
3
+ export declare const ImagingStudyClaim: {
4
+ readonly Identifier: "ImagingStudy.identifier";
5
+ readonly Status: "ImagingStudy.status";
6
+ readonly Modality: "ImagingStudy.modality";
7
+ readonly Subject: "ImagingStudy.subject";
8
+ readonly Patient: "ImagingStudy.patient";
9
+ readonly Encounter: "ImagingStudy.encounter";
10
+ readonly Started: "ImagingStudy.started";
11
+ readonly BasedOn: "ImagingStudy.based-on";
12
+ readonly Referrer: "ImagingStudy.referrer";
13
+ readonly Interpreter: "ImagingStudy.interpreter";
14
+ readonly Endpoint: "ImagingStudy.endpoint";
15
+ readonly ProcedureReference: "ImagingStudy.procedure-reference";
16
+ readonly ProcedureCode: "ImagingStudy.procedure-code";
17
+ readonly ReasonCode: "ImagingStudy.reason-code";
18
+ readonly ReasonReference: "ImagingStudy.reason-reference";
19
+ readonly Location: "ImagingStudy.location";
20
+ readonly NumberOfSeries: "ImagingStudy.number-of-series";
21
+ readonly NumberOfInstances: "ImagingStudy.number-of-instances";
22
+ readonly Description: "ImagingStudy.description";
23
+ };
24
+ export type ImagingStudyClaimKey = typeof ImagingStudyClaim[keyof typeof ImagingStudyClaim];
25
+ export declare const ImagingStudyClaimSpecs: readonly ClaimSpec[];
@@ -0,0 +1,43 @@
1
+ /** Product-neutral flat claims for searchable FHIR R4 ImagingStudy metadata. */
2
+ export const ImagingStudyClaim = {
3
+ Identifier: 'ImagingStudy.identifier',
4
+ Status: 'ImagingStudy.status',
5
+ Modality: 'ImagingStudy.modality',
6
+ Subject: 'ImagingStudy.subject',
7
+ Patient: 'ImagingStudy.patient',
8
+ Encounter: 'ImagingStudy.encounter',
9
+ Started: 'ImagingStudy.started',
10
+ BasedOn: 'ImagingStudy.based-on',
11
+ Referrer: 'ImagingStudy.referrer',
12
+ Interpreter: 'ImagingStudy.interpreter',
13
+ Endpoint: 'ImagingStudy.endpoint',
14
+ ProcedureReference: 'ImagingStudy.procedure-reference',
15
+ ProcedureCode: 'ImagingStudy.procedure-code',
16
+ ReasonCode: 'ImagingStudy.reason-code',
17
+ ReasonReference: 'ImagingStudy.reason-reference',
18
+ Location: 'ImagingStudy.location',
19
+ NumberOfSeries: 'ImagingStudy.number-of-series',
20
+ NumberOfInstances: 'ImagingStudy.number-of-instances',
21
+ Description: 'ImagingStudy.description',
22
+ };
23
+ export const ImagingStudyClaimSpecs = Object.freeze([
24
+ { key: ImagingStudyClaim.Identifier, meaning: 'Business or DICOM study identifier.', example: 'imaging-study-1' },
25
+ { key: ImagingStudyClaim.Status, meaning: 'ImagingStudy lifecycle status.', example: 'available' },
26
+ { key: ImagingStudyClaim.Modality, meaning: 'Repeatable DICOM modality token.', example: 'http://dicom.nema.org/resources/ontology/DCM|DX' },
27
+ { key: ImagingStudyClaim.Subject, meaning: 'Canonical subject reference.', example: 'did:web:subject.example' },
28
+ { key: ImagingStudyClaim.Patient, meaning: 'FHIR R4 patient search alias.', example: 'did:web:subject.example' },
29
+ { key: ImagingStudyClaim.Encounter, meaning: 'Encounter context reference.', example: 'Encounter/encounter-1' },
30
+ { key: ImagingStudyClaim.Started, meaning: 'Study start instant.', example: '2026-09-26T10:15:00Z' },
31
+ { key: ImagingStudyClaim.BasedOn, meaning: 'Originating request references (CSV).', example: 'ServiceRequest/radiography-1' },
32
+ { key: ImagingStudyClaim.Referrer, meaning: 'Referring practitioner reference.', example: 'PractitionerRole/veterinarian-1' },
33
+ { key: ImagingStudyClaim.Interpreter, meaning: 'Interpreter references (CSV).', example: 'PractitionerRole/radiologist-1' },
34
+ { key: ImagingStudyClaim.Endpoint, meaning: 'Protected imaging endpoint references (CSV).', example: 'Endpoint/dicomweb-1' },
35
+ { key: ImagingStudyClaim.ProcedureReference, meaning: 'Performed procedure reference.', example: 'Procedure/radiography-1' },
36
+ { key: ImagingStudyClaim.ProcedureCode, meaning: 'Performed procedure coding token.', example: 'http://snomed.info/sct|363680008' },
37
+ { key: ImagingStudyClaim.ReasonCode, meaning: 'Study reason coding token.', example: 'http://snomed.info/sct|125605004' },
38
+ { key: ImagingStudyClaim.ReasonReference, meaning: 'Study reason references (CSV).', example: 'Condition/fracture-1' },
39
+ { key: ImagingStudyClaim.Location, meaning: 'Acquisition location reference.', example: 'Location/imaging-1' },
40
+ { key: ImagingStudyClaim.NumberOfSeries, meaning: 'Declared series count.', example: '1' },
41
+ { key: ImagingStudyClaim.NumberOfInstances, meaning: 'Declared instance count.', example: '2' },
42
+ { key: ImagingStudyClaim.Description, meaning: 'Study description.', example: 'Left forelimb radiographs' },
43
+ ]);
@@ -17,6 +17,7 @@ export * from './encounter-claims.js';
17
17
  export * from './flag-claims.js';
18
18
  export * from './immunization-claims.js';
19
19
  export * from './immunization-recommendation-claims.js';
20
+ export * from './imaging-study-claims.js';
20
21
  export * from './invoice-claims.js';
21
22
  export * from './location-claims.js';
22
23
  export * from './medication-statement-claims.js';
@@ -17,6 +17,7 @@ export * from './encounter-claims.js';
17
17
  export * from './flag-claims.js';
18
18
  export * from './immunization-claims.js';
19
19
  export * from './immunization-recommendation-claims.js';
20
+ export * from './imaging-study-claims.js';
20
21
  export * from './invoice-claims.js';
21
22
  export * from './location-claims.js';
22
23
  export * from './medication-statement-claims.js';
@@ -9,6 +9,8 @@ export declare const FhirCodeSystems: Readonly<{
9
9
  readonly SnomedCt: "http://snomed.info/sct";
10
10
  readonly Ucum: "http://unitsofmeasure.org";
11
11
  readonly CommunicationCategory: "http://terminology.hl7.org/CodeSystem/communication-category";
12
+ readonly SubscriptionChannelType: "http://terminology.hl7.org/CodeSystem/subscription-channel-type";
13
+ readonly TaskStatus: "http://hl7.org/fhir/task-status";
12
14
  readonly ActReason: "http://terminology.hl7.org/CodeSystem/v3-ActReason";
13
15
  readonly ObservationCategory: "http://terminology.hl7.org/CodeSystem/observation-category";
14
16
  readonly EventTiming: "http://hl7.org/fhir/event-timing";
@@ -9,6 +9,8 @@ export const FhirCodeSystems = Object.freeze({
9
9
  SnomedCt: 'http://snomed.info/sct',
10
10
  Ucum: 'http://unitsofmeasure.org',
11
11
  CommunicationCategory: 'http://terminology.hl7.org/CodeSystem/communication-category',
12
+ SubscriptionChannelType: 'http://terminology.hl7.org/CodeSystem/subscription-channel-type',
13
+ TaskStatus: 'http://hl7.org/fhir/task-status',
12
14
  ActReason: 'http://terminology.hl7.org/CodeSystem/v3-ActReason',
13
15
  ObservationCategory: 'http://terminology.hl7.org/CodeSystem/observation-category',
14
16
  EventTiming: 'http://hl7.org/fhir/event-timing',
@@ -108,6 +108,8 @@ export declare const FhirResourceTypes: Readonly<{
108
108
  }>;
109
109
  /** FHIR R5-only resources currently emitted by shared subscription contracts. */
110
110
  export declare const ResourceTypesFhirR5: Readonly<{
111
+ readonly Subscription: "Subscription";
112
+ readonly SubscriptionTopic: "SubscriptionTopic";
111
113
  readonly SubscriptionStatus: "SubscriptionStatus";
112
114
  readonly AdverseEvent: "AdverseEvent";
113
115
  readonly AllergyIntolerance: "AllergyIntolerance";
@@ -61,6 +61,8 @@ export const FhirResourceTypes = ResourceTypesFhirR4;
61
61
  /** FHIR R5-only resources currently emitted by shared subscription contracts. */
62
62
  export const ResourceTypesFhirR5 = Object.freeze({
63
63
  ...ResourceTypesFhirR4,
64
+ Subscription: 'Subscription',
65
+ SubscriptionTopic: 'SubscriptionTopic',
64
66
  SubscriptionStatus: 'SubscriptionStatus',
65
67
  });
66
68
  /** FHIR complex datatypes used as discriminators inside claims-first payloads. */
@@ -1,12 +1,15 @@
1
1
  // Copyright 2026 Conéctate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
2
2
  // File: src/utils/convert-encounter.ts
3
3
  import { EncounterClaim } from '../claims/encounter-claims.js';
4
- import { codingFromValue, codingToValue, referenceToValue } from './convert-shared.js';
4
+ import { codingFromValue, codingToValue, referenceListToCsv, referenceToValue } from './convert-shared.js';
5
5
  export function encounterFlatToFhirR4(claims) {
6
6
  const subject = claims[EncounterClaim.Subject] ?? claims[EncounterClaim.Patient];
7
7
  return {
8
8
  resourceType: 'Encounter',
9
9
  identifier: claims[EncounterClaim.Identifier] ? [{ value: claims[EncounterClaim.Identifier] }] : undefined,
10
+ appointment: claims[EncounterClaim.Appointment]
11
+ ? claims[EncounterClaim.Appointment].split(',').map((reference) => ({ reference: reference.trim() }))
12
+ : undefined,
10
13
  status: claims[EncounterClaim.Status],
11
14
  class: claims[EncounterClaim.Class]
12
15
  ? (() => {
@@ -37,6 +40,7 @@ export function encounterFhirR4ToFlat(resource) {
37
40
  const klass = resource.class;
38
41
  return {
39
42
  [EncounterClaim.Identifier]: resource.identifier?.[0]?.value,
43
+ [EncounterClaim.Appointment]: referenceListToCsv(resource.appointment),
40
44
  [EncounterClaim.Status]: resource.status,
41
45
  [EncounterClaim.Class]: klass?.code ? (klass.system ? `${klass.system}|${klass.code}` : klass.code) : undefined,
42
46
  [EncounterClaim.Type]: codingToValue(resource.type?.[0]?.coding?.[0]),
@@ -0,0 +1,4 @@
1
+ import type { FhirResource, FlatClaims } from './convert-shared.js';
2
+ /** Projects searchable ImagingStudy metadata; DICOM bytes remain in protected imaging storage. */
3
+ export declare function imagingStudyFlatToFhirR4(claims: FlatClaims): FhirResource;
4
+ export declare function imagingStudyFhirR4ToFlat(resource: FhirResource): FlatClaims;
@@ -0,0 +1,82 @@
1
+ import { ImagingStudyClaim } from '../claims/imaging-study-claims.js';
2
+ import { codingFromValue, codingToValue, referenceListToCsv, referenceToValue } from './convert-shared.js';
3
+ function csv(values) {
4
+ if (!Array.isArray(values))
5
+ return undefined;
6
+ const normalized = values.map((value) => String(value || '').trim()).filter(Boolean);
7
+ return normalized.length ? normalized.join(',') : undefined;
8
+ }
9
+ function codingCsv(values) {
10
+ if (!Array.isArray(values))
11
+ return undefined;
12
+ return csv(values.map((value) => codingToValue(value)));
13
+ }
14
+ function codingList(value) {
15
+ if (typeof value !== 'string' || !value.trim())
16
+ return undefined;
17
+ const codings = value.split(',').flatMap((token) => codingFromValue(token.trim()) || []);
18
+ return codings.length ? codings : undefined;
19
+ }
20
+ /** Projects searchable ImagingStudy metadata; DICOM bytes remain in protected imaging storage. */
21
+ export function imagingStudyFlatToFhirR4(claims) {
22
+ const subject = claims[ImagingStudyClaim.Subject] ?? claims[ImagingStudyClaim.Patient];
23
+ return {
24
+ resourceType: 'ImagingStudy',
25
+ identifier: claims[ImagingStudyClaim.Identifier] ? [{ value: claims[ImagingStudyClaim.Identifier] }] : undefined,
26
+ status: claims[ImagingStudyClaim.Status],
27
+ modality: codingList(claims[ImagingStudyClaim.Modality]),
28
+ subject: subject ? { reference: subject } : undefined,
29
+ encounter: claims[ImagingStudyClaim.Encounter] ? { reference: claims[ImagingStudyClaim.Encounter] } : undefined,
30
+ started: claims[ImagingStudyClaim.Started],
31
+ basedOn: references(claims[ImagingStudyClaim.BasedOn]),
32
+ referrer: claims[ImagingStudyClaim.Referrer] ? { reference: claims[ImagingStudyClaim.Referrer] } : undefined,
33
+ interpreter: references(claims[ImagingStudyClaim.Interpreter]),
34
+ endpoint: references(claims[ImagingStudyClaim.Endpoint]),
35
+ procedureReference: claims[ImagingStudyClaim.ProcedureReference] ? { reference: claims[ImagingStudyClaim.ProcedureReference] } : undefined,
36
+ procedureCode: claims[ImagingStudyClaim.ProcedureCode] ? [{ coding: codingFromValue(claims[ImagingStudyClaim.ProcedureCode]) }] : undefined,
37
+ reasonCode: claims[ImagingStudyClaim.ReasonCode] ? [{ coding: codingFromValue(claims[ImagingStudyClaim.ReasonCode]) }] : undefined,
38
+ reasonReference: references(claims[ImagingStudyClaim.ReasonReference]),
39
+ location: claims[ImagingStudyClaim.Location] ? { reference: claims[ImagingStudyClaim.Location] } : undefined,
40
+ numberOfSeries: numberValue(claims[ImagingStudyClaim.NumberOfSeries]),
41
+ numberOfInstances: numberValue(claims[ImagingStudyClaim.NumberOfInstances]),
42
+ description: claims[ImagingStudyClaim.Description],
43
+ };
44
+ }
45
+ export function imagingStudyFhirR4ToFlat(resource) {
46
+ return {
47
+ [ImagingStudyClaim.Identifier]: resource.identifier?.[0]?.value,
48
+ [ImagingStudyClaim.Status]: resource.status,
49
+ [ImagingStudyClaim.Modality]: codingCsv(resource.modality),
50
+ [ImagingStudyClaim.Subject]: referenceToValue(resource.subject),
51
+ [ImagingStudyClaim.Patient]: referenceToValue(resource.subject),
52
+ [ImagingStudyClaim.Encounter]: referenceToValue(resource.encounter),
53
+ [ImagingStudyClaim.Started]: resource.started,
54
+ [ImagingStudyClaim.BasedOn]: referenceListToCsv(resource.basedOn),
55
+ [ImagingStudyClaim.Referrer]: referenceToValue(resource.referrer),
56
+ [ImagingStudyClaim.Interpreter]: referenceListToCsv(resource.interpreter),
57
+ [ImagingStudyClaim.Endpoint]: referenceListToCsv(resource.endpoint),
58
+ [ImagingStudyClaim.ProcedureReference]: referenceToValue(resource.procedureReference),
59
+ [ImagingStudyClaim.ProcedureCode]: codingToValue(resource.procedureCode?.[0]?.coding?.[0]),
60
+ [ImagingStudyClaim.ReasonCode]: codingToValue(resource.reasonCode?.[0]?.coding?.[0]),
61
+ [ImagingStudyClaim.ReasonReference]: referenceListToCsv(resource.reasonReference),
62
+ [ImagingStudyClaim.Location]: referenceToValue(resource.location),
63
+ [ImagingStudyClaim.NumberOfSeries]: scalarNumber(resource.numberOfSeries),
64
+ [ImagingStudyClaim.NumberOfInstances]: scalarNumber(resource.numberOfInstances),
65
+ [ImagingStudyClaim.Description]: resource.description,
66
+ };
67
+ }
68
+ function references(value) {
69
+ if (typeof value !== 'string' || !value.trim())
70
+ return undefined;
71
+ const output = value.split(',').map((reference) => reference.trim()).filter(Boolean).map((reference) => ({ reference }));
72
+ return output.length ? output : undefined;
73
+ }
74
+ function scalarNumber(value) {
75
+ return typeof value === 'number' && Number.isFinite(value) ? String(value) : undefined;
76
+ }
77
+ function numberValue(value) {
78
+ if (typeof value !== 'string' || !value.trim())
79
+ return undefined;
80
+ const number = Number(value);
81
+ return Number.isFinite(number) ? number : undefined;
82
+ }
@@ -15,6 +15,7 @@ export * from './convert-document-reference.js';
15
15
  export * from './convert-encounter.js';
16
16
  export * from './convert-flag.js';
17
17
  export * from './convert-immunization.js';
18
+ export * from './convert-imaging-study.js';
18
19
  export * from './convert-location.js';
19
20
  export * from './convert-medication-statement.js';
20
21
  export * from './convert-observation.js';
@@ -15,6 +15,7 @@ export * from './convert-document-reference.js';
15
15
  export * from './convert-encounter.js';
16
16
  export * from './convert-flag.js';
17
17
  export * from './convert-immunization.js';
18
+ export * from './convert-imaging-study.js';
18
19
  export * from './convert-location.js';
19
20
  export * from './convert-medication-statement.js';
20
21
  export * from './convert-observation.js';
package/dist/index.d.ts CHANGED
@@ -22,3 +22,4 @@ export * from './research-study-r5.js';
22
22
  export * from './fhir-tokens.js';
23
23
  export * from './related-person.js';
24
24
  export * from './imaging-types.js';
25
+ export * from './subscription-r5.js';
package/dist/index.js CHANGED
@@ -22,3 +22,4 @@ export * from './research-study-r5.js';
22
22
  export * from './fhir-tokens.js';
23
23
  export * from './related-person.js';
24
24
  export * from './imaging-types.js';
25
+ export * from './subscription-r5.js';
@@ -0,0 +1,150 @@
1
+ import type { FhirResource } from './fhir-types.js';
2
+ /** FHIR R5 version implemented by this native subscription boundary. */
3
+ export declare const SubscriptionR5Version: "5.0.0";
4
+ export declare const SubscriptionR5ChannelType: Readonly<{
5
+ readonly RestHook: "rest-hook";
6
+ }>;
7
+ export declare const SubscriptionR5Content: Readonly<{
8
+ readonly Empty: "empty";
9
+ readonly IdOnly: "id-only";
10
+ readonly FullResource: "full-resource";
11
+ }>;
12
+ export declare const SubscriptionR5Status: Readonly<{
13
+ readonly Requested: "requested";
14
+ readonly Active: "active";
15
+ readonly Error: "error";
16
+ readonly Off: "off";
17
+ readonly EnteredInError: "entered-in-error";
18
+ }>;
19
+ export declare const SubscriptionTopicR5Status: Readonly<{
20
+ readonly Draft: "draft";
21
+ readonly Active: "active";
22
+ readonly Retired: "retired";
23
+ readonly Unknown: "unknown";
24
+ }>;
25
+ export declare const SubscriptionR5NotificationType: Readonly<{
26
+ readonly Handshake: "handshake";
27
+ readonly Heartbeat: "heartbeat";
28
+ readonly EventNotification: "event-notification";
29
+ readonly QueryStatus: "query-status";
30
+ }>;
31
+ export declare const SubscriptionR5BundleType: Readonly<{
32
+ readonly Notification: "subscription-notification";
33
+ }>;
34
+ export declare const SubscriptionR5MediaType: "application/fhir+json";
35
+ export declare const FhirFlatClaimContext: Readonly<{
36
+ readonly Api: "org.hl7.fhir.api";
37
+ }>;
38
+ export declare const CommunicationStatus: Readonly<{
39
+ readonly Completed: "completed";
40
+ }>;
41
+ export declare const CommunicationCategoryCode: Readonly<{
42
+ readonly Notification: "notification";
43
+ }>;
44
+ export declare const TaskStatusCode: Readonly<{
45
+ readonly Completed: "completed";
46
+ readonly Failed: "failed";
47
+ }>;
48
+ type SearchParameterType = 'date' | 'reference' | 'string' | 'token' | 'uri';
49
+ export type R5SearchParameterDefinition = Readonly<{
50
+ code: string;
51
+ type: SearchParameterType;
52
+ expression: string;
53
+ }>;
54
+ /** Exact Subscription-specific SearchParameters published by FHIR R5 5.0.0. */
55
+ export declare const SubscriptionR5SearchParameterCatalog: readonly R5SearchParameterDefinition[];
56
+ /** Exact SubscriptionTopic-specific SearchParameters published by FHIR R5 5.0.0. */
57
+ export declare const SubscriptionTopicR5SearchParameterCatalog: readonly R5SearchParameterDefinition[];
58
+ export type SubscriptionR5Filter = Readonly<{
59
+ resourceType?: string;
60
+ filterParameter: string;
61
+ comparator?: 'eq' | 'ne';
62
+ modifier?: string;
63
+ value: string;
64
+ }>;
65
+ export type SubscriptionR5Parameter = Readonly<{
66
+ name: string;
67
+ value: string;
68
+ }>;
69
+ export type SubscriptionR5 = Readonly<{
70
+ resourceType: 'Subscription';
71
+ id: string;
72
+ status: (typeof SubscriptionR5Status)[keyof typeof SubscriptionR5Status];
73
+ topic: string;
74
+ reason?: string;
75
+ filterBy?: readonly SubscriptionR5Filter[];
76
+ channelType: Readonly<{
77
+ system: string;
78
+ code: 'rest-hook';
79
+ }>;
80
+ endpoint: string;
81
+ parameter?: readonly SubscriptionR5Parameter[];
82
+ contentType: typeof SubscriptionR5MediaType;
83
+ content: (typeof SubscriptionR5Content)[keyof typeof SubscriptionR5Content];
84
+ }>;
85
+ export type SubscriptionTopicR5 = Readonly<{
86
+ resourceType: 'SubscriptionTopic';
87
+ id: string;
88
+ url: string;
89
+ status: (typeof SubscriptionTopicR5Status)[keyof typeof SubscriptionTopicR5Status];
90
+ resourceTrigger: readonly Readonly<{
91
+ resource: string;
92
+ }>[];
93
+ canFilterBy: readonly Readonly<{
94
+ resource: string;
95
+ filterParameter: string;
96
+ comparator: readonly ('eq' | 'ne')[];
97
+ }>[];
98
+ }>;
99
+ /** Builds the native R5 topic used to notify subscribers about Communication writes. */
100
+ export declare function buildCommunicationSubscriptionTopicR5(input: Readonly<{
101
+ id: string;
102
+ url: string;
103
+ }>): SubscriptionTopicR5;
104
+ /** Builds one native R5 rest-hook Subscription. This resource is configuration, not flat clinical state. */
105
+ export declare function buildRestHookSubscriptionR5(input: Readonly<{
106
+ id: string;
107
+ topic: string;
108
+ endpoint: string;
109
+ filters?: readonly SubscriptionR5Filter[];
110
+ parameters?: readonly SubscriptionR5Parameter[];
111
+ content?: SubscriptionR5['content'];
112
+ reason?: string;
113
+ }>): SubscriptionR5;
114
+ export type TerminalJobStatusCode = (typeof TaskStatusCode)[keyof typeof TaskStatusCode];
115
+ /**
116
+ * Builds the canonical claims-first Communication emitted when an asynchronous
117
+ * Task reaches a terminal state. `Communication.status=completed` describes
118
+ * the sent message; `Communication.content-code` carries the linked Task state.
119
+ */
120
+ export declare function buildTerminalJobCommunication(input: Readonly<{
121
+ id: string;
122
+ threadId: string;
123
+ jobId: string;
124
+ jobStatus: TerminalJobStatusCode;
125
+ recipient: string;
126
+ sender: string;
127
+ researchStudyReference: string;
128
+ sent: string;
129
+ }>): FhirResource & Readonly<{
130
+ resourceType: 'Communication';
131
+ id: string;
132
+ meta: Readonly<{
133
+ claims: Readonly<Record<string, string>>;
134
+ }>;
135
+ }>;
136
+ export type ParsedSubscriptionNotificationEventR5 = Readonly<{
137
+ eventNumber: string;
138
+ timestamp?: string;
139
+ focusReference: string;
140
+ focusResource: FhirResource;
141
+ }>;
142
+ export type ParsedSubscriptionNotificationR5 = Readonly<{
143
+ type: (typeof SubscriptionR5NotificationType)[keyof typeof SubscriptionR5NotificationType];
144
+ subscriptionReference: string;
145
+ topic?: string;
146
+ events: readonly ParsedSubscriptionNotificationEventR5[];
147
+ }>;
148
+ /** Validates a standard R5 notification Bundle and resolves every advertised full-resource focus. */
149
+ export declare function parseSubscriptionNotificationR5(input: unknown): ParsedSubscriptionNotificationR5;
150
+ export {};
@@ -0,0 +1,202 @@
1
+ import { CommunicationClaim } from './claims/communication-claims.js';
2
+ import { FhirCodeSystems } from './constants/fhir-code-systems.js';
3
+ import { ResourceTypesFhirR5 } from './constants/fhir-resource-types.js';
4
+ /** FHIR R5 version implemented by this native subscription boundary. */
5
+ export const SubscriptionR5Version = '5.0.0';
6
+ export const SubscriptionR5ChannelType = Object.freeze({ RestHook: 'rest-hook' });
7
+ export const SubscriptionR5Content = Object.freeze({
8
+ Empty: 'empty',
9
+ IdOnly: 'id-only',
10
+ FullResource: 'full-resource',
11
+ });
12
+ export const SubscriptionR5Status = Object.freeze({
13
+ Requested: 'requested',
14
+ Active: 'active',
15
+ Error: 'error',
16
+ Off: 'off',
17
+ EnteredInError: 'entered-in-error',
18
+ });
19
+ export const SubscriptionTopicR5Status = Object.freeze({
20
+ Draft: 'draft',
21
+ Active: 'active',
22
+ Retired: 'retired',
23
+ Unknown: 'unknown',
24
+ });
25
+ export const SubscriptionR5NotificationType = Object.freeze({
26
+ Handshake: 'handshake',
27
+ Heartbeat: 'heartbeat',
28
+ EventNotification: 'event-notification',
29
+ QueryStatus: 'query-status',
30
+ });
31
+ export const SubscriptionR5BundleType = Object.freeze({ Notification: 'subscription-notification' });
32
+ export const SubscriptionR5MediaType = 'application/fhir+json';
33
+ export const FhirFlatClaimContext = Object.freeze({ Api: 'org.hl7.fhir.api' });
34
+ export const CommunicationStatus = Object.freeze({ Completed: 'completed' });
35
+ export const CommunicationCategoryCode = Object.freeze({ Notification: 'notification' });
36
+ export const TaskStatusCode = Object.freeze({ Completed: 'completed', Failed: 'failed' });
37
+ const searchParameter = (code, type, expression) => Object.freeze({ code, type, expression });
38
+ /** Exact Subscription-specific SearchParameters published by FHIR R5 5.0.0. */
39
+ export const SubscriptionR5SearchParameterCatalog = Object.freeze([
40
+ searchParameter('contact', 'token', 'Subscription.contact'),
41
+ searchParameter('content-level', 'token', 'Subscription.content'),
42
+ searchParameter('filter-value', 'string', 'Subscription.filterBy.value'),
43
+ searchParameter('identifier', 'token', 'Subscription.identifier'),
44
+ searchParameter('name', 'string', 'Subscription.name'),
45
+ searchParameter('owner', 'reference', 'Subscription.managingEntity'),
46
+ searchParameter('payload', 'token', 'Subscription.contentType'),
47
+ searchParameter('status', 'token', 'Subscription.status'),
48
+ searchParameter('topic', 'uri', 'Subscription.topic'),
49
+ searchParameter('type', 'token', 'Subscription.channelType'),
50
+ searchParameter('url', 'uri', 'Subscription.endpoint'),
51
+ ]);
52
+ /** Exact SubscriptionTopic-specific SearchParameters published by FHIR R5 5.0.0. */
53
+ export const SubscriptionTopicR5SearchParameterCatalog = Object.freeze([
54
+ searchParameter('date', 'date', 'SubscriptionTopic.date'),
55
+ searchParameter('derived-or-self', 'uri', 'SubscriptionTopic.url | SubscriptionTopic.derivedFrom'),
56
+ searchParameter('effective', 'date', 'SubscriptionTopic.effectivePeriod'),
57
+ searchParameter('event', 'token', 'SubscriptionTopic.eventTrigger.event'),
58
+ searchParameter('identifier', 'token', 'SubscriptionTopic.identifier'),
59
+ searchParameter('resource', 'uri', 'SubscriptionTopic.resourceTrigger.resource | SubscriptionTopic.eventTrigger.resource | SubscriptionTopic.canFilterBy.resource | SubscriptionTopic.notificationShape.resource'),
60
+ searchParameter('status', 'token', 'SubscriptionTopic.status'),
61
+ searchParameter('title', 'string', 'SubscriptionTopic.title'),
62
+ searchParameter('trigger-description', 'string', 'SubscriptionTopic.resourceTrigger.description'),
63
+ searchParameter('url', 'uri', 'SubscriptionTopic.url'),
64
+ searchParameter('version', 'token', 'SubscriptionTopic.version'),
65
+ ]);
66
+ /** Builds the native R5 topic used to notify subscribers about Communication writes. */
67
+ export function buildCommunicationSubscriptionTopicR5(input) {
68
+ return Object.freeze({
69
+ resourceType: ResourceTypesFhirR5.SubscriptionTopic,
70
+ id: required(input.id, 'subscription_topic_id_required'),
71
+ url: absoluteHttpsUrl(input.url, 'subscription_topic_url_invalid'),
72
+ status: SubscriptionTopicR5Status.Active,
73
+ resourceTrigger: Object.freeze([{ resource: ResourceTypesFhirR5.Communication }]),
74
+ canFilterBy: Object.freeze([
75
+ Object.freeze({ resource: ResourceTypesFhirR5.Communication, filterParameter: 'identifier', comparator: Object.freeze(['eq', 'ne']) }),
76
+ Object.freeze({ resource: ResourceTypesFhirR5.Communication, filterParameter: 'recipient', comparator: Object.freeze(['eq', 'ne']) }),
77
+ Object.freeze({ resource: ResourceTypesFhirR5.Communication, filterParameter: 'status', comparator: Object.freeze(['eq', 'ne']) }),
78
+ ]),
79
+ });
80
+ }
81
+ /** Builds one native R5 rest-hook Subscription. This resource is configuration, not flat clinical state. */
82
+ export function buildRestHookSubscriptionR5(input) {
83
+ const filters = Object.freeze((input.filters ?? []).map(filter => Object.freeze({
84
+ ...(filter.resourceType ? { resourceType: required(filter.resourceType, 'subscription_filter_resource_required') } : {}),
85
+ filterParameter: required(filter.filterParameter, 'subscription_filter_parameter_required'),
86
+ ...(filter.comparator ? { comparator: filter.comparator } : {}),
87
+ ...(filter.modifier ? { modifier: filter.modifier } : {}),
88
+ value: required(filter.value, 'subscription_filter_value_required'),
89
+ })));
90
+ const parameters = Object.freeze((input.parameters ?? []).map(parameter => Object.freeze({
91
+ name: required(parameter.name, 'subscription_parameter_name_required'),
92
+ value: required(parameter.value, 'subscription_parameter_value_required'),
93
+ })));
94
+ return Object.freeze({
95
+ resourceType: ResourceTypesFhirR5.Subscription,
96
+ id: required(input.id, 'subscription_id_required'),
97
+ status: SubscriptionR5Status.Requested,
98
+ topic: absoluteHttpsUrl(input.topic, 'subscription_topic_url_invalid'),
99
+ ...(input.reason?.trim() ? { reason: input.reason.trim() } : {}),
100
+ ...(filters.length ? { filterBy: filters } : {}),
101
+ channelType: Object.freeze({
102
+ system: FhirCodeSystems.SubscriptionChannelType,
103
+ code: SubscriptionR5ChannelType.RestHook,
104
+ }),
105
+ endpoint: absoluteHttpsUrl(input.endpoint, 'subscription_endpoint_invalid'),
106
+ ...(parameters.length ? { parameter: parameters } : {}),
107
+ contentType: SubscriptionR5MediaType,
108
+ content: input.content ?? SubscriptionR5Content.FullResource,
109
+ });
110
+ }
111
+ /**
112
+ * Builds the canonical claims-first Communication emitted when an asynchronous
113
+ * Task reaches a terminal state. `Communication.status=completed` describes
114
+ * the sent message; `Communication.content-code` carries the linked Task state.
115
+ */
116
+ export function buildTerminalJobCommunication(input) {
117
+ if (!Object.values(TaskStatusCode).includes(input.jobStatus))
118
+ throw new TypeError('terminal_job_status_invalid');
119
+ const sent = required(input.sent, 'terminal_job_sent_required');
120
+ if (Number.isNaN(Date.parse(sent)))
121
+ throw new TypeError('terminal_job_sent_invalid');
122
+ return Object.freeze({
123
+ resourceType: ResourceTypesFhirR5.Communication,
124
+ id: required(input.id, 'terminal_job_communication_id_required'),
125
+ meta: Object.freeze({ claims: Object.freeze({
126
+ '@context': FhirFlatClaimContext.Api,
127
+ [CommunicationClaim.Identifier]: required(input.threadId, 'terminal_job_thread_required'),
128
+ [CommunicationClaim.Status]: CommunicationStatus.Completed,
129
+ [CommunicationClaim.Category]: `${FhirCodeSystems.CommunicationCategory}|${CommunicationCategoryCode.Notification}`,
130
+ [CommunicationClaim.Recipient]: required(input.recipient, 'terminal_job_recipient_required'),
131
+ [CommunicationClaim.Sender]: required(input.sender, 'terminal_job_sender_required'),
132
+ [CommunicationClaim.Sent]: sent,
133
+ [CommunicationClaim.Subject]: required(input.researchStudyReference, 'terminal_job_study_required'),
134
+ [CommunicationClaim.ContentReference]: `${ResourceTypesFhirR5.Task}/${required(input.jobId, 'terminal_job_id_required')}`,
135
+ [CommunicationClaim.ContentCode]: `${FhirCodeSystems.TaskStatus}|${input.jobStatus}`,
136
+ }) }),
137
+ });
138
+ }
139
+ /** Validates a standard R5 notification Bundle and resolves every advertised full-resource focus. */
140
+ export function parseSubscriptionNotificationR5(input) {
141
+ const bundle = record(input);
142
+ if (bundle.resourceType !== ResourceTypesFhirR5.Bundle || bundle.type !== SubscriptionR5BundleType.Notification) {
143
+ throw new TypeError('subscription_notification_bundle_invalid');
144
+ }
145
+ const entries = Array.isArray(bundle.entry) ? bundle.entry : [];
146
+ if (entries.length < 1 || entries.length > 101)
147
+ throw new TypeError('subscription_notification_entries_invalid');
148
+ const resources = entries.map(entry => record(record(entry).resource)).filter(resource => resource.resourceType);
149
+ const status = resources.find(resource => resource.resourceType === ResourceTypesFhirR5.SubscriptionStatus);
150
+ if (!status)
151
+ throw new TypeError('subscription_notification_status_missing');
152
+ const notificationType = required(String(status.type ?? ''), 'subscription_notification_type_missing');
153
+ if (!Object.values(SubscriptionR5NotificationType).includes(notificationType)) {
154
+ throw new TypeError('subscription_notification_type_invalid');
155
+ }
156
+ const subscriptionReference = required(String(record(status.subscription).reference ?? ''), 'subscription_notification_subscription_missing');
157
+ const rawEvents = Array.isArray(status.notificationEvent) ? status.notificationEvent : [];
158
+ if (notificationType === SubscriptionR5NotificationType.EventNotification && rawEvents.length < 1) {
159
+ throw new TypeError('subscription_notification_event_missing');
160
+ }
161
+ const events = rawEvents.map(rawEvent => {
162
+ const event = record(rawEvent);
163
+ const focusReference = required(String(record(event.focus).reference ?? ''), 'subscription_notification_focus_missing');
164
+ const focusResource = resources.find(resource => `${resource.resourceType}/${resource.id ?? ''}` === focusReference);
165
+ if (!focusResource)
166
+ throw new TypeError('subscription_notification_focus_missing');
167
+ return Object.freeze({
168
+ eventNumber: required(String(event.eventNumber ?? ''), 'subscription_notification_event_number_missing'),
169
+ ...(event.timestamp ? { timestamp: String(event.timestamp) } : {}),
170
+ focusReference,
171
+ focusResource: focusResource,
172
+ });
173
+ });
174
+ return Object.freeze({
175
+ type: notificationType,
176
+ subscriptionReference,
177
+ ...(status.topic ? { topic: String(status.topic) } : {}),
178
+ events: Object.freeze(events),
179
+ });
180
+ }
181
+ function absoluteHttpsUrl(value, error) {
182
+ const text = required(value, error);
183
+ let url;
184
+ try {
185
+ url = new URL(text);
186
+ }
187
+ catch {
188
+ throw new TypeError(error);
189
+ }
190
+ if (url.protocol !== 'https:')
191
+ throw new TypeError(error);
192
+ return url.href;
193
+ }
194
+ function required(value, error) {
195
+ const text = String(value ?? '').trim();
196
+ if (!text)
197
+ throw new TypeError(error);
198
+ return text;
199
+ }
200
+ function record(value) {
201
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
202
+ }
@@ -28,6 +28,7 @@ and transport assumptions must not be copied.
28
28
  | Anonymization field inventory | Test input only | A separately reviewed policy contract; old field lists do not become policy automatically |
29
29
  | ATC, LOINC, SNOMED and EMA JSON | Validate before reuse | Terminology service fixtures, never authoritative embedded catalogs |
30
30
  | DICOM parsing | Extract later | Separate imaging package; do not add `dicom-parser` to the neutral core |
31
+ | ImagingStudy metadata | Rewrite | Neutral flat claims and R4 projection for subject, encounter, modality, procedure and protected endpoint references; DICOM bytes remain outside |
31
32
  | SHA-1 attachment hashing | Retire | No compatibility writer; historical read/verification belongs in a versioned adapter |
32
33
  | `MessageHeader` transport conventions | Retire | Transport is outside the FHIR data contract |
33
34
  | UHC-branded templates and defaults | Retire or move downstream | Product policy belongs in UHC packages, not here |
@@ -51,6 +52,8 @@ The migration surface is executable rather than documentary only:
51
52
  - every migrated claim family has a named catalog and shared list helpers;
52
53
  - every migrated product-neutral converter participates in the converter
53
54
  round-trip matrix, with rich optional-field fixtures for complex resources;
55
+ - Encounter retains its originating Appointment and ImagingStudy retains
56
+ searchable metadata without embedding DICOM object bytes;
54
57
  - FHIR primitives, typed searches, Bundle traversal and fallback conversion
55
58
  have strict success and rejection tests;
56
59
  - Immunization graphs and reminders live once under `src/builders/`; the former
@@ -109,7 +112,22 @@ family for every assessment scale.
109
112
  | Preserve an obsolete input | Explicit read-only compatibility adapter |
110
113
  | Add transport, SMART or persistence behavior | Owning SDK/service, never this package |
111
114
  | Add sector terminology or policy | SOS, Vet or UHC data package |
115
+ | Configure an R5 SubscriptionTopic/Subscription | Native builder and official search catalog in `subscription-r5`; registration remains in the owning service |
116
+ | Persist or deliver a subscription event | GW/BFF transport and storage, never this package |
112
117
 
113
118
  Converters and builders import catalog symbols. Tests may spell native FHIR
114
119
  wire fields, but do not duplicate flat-claim paths except when proving that a
115
120
  malformed or invented path is rejected.
121
+
122
+ ## Subscription notification boundary
123
+
124
+ FHIR R5 `SubscriptionTopic` and `Subscription` are native configuration
125
+ resources, not flat clinical persistence. This package owns their strict typed
126
+ builders, the official R5 resource-specific SearchParameter catalogs and the
127
+ parser for `Bundle[type=subscription-notification]`. It also owns the minimal
128
+ claims-first terminal Task `Communication` shape so producers and receivers do
129
+ not duplicate claim paths.
130
+
131
+ HTTP delivery, authentication, durable outbox/inbox storage and browser or
132
+ email/push fan-out remain in GW, DataConv and the product BFFs. The neutral
133
+ package contains no HTTP client and no gateway policy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhir-data-utils-ts",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "FHIR-version-neutral flat-claim and SearchParameter data utilities",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -107,6 +107,10 @@
107
107
  "types": "./dist/imaging-types.d.ts",
108
108
  "default": "./dist/imaging-types.js"
109
109
  },
110
+ "./subscription-r5": {
111
+ "types": "./dist/subscription-r5.d.ts",
112
+ "default": "./dist/subscription-r5.js"
113
+ },
110
114
  "./constants": {
111
115
  "types": "./dist/constants/index.d.ts",
112
116
  "default": "./dist/constants/index.js"