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.
- package/.codex/skills/govern-fhir-data-utilities/SKILL.md +9 -0
- package/CHANGELOG.md +20 -0
- package/README.md +26 -0
- package/dist/claim-helpers/claims-helpers-encounter.d.ts +2 -0
- package/dist/claim-helpers/claims-helpers-encounter.js +6 -0
- package/dist/claim-helpers/claims-helpers-imaging-study.d.ts +8 -0
- package/dist/claim-helpers/claims-helpers-imaging-study.js +20 -0
- package/dist/claim-helpers/index.d.ts +1 -0
- package/dist/claim-helpers/index.js +1 -0
- package/dist/claims/communication-claims.d.ts +4 -0
- package/dist/claims/communication-claims.js +4 -0
- package/dist/claims/encounter-claims.d.ts +13 -0
- package/dist/claims/encounter-claims.js +15 -0
- package/dist/claims/imaging-study-claims.d.ts +25 -0
- package/dist/claims/imaging-study-claims.js +43 -0
- package/dist/claims/index.d.ts +1 -0
- package/dist/claims/index.js +1 -0
- package/dist/constants/fhir-code-systems.d.ts +2 -0
- package/dist/constants/fhir-code-systems.js +2 -0
- package/dist/constants/fhir-resource-types.d.ts +2 -0
- package/dist/constants/fhir-resource-types.js +2 -0
- package/dist/converters/convert-encounter.js +5 -1
- package/dist/converters/convert-imaging-study.d.ts +4 -0
- package/dist/converters/convert-imaging-study.js +82 -0
- package/dist/converters/index.d.ts +1 -0
- package/dist/converters/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/subscription-r5.d.ts +150 -0
- package/dist/subscription-r5.js +202 -0
- package/docs/UHC_FHIR_UTILS_MIGRATION.md +18 -0
- 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
|
+
]);
|
package/dist/claims/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/claims/index.js
CHANGED
|
@@ -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';
|
package/dist/converters/index.js
CHANGED
|
@@ -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
package/dist/index.js
CHANGED
|
@@ -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
|
+
"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"
|