fhir-data-utils-ts 0.3.3 → 0.3.4
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 +4 -0
- package/CHANGELOG.md +10 -0
- package/README.md +19 -0
- package/dist/claims/communication-claims.d.ts +4 -0
- package/dist/claims/communication-claims.js +4 -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/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 +15 -0
- package/package.json +5 -1
|
@@ -29,6 +29,10 @@ 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.
|
|
32
36
|
|
|
33
37
|
## Change with executable contracts
|
|
34
38
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.4 - 2026-09-23
|
|
4
|
+
|
|
5
|
+
- Add native FHIR R5 `SubscriptionTopic` and HTTPS rest-hook `Subscription`
|
|
6
|
+
builders plus the official resource-specific SearchParameter catalogs.
|
|
7
|
+
- Add a strict `subscription-notification` Bundle parser that resolves each
|
|
8
|
+
advertised full-resource event focus and distinguishes handshake events.
|
|
9
|
+
- Add the claims-first terminal-job `Communication` contract used by DataConv
|
|
10
|
+
and professional BFFs without introducing transport, storage or product
|
|
11
|
+
policy into the neutral package.
|
|
12
|
+
|
|
3
13
|
## 0.3.3 - 2026-09-23
|
|
4
14
|
|
|
5
15
|
- 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;
|
|
@@ -111,6 +114,22 @@ The package exports builders, traversal/confirmation helpers and
|
|
|
111
114
|
`CODING_REVIEW_EXAMPLES`. Tests and consumers reuse them instead of duplicating
|
|
112
115
|
claim strings, terminology identifiers, codes or UUIDs.
|
|
113
116
|
|
|
117
|
+
## R5 asynchronous notifications
|
|
118
|
+
|
|
119
|
+
`fhir-data-utils-ts/subscription-r5` separates three boundaries:
|
|
120
|
+
|
|
121
|
+
- `SubscriptionTopic` and `Subscription` are native R5 configuration resources;
|
|
122
|
+
- the job-completion `Communication` is canonical flat-claim business data;
|
|
123
|
+
- `Bundle[type=subscription-notification]` is parsed only at the receiving
|
|
124
|
+
rest-hook boundary.
|
|
125
|
+
|
|
126
|
+
The shared package owns types, builders, official SearchParameter metadata and
|
|
127
|
+
validation. DataConv owns emitting the terminal Communication, GW owns durable
|
|
128
|
+
subscription delivery, and each BFF owns authenticated inbox persistence and
|
|
129
|
+
browser/email/push fan-out. Tenant-level Communication subscriptions initially
|
|
130
|
+
use no server-side filter; the receiving BFF routes the minimal full resource by
|
|
131
|
+
its canonical `Communication.recipient` claim.
|
|
132
|
+
|
|
114
133
|
## Verification gate
|
|
115
134
|
|
|
116
135
|
`npm run check` runs strict type checking, all contract tests and built-in
|
|
@@ -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. */
|
|
@@ -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. */
|
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
|
+
}
|
|
@@ -109,7 +109,22 @@ family for every assessment scale.
|
|
|
109
109
|
| Preserve an obsolete input | Explicit read-only compatibility adapter |
|
|
110
110
|
| Add transport, SMART or persistence behavior | Owning SDK/service, never this package |
|
|
111
111
|
| Add sector terminology or policy | SOS, Vet or UHC data package |
|
|
112
|
+
| Configure an R5 SubscriptionTopic/Subscription | Native builder and official search catalog in `subscription-r5`; registration remains in the owning service |
|
|
113
|
+
| Persist or deliver a subscription event | GW/BFF transport and storage, never this package |
|
|
112
114
|
|
|
113
115
|
Converters and builders import catalog symbols. Tests may spell native FHIR
|
|
114
116
|
wire fields, but do not duplicate flat-claim paths except when proving that a
|
|
115
117
|
malformed or invented path is rejected.
|
|
118
|
+
|
|
119
|
+
## Subscription notification boundary
|
|
120
|
+
|
|
121
|
+
FHIR R5 `SubscriptionTopic` and `Subscription` are native configuration
|
|
122
|
+
resources, not flat clinical persistence. This package owns their strict typed
|
|
123
|
+
builders, the official R5 resource-specific SearchParameter catalogs and the
|
|
124
|
+
parser for `Bundle[type=subscription-notification]`. It also owns the minimal
|
|
125
|
+
claims-first terminal Task `Communication` shape so producers and receivers do
|
|
126
|
+
not duplicate claim paths.
|
|
127
|
+
|
|
128
|
+
HTTP delivery, authentication, durable outbox/inbox storage and browser or
|
|
129
|
+
email/push fan-out remain in GW, DataConv and the product BFFs. The neutral
|
|
130
|
+
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.4",
|
|
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"
|