sos-sdk-core-ts 0.3.0 → 0.4.0
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/CHANGELOG.md +35 -1
- package/README.md +27 -1
- package/dist/care-emergency-alert-orchestration.d.ts +47 -0
- package/dist/care-emergency-alert-orchestration.js +134 -2
- package/dist/communication-bundle.d.ts +91 -0
- package/dist/communication-bundle.js +249 -0
- package/dist/equipment-compliance-orchestration.d.ts +39 -0
- package/dist/equipment-compliance-orchestration.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/docs/CARE_EMERGENCY_AND_ALERT_ORCHESTRATION.md +35 -1
- package/docs/COMMUNICATION_BUNDLE_EDITOR_READER.md +84 -0
- package/docs/EQUIPMENT_COMPLIANCE.md +43 -0
- package/package.json +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.4.0 - 2026-09-29
|
|
4
|
+
|
|
5
|
+
- Add the shared high-level Communication Bundle editor and reader for SOS,
|
|
6
|
+
Vet and UHC. Author an outer batch Bundle of Communications, validate each
|
|
7
|
+
attached inner document/batch/collection Bundle with
|
|
8
|
+
`fhir-data-utils-ts@0.6.0`, and keep PDF, image, audio and calendar files as
|
|
9
|
+
inner DocumentReference resources.
|
|
10
|
+
- Pin `sos-data-utils-ts@0.5.1` and extend the existing
|
|
11
|
+
`gdc-common-utils-ts@2.9.21` Bundle editor, reader and attached-Bundle session
|
|
12
|
+
rather than introducing a parallel high-level implementation.
|
|
13
|
+
|
|
14
|
+
- Add runtime-neutral, department-aware equipment calibration and operating-authorization orchestration; blood-bank labeling remains explicitly deferred.
|
|
15
|
+
|
|
16
|
+
- Link the coordinated SOS/Vet/UHC clinical accessor adoption boundary so
|
|
17
|
+
shared orchestration is added only with an actual multichannel workflow.
|
|
18
|
+
|
|
19
|
+
- Require the initial emergency triage Task owner to be a professional Group,
|
|
20
|
+
while keeping the accepting PractitionerRole as Task owner and authenticated
|
|
21
|
+
transport actor after acknowledgement.
|
|
22
|
+
- Define the department Organization as the FHIR Communication sender and keep
|
|
23
|
+
the professional Group solely as the stable reply queue.
|
|
24
|
+
- Clarify that Group reply routing never replaces the legal Organization
|
|
25
|
+
document author, professional attester or authenticated transport actor.
|
|
26
|
+
|
|
27
|
+
## 0.3.1 - 2026-09-27
|
|
28
|
+
|
|
29
|
+
- Pin the latest already-published `sos-data-utils-ts@0.4.2` baseline while
|
|
30
|
+
grouped unreleased Task lifecycle changes continue through immutable local
|
|
31
|
+
tarballs.
|
|
32
|
+
- Add high-level emergency triage Task readers and deterministic
|
|
33
|
+
acknowledge/start/escalate/reject/complete transitions without rewriting the
|
|
34
|
+
originating ServiceRequest.
|
|
35
|
+
- Preserve the protected communication-thread correlation on the Task and
|
|
36
|
+
authorize responder callbacks only for the accepted or in-progress Task
|
|
37
|
+
owner; endpoint resolution and signaling remain product-server concerns.
|
|
4
38
|
|
|
5
39
|
## 0.3.0 - 2026-09-26
|
|
6
40
|
|
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# Shared SOS SDK Core
|
|
2
|
+
|
|
3
|
+
Shared SOS/Vet/UHC orchestration includes [equipment calibration and operating authorization](docs/EQUIPMENT_COMPLIANCE.md) without embedding product roles or persistence plumbing.
|
|
2
4
|
## Blocking local-first SDK release
|
|
3
5
|
|
|
4
6
|
The canonical contract is [`SDK_LAYERING.md`](https://github.com/Fundacion-UNID/sos-data-utils-ts/blob/main/docs/SDK_LAYERING.md).
|
|
@@ -38,6 +40,24 @@ jobs; those adapters do not belong here.
|
|
|
38
40
|
Git, workspace, vendored and `file:` dependencies are forbidden in committed
|
|
39
41
|
or deployed dependency state.
|
|
40
42
|
|
|
43
|
+
The cross-product Bundle/Communication editor and reader are owned here as
|
|
44
|
+
high-level extensions of the existing `gdc-common-utils-ts` `BundleEditor`,
|
|
45
|
+
`BundleReader` and `CommunicationAttachedBundleSession`. They preserve those
|
|
46
|
+
APIs instead of duplicating their attachment or claims-first lifecycle, while
|
|
47
|
+
`fhir-data-utils-ts` supplies neutral import, projection and validation tools.
|
|
48
|
+
Follow the
|
|
49
|
+
[SOS/Vet/UHC clinical accessor adoption matrix](https://github.com/Fundacion-UNID/sos-data-utils-ts/blob/main/docs/CLINICAL_TYPED_ACCESSOR_ADOPTION.md)
|
|
50
|
+
before changing dependencies: unaffected portals and assistants are not forced
|
|
51
|
+
to upgrade merely because a lower package published a newer editor.
|
|
52
|
+
|
|
53
|
+
All cross-participant exchanges use the established claims-first outer batch
|
|
54
|
+
Bundle containing one or more Communication resources. Each Communication
|
|
55
|
+
contains exactly one inner document, batch or collection Bundle in its
|
|
56
|
+
`application/fhir+json` attachment claims. Binary files such as PDF, image,
|
|
57
|
+
audio or `text/calendar` are DocumentReference entries inside the inner Bundle.
|
|
58
|
+
Native FHIR R4/R5 is materialized only at an explicit adapter boundary. See
|
|
59
|
+
[Communication Bundle editor and reader](docs/COMMUNICATION_BUNDLE_EDITOR_READER.md).
|
|
60
|
+
|
|
41
61
|
The emergency-group contract stores subject references only on an enumerated
|
|
42
62
|
FHIR Group. A group assistance request references that Group from one standard,
|
|
43
63
|
compact-coded ServiceRequest; it never copies members or persists split coding
|
|
@@ -63,10 +83,16 @@ a canonical SOSChain Place or vehicle without merging either identity.
|
|
|
63
83
|
`sos-sdk-core-ts/care-emergency-alert-orchestration` supplies the shared,
|
|
64
84
|
runtime-neutral plans for Appointment-to-Encounter continuity, protected
|
|
65
85
|
pre-publication calls/messages, resilient emergency ServiceRequest plus triage
|
|
66
|
-
Task creation, and governed
|
|
86
|
+
Task creation/transition/responder callback authorization, and governed
|
|
87
|
+
donation/public-risk campaigns. Transport,
|
|
67
88
|
persistence, authorization evidence acquisition and product policy remain in
|
|
68
89
|
the channel service, Node adapter and Vet/UHC/SOSChain layers.
|
|
69
90
|
|
|
91
|
+
Emergency triage starts with a professional Group owner and changes to the
|
|
92
|
+
accepting PractitionerRole. The Group remains a reply queue, never the
|
|
93
|
+
clinical-document author or human Communication sender; those identities stay
|
|
94
|
+
separate from the professional attester and transport audit evidence.
|
|
95
|
+
|
|
70
96
|
See [docs/CARE_EMERGENCY_AND_ALERT_ORCHESTRATION.md](docs/CARE_EMERGENCY_AND_ALERT_ORCHESTRATION.md)
|
|
71
97
|
for lifecycle, authorization and persistence invariants.
|
|
72
98
|
|
|
@@ -54,6 +54,53 @@ export declare function orchestrateEmergencyRequest(input: Readonly<{
|
|
|
54
54
|
serviceRequestEntry: FlatClaimResourceEntry;
|
|
55
55
|
triageTaskEntry: FlatClaimResourceEntry;
|
|
56
56
|
}>;
|
|
57
|
+
export type EmergencyTriageAction = 'acknowledge' | 'start' | 'escalate' | 'reject' | 'complete';
|
|
58
|
+
export type EmergencyTriageTask = Readonly<{
|
|
59
|
+
taskReference: string;
|
|
60
|
+
status: string;
|
|
61
|
+
focusReference: string;
|
|
62
|
+
subjectReference: string;
|
|
63
|
+
requesterReference: string;
|
|
64
|
+
ownerReference: string;
|
|
65
|
+
authoredAt: string;
|
|
66
|
+
communicationThreadId?: string;
|
|
67
|
+
lastModified?: string;
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Reads only the standard, claims-first coordinates required by a responder
|
|
71
|
+
* console. It does not resolve contact endpoints or grant callback authority.
|
|
72
|
+
*/
|
|
73
|
+
export declare function readEmergencyTriageTask(taskEntry: FlatClaimResourceEntry): EmergencyTriageTask;
|
|
74
|
+
/**
|
|
75
|
+
* Applies one deterministic responder transition to the Task only. The
|
|
76
|
+
* ServiceRequest remains immutable; callers must authorize the responder and
|
|
77
|
+
* resolve any escalation owner before invoking this function.
|
|
78
|
+
*/
|
|
79
|
+
export declare function transitionEmergencyTriageTask(input: Readonly<{
|
|
80
|
+
taskEntry: FlatClaimResourceEntry;
|
|
81
|
+
action: EmergencyTriageAction;
|
|
82
|
+
responderReference: string;
|
|
83
|
+
escalationOwnerReference?: string;
|
|
84
|
+
occurredAt: string;
|
|
85
|
+
}>): FlatClaimResourceEntry;
|
|
86
|
+
export type EmergencyResponderCallback = Readonly<{
|
|
87
|
+
taskReference: string;
|
|
88
|
+
serviceRequestReference: string;
|
|
89
|
+
subjectReference: string;
|
|
90
|
+
requesterReference: string;
|
|
91
|
+
responderReference: string;
|
|
92
|
+
communicationThreadId: string;
|
|
93
|
+
media: 'audio' | 'video';
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* Authorizes callback correlation for the current Task owner. Protected
|
|
97
|
+
* endpoint resolution and WebRTC signaling remain in the product server.
|
|
98
|
+
*/
|
|
99
|
+
export declare function prepareEmergencyResponderCallback(input: Readonly<{
|
|
100
|
+
taskEntry: FlatClaimResourceEntry;
|
|
101
|
+
responderReference: string;
|
|
102
|
+
media: 'audio' | 'video';
|
|
103
|
+
}>): EmergencyResponderCallback;
|
|
57
104
|
export type GovernedAlertCampaignPlan = Readonly<{
|
|
58
105
|
campaign: GovernedAlertCampaign;
|
|
59
106
|
cohortSnapshot: GovernedAlertCohortSnapshot;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { freezeGovernedAlertCohort, normalizeCareJourneyTransition, normalizeEmergencyIntake, normalizeGovernedAlertCampaign, recordGovernedAlertDelivery, } from 'sos-data-utils-ts/care-emergency-alert-records';
|
|
2
|
+
import { TaskClaim, TaskStatus } from 'sos-data-utils-ts/task-artifact-schema';
|
|
2
3
|
import { buildCohortCommunicationRequestFlatEntry, buildEncounterCareJourneyFlatGraph, buildServiceRequestFlatEntry, buildServiceRequestTriageTaskFlatEntry, } from 'fhir-data-utils-ts/care-emergency-alert';
|
|
3
4
|
/**
|
|
4
5
|
* Plans one idempotent Appointment-to-Encounter transition. Persistence must
|
|
@@ -57,6 +58,7 @@ export function authorizeEncounterCommunication(input) {
|
|
|
57
58
|
/** Creates independent standard ServiceRequest and durable triage Task entries. */
|
|
58
59
|
export function orchestrateEmergencyRequest(input) {
|
|
59
60
|
const intake = normalizeEmergencyIntake(input.intake);
|
|
61
|
+
const triageGroupReference = reference(intake.triageOwnerReference, 'Group', 'emergency_triage_group_owner_required');
|
|
60
62
|
const serviceRequestEntry = buildServiceRequestFlatEntry({
|
|
61
63
|
entryId: intake.serviceRequestEntryId,
|
|
62
64
|
identifier: input.serviceRequestIdentifier,
|
|
@@ -69,17 +71,135 @@ export function orchestrateEmergencyRequest(input) {
|
|
|
69
71
|
performerReferences: [intake.targetServiceReference],
|
|
70
72
|
authoredAt: intake.receivedAt,
|
|
71
73
|
});
|
|
72
|
-
const
|
|
74
|
+
const baseTriageTaskEntry = buildServiceRequestTriageTaskFlatEntry({
|
|
73
75
|
entryId: intake.triageTaskEntryId,
|
|
74
76
|
identifier: input.triageTaskIdentifier,
|
|
75
77
|
serviceRequestReference: serviceRequestEntry.reference,
|
|
76
78
|
subjectReference: intake.subjectReference,
|
|
77
79
|
requesterReference: intake.requesterReference,
|
|
78
|
-
ownerReference:
|
|
80
|
+
ownerReference: triageGroupReference,
|
|
79
81
|
authoredAt: intake.receivedAt,
|
|
80
82
|
});
|
|
83
|
+
const triageTaskEntry = Object.freeze({
|
|
84
|
+
...baseTriageTaskEntry,
|
|
85
|
+
claims: Object.freeze({
|
|
86
|
+
...baseTriageTaskEntry.claims,
|
|
87
|
+
[TaskClaim.GroupIdentifier]: intake.communicationThreadId,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
81
90
|
return Object.freeze({ intake, serviceRequestEntry, triageTaskEntry });
|
|
82
91
|
}
|
|
92
|
+
// Compatibility overlay for the published data package used by clean installs.
|
|
93
|
+
// The next grouped release exposes the same standard values directly there.
|
|
94
|
+
const EmergencyTaskStatus = Object.freeze({
|
|
95
|
+
...TaskStatus,
|
|
96
|
+
Received: 'received',
|
|
97
|
+
Accepted: 'accepted',
|
|
98
|
+
Rejected: 'rejected',
|
|
99
|
+
Ready: 'ready',
|
|
100
|
+
OnHold: 'on-hold',
|
|
101
|
+
EnteredInError: 'entered-in-error',
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Reads only the standard, claims-first coordinates required by a responder
|
|
105
|
+
* console. It does not resolve contact endpoints or grant callback authority.
|
|
106
|
+
*/
|
|
107
|
+
export function readEmergencyTriageTask(taskEntry) {
|
|
108
|
+
if (taskEntry.resourceType !== 'Task' || taskEntry.reference !== `Task/${taskEntry.entryId}`) {
|
|
109
|
+
throw new TypeError('emergency_triage_task_invalid');
|
|
110
|
+
}
|
|
111
|
+
const claims = taskEntry.claims;
|
|
112
|
+
const status = scalarClaim(claims[TaskClaim.Status], 'emergency_triage_status_required');
|
|
113
|
+
if (!Object.values(EmergencyTaskStatus).includes(status)) {
|
|
114
|
+
throw new TypeError('emergency_triage_status_invalid');
|
|
115
|
+
}
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
taskReference: taskEntry.reference,
|
|
118
|
+
status,
|
|
119
|
+
focusReference: reference(scalarClaim(claims[TaskClaim.Focus], 'emergency_triage_focus_required'), 'ServiceRequest', 'emergency_triage_focus_invalid'),
|
|
120
|
+
subjectReference: reference(scalarClaim(claims[TaskClaim.For], 'emergency_triage_subject_required'), undefined, 'emergency_triage_subject_invalid'),
|
|
121
|
+
requesterReference: reference(scalarClaim(claims[TaskClaim.Requester], 'emergency_triage_requester_required'), undefined, 'emergency_triage_requester_invalid'),
|
|
122
|
+
ownerReference: reference(scalarClaim(claims[TaskClaim.Owner], 'emergency_triage_owner_required'), undefined, 'emergency_triage_owner_invalid'),
|
|
123
|
+
authoredAt: dateTime(scalarClaim(claims[TaskClaim.AuthoredOn], 'emergency_triage_authored_on_required'), 'emergency_triage_authored_on_invalid'),
|
|
124
|
+
...(claims[TaskClaim.GroupIdentifier]
|
|
125
|
+
? { communicationThreadId: scalarClaim(claims[TaskClaim.GroupIdentifier], 'emergency_triage_thread_invalid') }
|
|
126
|
+
: {}),
|
|
127
|
+
...(claims[TaskClaim.LastModified]
|
|
128
|
+
? { lastModified: dateTime(scalarClaim(claims[TaskClaim.LastModified], 'emergency_triage_last_modified_invalid'), 'emergency_triage_last_modified_invalid') }
|
|
129
|
+
: {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Applies one deterministic responder transition to the Task only. The
|
|
134
|
+
* ServiceRequest remains immutable; callers must authorize the responder and
|
|
135
|
+
* resolve any escalation owner before invoking this function.
|
|
136
|
+
*/
|
|
137
|
+
export function transitionEmergencyTriageTask(input) {
|
|
138
|
+
const current = readEmergencyTriageTask(input.taskEntry);
|
|
139
|
+
const responderReference = reference(input.responderReference, 'PractitionerRole', 'emergency_triage_responder_invalid');
|
|
140
|
+
const transition = emergencyTriageTransition(current.status, input.action);
|
|
141
|
+
const ownerReference = input.action === 'escalate'
|
|
142
|
+
? reference(input.escalationOwnerReference, undefined, 'emergency_triage_escalation_owner_required')
|
|
143
|
+
: input.action === 'acknowledge'
|
|
144
|
+
? responderReference
|
|
145
|
+
: current.ownerReference;
|
|
146
|
+
const claims = {
|
|
147
|
+
...input.taskEntry.claims,
|
|
148
|
+
[TaskClaim.Status]: transition,
|
|
149
|
+
[TaskClaim.Owner]: ownerReference,
|
|
150
|
+
[TaskClaim.LastModified]: dateTime(input.occurredAt, 'emergency_triage_occurred_at_invalid'),
|
|
151
|
+
...(input.action === 'escalate' ? { [TaskClaim.EscalationRecipient]: ownerReference } : {}),
|
|
152
|
+
};
|
|
153
|
+
return Object.freeze({ ...input.taskEntry, claims: Object.freeze(claims) });
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Authorizes callback correlation for the current Task owner. Protected
|
|
157
|
+
* endpoint resolution and WebRTC signaling remain in the product server.
|
|
158
|
+
*/
|
|
159
|
+
export function prepareEmergencyResponderCallback(input) {
|
|
160
|
+
const task = readEmergencyTriageTask(input.taskEntry);
|
|
161
|
+
const responderReference = reference(input.responderReference, 'PractitionerRole', 'emergency_callback_responder_invalid');
|
|
162
|
+
if (task.ownerReference !== responderReference)
|
|
163
|
+
throw new TypeError('emergency_callback_task_owner_required');
|
|
164
|
+
if (task.status !== EmergencyTaskStatus.Accepted && task.status !== EmergencyTaskStatus.InProgress) {
|
|
165
|
+
throw new TypeError('emergency_callback_active_task_required');
|
|
166
|
+
}
|
|
167
|
+
if (!task.communicationThreadId)
|
|
168
|
+
throw new TypeError('emergency_callback_thread_required');
|
|
169
|
+
if (input.media !== 'audio' && input.media !== 'video')
|
|
170
|
+
throw new TypeError('emergency_callback_media_invalid');
|
|
171
|
+
return Object.freeze({
|
|
172
|
+
taskReference: task.taskReference,
|
|
173
|
+
serviceRequestReference: task.focusReference,
|
|
174
|
+
subjectReference: task.subjectReference,
|
|
175
|
+
requesterReference: task.requesterReference,
|
|
176
|
+
responderReference,
|
|
177
|
+
communicationThreadId: task.communicationThreadId,
|
|
178
|
+
media: input.media,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function emergencyTriageTransition(currentStatus, action) {
|
|
182
|
+
const transitions = {
|
|
183
|
+
acknowledge: { [EmergencyTaskStatus.Requested]: EmergencyTaskStatus.Accepted, [EmergencyTaskStatus.Received]: EmergencyTaskStatus.Accepted },
|
|
184
|
+
start: { [EmergencyTaskStatus.Accepted]: EmergencyTaskStatus.InProgress, [EmergencyTaskStatus.Ready]: EmergencyTaskStatus.InProgress },
|
|
185
|
+
escalate: {
|
|
186
|
+
[EmergencyTaskStatus.Requested]: EmergencyTaskStatus.Requested,
|
|
187
|
+
[EmergencyTaskStatus.Received]: EmergencyTaskStatus.Requested,
|
|
188
|
+
[EmergencyTaskStatus.Accepted]: EmergencyTaskStatus.Requested,
|
|
189
|
+
[EmergencyTaskStatus.InProgress]: EmergencyTaskStatus.Requested,
|
|
190
|
+
},
|
|
191
|
+
reject: {
|
|
192
|
+
[EmergencyTaskStatus.Requested]: EmergencyTaskStatus.Rejected,
|
|
193
|
+
[EmergencyTaskStatus.Received]: EmergencyTaskStatus.Rejected,
|
|
194
|
+
[EmergencyTaskStatus.Accepted]: EmergencyTaskStatus.Rejected,
|
|
195
|
+
},
|
|
196
|
+
complete: { [EmergencyTaskStatus.InProgress]: EmergencyTaskStatus.Completed },
|
|
197
|
+
};
|
|
198
|
+
const nextStatus = transitions[action]?.[currentStatus];
|
|
199
|
+
if (!nextStatus)
|
|
200
|
+
throw new TypeError('emergency_triage_transition_invalid');
|
|
201
|
+
return nextStatus;
|
|
202
|
+
}
|
|
83
203
|
/** Builds one campaign request bound to the frozen evaluated Group snapshot. */
|
|
84
204
|
export function prepareGovernedAlertCampaign(input) {
|
|
85
205
|
const campaign = normalizeGovernedAlertCampaign(input.campaign);
|
|
@@ -122,6 +242,18 @@ export function prepareGovernedAlertDelivery(input) {
|
|
|
122
242
|
outcome: input.outcome,
|
|
123
243
|
});
|
|
124
244
|
}
|
|
245
|
+
function scalarClaim(value, error) {
|
|
246
|
+
if (typeof value !== 'string' || !value.trim())
|
|
247
|
+
throw new TypeError(error);
|
|
248
|
+
return value.trim();
|
|
249
|
+
}
|
|
250
|
+
function dateTime(value, error) {
|
|
251
|
+
const normalized = scalarClaim(value, error);
|
|
252
|
+
if (!/^\d{4}-\d{2}-\d{2}T/u.test(normalized) || Number.isNaN(Date.parse(normalized))) {
|
|
253
|
+
throw new TypeError(error);
|
|
254
|
+
}
|
|
255
|
+
return normalized;
|
|
256
|
+
}
|
|
125
257
|
function uniqueReferences(values, error) {
|
|
126
258
|
const references = [...new Set(values.map(value => reference(value, undefined, error)))];
|
|
127
259
|
if (!references.length)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { BundleEditor } from 'gdc-common-utils-ts/utils/bundle-editor';
|
|
2
|
+
import { BundleReader } from 'gdc-common-utils-ts/utils/bundle-reader';
|
|
3
|
+
import { CommunicationAttachedBundleSession } from 'gdc-common-utils-ts/utils/communication-attached-bundle-session';
|
|
4
|
+
import { type FhirResource } from 'fhir-data-utils-ts';
|
|
5
|
+
export type CommunicationInnerBundleType = 'document' | 'batch' | 'collection';
|
|
6
|
+
export type AttachmentDocumentInput = Readonly<{
|
|
7
|
+
identifier: string;
|
|
8
|
+
subjectReference: string;
|
|
9
|
+
contentType: string;
|
|
10
|
+
dataBase64?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
date?: string;
|
|
15
|
+
authorReferences?: readonly string[];
|
|
16
|
+
eventReferences?: readonly string[];
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* SOS/Vet/UHC extension of the existing GDC BundleEditor.
|
|
20
|
+
*
|
|
21
|
+
* The inherited editor owns the canonical outer claims-first batch. Each
|
|
22
|
+
* staged message is a CommunicationAttachedBundleSession, so the existing
|
|
23
|
+
* attachment synchronization and Bundle lifecycle remain the source of truth.
|
|
24
|
+
* FHIR data utilities are used only at import/materialization boundaries.
|
|
25
|
+
*/
|
|
26
|
+
export declare class CommunicationBundleEditor extends BundleEditor {
|
|
27
|
+
#private;
|
|
28
|
+
constructor();
|
|
29
|
+
newCommunication(): CommunicationMessageEditor;
|
|
30
|
+
/** Builds the inherited claims-first outer batch after validating every attached Bundle. */
|
|
31
|
+
build(): ReturnType<BundleEditor['build']>;
|
|
32
|
+
}
|
|
33
|
+
/** High-level message editor extending the established attached-Bundle session. */
|
|
34
|
+
export declare class CommunicationMessageEditor extends CommunicationAttachedBundleSession {
|
|
35
|
+
#private;
|
|
36
|
+
constructor(parent: CommunicationBundleEditor);
|
|
37
|
+
setIdentifier(value: string): this;
|
|
38
|
+
getIdentifier(): string | undefined;
|
|
39
|
+
setStatus(value: string): this;
|
|
40
|
+
getStatus(): string | undefined;
|
|
41
|
+
setSubject(value: string): this;
|
|
42
|
+
getSubject(): string | undefined;
|
|
43
|
+
setSender(value: string): this;
|
|
44
|
+
getSender(): string | undefined;
|
|
45
|
+
setRecipients(values: readonly string[]): this;
|
|
46
|
+
getRecipients(): readonly string[];
|
|
47
|
+
setSent(value: string): this;
|
|
48
|
+
getSent(): string | undefined;
|
|
49
|
+
setAttachmentTitle(value: string): this;
|
|
50
|
+
getAttachmentTitle(): string | undefined;
|
|
51
|
+
setInnerBundleType(value: CommunicationInnerBundleType): this;
|
|
52
|
+
getInnerBundleType(): CommunicationInnerBundleType;
|
|
53
|
+
/** Adds one canonical claims-first resource to the existing attached-Bundle session. */
|
|
54
|
+
addResource(resource: FhirResource, request?: Readonly<{
|
|
55
|
+
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'GET';
|
|
56
|
+
url: string;
|
|
57
|
+
ifMatch?: string;
|
|
58
|
+
}>): this;
|
|
59
|
+
/** Adds a PDF, image, audio or calendar file as an inner DocumentReference. */
|
|
60
|
+
addAttachmentDocument(input: AttachmentDocumentInput): this;
|
|
61
|
+
doneCommunication(): CommunicationBundleEditor;
|
|
62
|
+
/** @internal */
|
|
63
|
+
validateForBuild(): void;
|
|
64
|
+
/** @internal */
|
|
65
|
+
getCompleteCommunicationClaims(): Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
/** Read-only extension of the established BundleReader for an outer Communication batch. */
|
|
68
|
+
export declare class CommunicationBundleReader extends BundleReader {
|
|
69
|
+
#private;
|
|
70
|
+
constructor(outerBatch: Record<string, unknown>);
|
|
71
|
+
getCommunicationCount(): number;
|
|
72
|
+
openCommunication(index: number): CommunicationMessageReader;
|
|
73
|
+
}
|
|
74
|
+
/** High-level getters over one claims-first Communication and its attached Bundle. */
|
|
75
|
+
export declare class CommunicationMessageReader extends BundleReader {
|
|
76
|
+
#private;
|
|
77
|
+
constructor(entry: Record<string, unknown>);
|
|
78
|
+
getCommunication(): FhirResource;
|
|
79
|
+
getInnerBundle(): Record<string, unknown>;
|
|
80
|
+
getIdentifier(): string;
|
|
81
|
+
getStatus(): string | undefined;
|
|
82
|
+
getSubject(): string | undefined;
|
|
83
|
+
getSender(): string | undefined;
|
|
84
|
+
getRecipients(): readonly string[];
|
|
85
|
+
getSent(): string | undefined;
|
|
86
|
+
getAttachmentTitle(): string | undefined;
|
|
87
|
+
getInnerBundleType(): string | undefined;
|
|
88
|
+
getResourcesByType<TResource extends FhirResource = FhirResource>(resourceType: string): readonly TResource[];
|
|
89
|
+
getAppointments(): readonly FhirResource[];
|
|
90
|
+
getDocumentReferences(): readonly FhirResource[];
|
|
91
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
7
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
|
+
};
|
|
12
|
+
var _CommunicationBundleEditor_instances, _CommunicationBundleEditor_messages, _CommunicationBundleEditor_committed, _CommunicationBundleEditor_commitCommunication, _CommunicationMessageEditor_parent, _CommunicationMessageEditor_status, _CommunicationMessageEditor_sender, _CommunicationMessageEditor_recipients, _CommunicationMessageEditor_sent, _CommunicationMessageEditor_attachmentTitle, _CommunicationBundleReader_communicationEntries, _CommunicationMessageReader_resource, _CommunicationMessageReader_claims;
|
|
13
|
+
import { BundleEditor, BundleOperations, BundleTypes } from 'gdc-common-utils-ts/utils/bundle-editor';
|
|
14
|
+
import { BundleReader } from 'gdc-common-utils-ts/utils/bundle-reader';
|
|
15
|
+
import { CommunicationAttachedBundleSession, CommunicationClaimsContext, } from 'gdc-common-utils-ts/utils/communication-attached-bundle-session';
|
|
16
|
+
import { decodeAttachedBundleFromCommunicationClaims } from 'gdc-common-utils-ts/utils/communication-didcomm-payload';
|
|
17
|
+
import { CommunicationClaim } from 'gdc-common-utils-ts/models/interoperable-claims/communication-claims';
|
|
18
|
+
import { DocumentReferenceClaim } from 'fhir-data-utils-ts';
|
|
19
|
+
/**
|
|
20
|
+
* SOS/Vet/UHC extension of the existing GDC BundleEditor.
|
|
21
|
+
*
|
|
22
|
+
* The inherited editor owns the canonical outer claims-first batch. Each
|
|
23
|
+
* staged message is a CommunicationAttachedBundleSession, so the existing
|
|
24
|
+
* attachment synchronization and Bundle lifecycle remain the source of truth.
|
|
25
|
+
* FHIR data utilities are used only at import/materialization boundaries.
|
|
26
|
+
*/
|
|
27
|
+
export class CommunicationBundleEditor extends BundleEditor {
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
_CommunicationBundleEditor_instances.add(this);
|
|
31
|
+
_CommunicationBundleEditor_messages.set(this, []);
|
|
32
|
+
_CommunicationBundleEditor_committed.set(this, false);
|
|
33
|
+
this.setBundleOperation(BundleOperations.create)
|
|
34
|
+
.setBundleType(BundleTypes.batch)
|
|
35
|
+
.setAllowedResourceType('Communication');
|
|
36
|
+
}
|
|
37
|
+
newCommunication() {
|
|
38
|
+
if (__classPrivateFieldGet(this, _CommunicationBundleEditor_committed, "f"))
|
|
39
|
+
throw new TypeError('communication_bundle_already_built');
|
|
40
|
+
const message = new CommunicationMessageEditor(this);
|
|
41
|
+
__classPrivateFieldGet(this, _CommunicationBundleEditor_messages, "f").push(message);
|
|
42
|
+
return message;
|
|
43
|
+
}
|
|
44
|
+
/** Builds the inherited claims-first outer batch after validating every attached Bundle. */
|
|
45
|
+
build() {
|
|
46
|
+
if (!__classPrivateFieldGet(this, _CommunicationBundleEditor_messages, "f").length)
|
|
47
|
+
throw new TypeError('communication_required');
|
|
48
|
+
if (!__classPrivateFieldGet(this, _CommunicationBundleEditor_committed, "f")) {
|
|
49
|
+
for (const message of __classPrivateFieldGet(this, _CommunicationBundleEditor_messages, "f"))
|
|
50
|
+
__classPrivateFieldGet(this, _CommunicationBundleEditor_instances, "m", _CommunicationBundleEditor_commitCommunication).call(this, message);
|
|
51
|
+
__classPrivateFieldSet(this, _CommunicationBundleEditor_committed, true, "f");
|
|
52
|
+
}
|
|
53
|
+
return super.build();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
_CommunicationBundleEditor_messages = new WeakMap(), _CommunicationBundleEditor_committed = new WeakMap(), _CommunicationBundleEditor_instances = new WeakSet(), _CommunicationBundleEditor_commitCommunication = function _CommunicationBundleEditor_commitCommunication(message) {
|
|
57
|
+
message.validateForBuild();
|
|
58
|
+
const claims = message.getCompleteCommunicationClaims();
|
|
59
|
+
const identifier = requireText(claims[CommunicationClaim.Identifier], 'communication_identifier_required');
|
|
60
|
+
const entry = this.newEntry(identifier);
|
|
61
|
+
for (const [claim, value] of Object.entries(claims))
|
|
62
|
+
entry.setClaim(claim, value);
|
|
63
|
+
entry.create();
|
|
64
|
+
};
|
|
65
|
+
/** High-level message editor extending the established attached-Bundle session. */
|
|
66
|
+
export class CommunicationMessageEditor extends CommunicationAttachedBundleSession {
|
|
67
|
+
constructor(parent) {
|
|
68
|
+
super({
|
|
69
|
+
communicationClaims: { '@context': CommunicationClaimsContext },
|
|
70
|
+
initialBundle: { resourceType: 'Bundle', type: 'batch', data: [] },
|
|
71
|
+
});
|
|
72
|
+
_CommunicationMessageEditor_parent.set(this, void 0);
|
|
73
|
+
_CommunicationMessageEditor_status.set(this, void 0);
|
|
74
|
+
_CommunicationMessageEditor_sender.set(this, void 0);
|
|
75
|
+
_CommunicationMessageEditor_recipients.set(this, []);
|
|
76
|
+
_CommunicationMessageEditor_sent.set(this, void 0);
|
|
77
|
+
_CommunicationMessageEditor_attachmentTitle.set(this, void 0);
|
|
78
|
+
__classPrivateFieldSet(this, _CommunicationMessageEditor_parent, parent, "f");
|
|
79
|
+
}
|
|
80
|
+
setIdentifier(value) { this.setCommunicationIdentifier(value); return this; }
|
|
81
|
+
getIdentifier() { return optionalText(this.getCommunicationIdentifier()); }
|
|
82
|
+
setStatus(value) { __classPrivateFieldSet(this, _CommunicationMessageEditor_status, requireText(value, 'communication_status_required'), "f"); return this; }
|
|
83
|
+
getStatus() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_status, "f"); }
|
|
84
|
+
setSubject(value) { this.setCommunicationSubject(value); return this; }
|
|
85
|
+
getSubject() { return optionalText(this.getCommunicationSubject()); }
|
|
86
|
+
setSender(value) { __classPrivateFieldSet(this, _CommunicationMessageEditor_sender, requireText(value, 'communication_sender_required'), "f"); return this; }
|
|
87
|
+
getSender() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_sender, "f"); }
|
|
88
|
+
setRecipients(values) { __classPrivateFieldSet(this, _CommunicationMessageEditor_recipients, uniqueReferences(values, 'communication_recipient_required'), "f"); return this; }
|
|
89
|
+
getRecipients() { return Object.freeze([...__classPrivateFieldGet(this, _CommunicationMessageEditor_recipients, "f")]); }
|
|
90
|
+
setSent(value) { __classPrivateFieldSet(this, _CommunicationMessageEditor_sent, requireText(value, 'communication_sent_required'), "f"); return this; }
|
|
91
|
+
getSent() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_sent, "f"); }
|
|
92
|
+
setAttachmentTitle(value) { __classPrivateFieldSet(this, _CommunicationMessageEditor_attachmentTitle, requireText(value, 'communication_attachment_title_required'), "f"); return this; }
|
|
93
|
+
getAttachmentTitle() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_attachmentTitle, "f"); }
|
|
94
|
+
setInnerBundleType(value) {
|
|
95
|
+
if (!['document', 'batch', 'collection'].includes(value))
|
|
96
|
+
throw new TypeError('fhir_inner_bundle_type_invalid');
|
|
97
|
+
this.setAttachedBundle({ ...this.getAttachedBundle(), type: value });
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
getInnerBundleType() { return this.getAttachedBundle().type; }
|
|
101
|
+
/** Adds one canonical claims-first resource to the existing attached-Bundle session. */
|
|
102
|
+
addResource(resource, request) {
|
|
103
|
+
const claims = claimsFromResource(resource);
|
|
104
|
+
const identifier = resourceIdentifier(resource, claims);
|
|
105
|
+
this.upsertActiveEntry({
|
|
106
|
+
resourceType: resource.resourceType,
|
|
107
|
+
claims,
|
|
108
|
+
fullUrl: `urn:uuid:${identifier}`,
|
|
109
|
+
...(request ? { request } : {}),
|
|
110
|
+
}).saveAndReleaseActiveEntry();
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
/** Adds a PDF, image, audio or calendar file as an inner DocumentReference. */
|
|
114
|
+
addAttachmentDocument(input) {
|
|
115
|
+
const identifier = requireText(input.identifier, 'document_reference_identifier_required');
|
|
116
|
+
const subjectReference = requireText(input.subjectReference, 'document_reference_subject_required');
|
|
117
|
+
const contentType = requireText(input.contentType, 'document_reference_content_type_required');
|
|
118
|
+
if (!input.dataBase64 && !input.url)
|
|
119
|
+
throw new TypeError('document_reference_content_required');
|
|
120
|
+
const claims = compactClaims({
|
|
121
|
+
'@context': CommunicationClaimsContext,
|
|
122
|
+
[DocumentReferenceClaim.Identifier]: identifier,
|
|
123
|
+
[DocumentReferenceClaim.Subject]: subjectReference,
|
|
124
|
+
[DocumentReferenceClaim.ContentType]: contentType,
|
|
125
|
+
[DocumentReferenceClaim.ContentData]: input.dataBase64,
|
|
126
|
+
[DocumentReferenceClaim.Location]: input.url,
|
|
127
|
+
[DocumentReferenceClaim.Description]: input.description || input.title,
|
|
128
|
+
[DocumentReferenceClaim.Date]: input.date,
|
|
129
|
+
[DocumentReferenceClaim.Author]: input.authorReferences?.join(','),
|
|
130
|
+
[DocumentReferenceClaim.EventReference]: input.eventReferences?.join(','),
|
|
131
|
+
});
|
|
132
|
+
this.upsertActiveDocumentReferenceEntry({ claims, fullUrl: `urn:uuid:${identifier}` }).saveAndReleaseActiveEntry();
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
doneCommunication() { return __classPrivateFieldGet(this, _CommunicationMessageEditor_parent, "f"); }
|
|
136
|
+
/** @internal */
|
|
137
|
+
validateForBuild() {
|
|
138
|
+
requireText(this.getCommunicationIdentifier(), 'communication_identifier_required');
|
|
139
|
+
requireText(__classPrivateFieldGet(this, _CommunicationMessageEditor_status, "f"), 'communication_status_required');
|
|
140
|
+
requireText(this.getCommunicationSubject(), 'communication_subject_required');
|
|
141
|
+
validateAttachedBundle(this.getAttachedBundle());
|
|
142
|
+
}
|
|
143
|
+
/** @internal */
|
|
144
|
+
getCompleteCommunicationClaims() {
|
|
145
|
+
return compactClaims({
|
|
146
|
+
...this.getCommunicationClaims(),
|
|
147
|
+
[CommunicationClaim.Status]: __classPrivateFieldGet(this, _CommunicationMessageEditor_status, "f"),
|
|
148
|
+
[CommunicationClaim.Sender]: __classPrivateFieldGet(this, _CommunicationMessageEditor_sender, "f"),
|
|
149
|
+
[CommunicationClaim.Recipient]: __classPrivateFieldGet(this, _CommunicationMessageEditor_recipients, "f").length ? __classPrivateFieldGet(this, _CommunicationMessageEditor_recipients, "f").join(',') : undefined,
|
|
150
|
+
[CommunicationClaim.Sent]: __classPrivateFieldGet(this, _CommunicationMessageEditor_sent, "f"),
|
|
151
|
+
[CommunicationClaim.ContentAttachmentTitle]: __classPrivateFieldGet(this, _CommunicationMessageEditor_attachmentTitle, "f"),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
_CommunicationMessageEditor_parent = new WeakMap(), _CommunicationMessageEditor_status = new WeakMap(), _CommunicationMessageEditor_sender = new WeakMap(), _CommunicationMessageEditor_recipients = new WeakMap(), _CommunicationMessageEditor_sent = new WeakMap(), _CommunicationMessageEditor_attachmentTitle = new WeakMap();
|
|
156
|
+
/** Read-only extension of the established BundleReader for an outer Communication batch. */
|
|
157
|
+
export class CommunicationBundleReader extends BundleReader {
|
|
158
|
+
constructor(outerBatch) {
|
|
159
|
+
super(outerBatch);
|
|
160
|
+
_CommunicationBundleReader_communicationEntries.set(this, void 0);
|
|
161
|
+
if (this.getBundleType() !== 'batch')
|
|
162
|
+
throw new TypeError('fhir_batch_bundle_required');
|
|
163
|
+
__classPrivateFieldSet(this, _CommunicationBundleReader_communicationEntries, Object.freeze(this.getEntries().filter(entry => entry.resource?.resourceType === 'Communication')), "f");
|
|
164
|
+
if (!__classPrivateFieldGet(this, _CommunicationBundleReader_communicationEntries, "f").length)
|
|
165
|
+
throw new TypeError('fhir_communication_required');
|
|
166
|
+
}
|
|
167
|
+
getCommunicationCount() { return __classPrivateFieldGet(this, _CommunicationBundleReader_communicationEntries, "f").length; }
|
|
168
|
+
openCommunication(index) {
|
|
169
|
+
if (!Number.isInteger(index) || index < 0 || index >= __classPrivateFieldGet(this, _CommunicationBundleReader_communicationEntries, "f").length)
|
|
170
|
+
throw new RangeError('communication_index_invalid');
|
|
171
|
+
return new CommunicationMessageReader(__classPrivateFieldGet(this, _CommunicationBundleReader_communicationEntries, "f")[index]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
_CommunicationBundleReader_communicationEntries = new WeakMap();
|
|
175
|
+
/** High-level getters over one claims-first Communication and its attached Bundle. */
|
|
176
|
+
export class CommunicationMessageReader extends BundleReader {
|
|
177
|
+
constructor(entry) {
|
|
178
|
+
const resource = entry.resource;
|
|
179
|
+
if (resource?.resourceType !== 'Communication')
|
|
180
|
+
throw new TypeError('fhir_communication_required');
|
|
181
|
+
const claims = claimsFromResource(resource);
|
|
182
|
+
const attachedBundle = decodeAttachedBundleFromCommunicationClaims(claims);
|
|
183
|
+
super(attachedBundle);
|
|
184
|
+
_CommunicationMessageReader_resource.set(this, void 0);
|
|
185
|
+
_CommunicationMessageReader_claims.set(this, void 0);
|
|
186
|
+
validateAttachedBundle(attachedBundle);
|
|
187
|
+
__classPrivateFieldSet(this, _CommunicationMessageReader_resource, clone(resource), "f");
|
|
188
|
+
__classPrivateFieldSet(this, _CommunicationMessageReader_claims, clone(claims), "f");
|
|
189
|
+
}
|
|
190
|
+
getCommunication() { return clone(__classPrivateFieldGet(this, _CommunicationMessageReader_resource, "f")); }
|
|
191
|
+
getInnerBundle() { return clone(decodeAttachedBundleFromCommunicationClaims(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f"))); }
|
|
192
|
+
getIdentifier() { return requireText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Identifier], 'communication_identifier_required'); }
|
|
193
|
+
getStatus() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Status]); }
|
|
194
|
+
getSubject() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Subject]); }
|
|
195
|
+
getSender() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Sender]); }
|
|
196
|
+
getRecipients() { return csv(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Recipient]); }
|
|
197
|
+
getSent() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.Sent]); }
|
|
198
|
+
getAttachmentTitle() { return optionalText(__classPrivateFieldGet(this, _CommunicationMessageReader_claims, "f")[CommunicationClaim.ContentAttachmentTitle]); }
|
|
199
|
+
getInnerBundleType() { return this.getBundleType(); }
|
|
200
|
+
getResourcesByType(resourceType) {
|
|
201
|
+
return Object.freeze(this.getEntries().flatMap(entry => {
|
|
202
|
+
const resource = entry.resource;
|
|
203
|
+
return resource?.resourceType === resourceType ? [clone(resource)] : [];
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
getAppointments() { return this.getResourcesByType('Appointment'); }
|
|
207
|
+
getDocumentReferences() { return this.getResourcesByType('DocumentReference'); }
|
|
208
|
+
}
|
|
209
|
+
_CommunicationMessageReader_resource = new WeakMap(), _CommunicationMessageReader_claims = new WeakMap();
|
|
210
|
+
function claimsFromResource(resource) {
|
|
211
|
+
const meta = resource.meta;
|
|
212
|
+
const claims = meta && typeof meta === 'object' && !Array.isArray(meta) ? meta.claims : undefined;
|
|
213
|
+
if (!claims || typeof claims !== 'object' || Array.isArray(claims))
|
|
214
|
+
throw new TypeError('claims_first_resource_required');
|
|
215
|
+
return clone(claims);
|
|
216
|
+
}
|
|
217
|
+
function resourceIdentifier(resource, claims) {
|
|
218
|
+
return requireText(resource.id || claims[`${resource.resourceType}.identifier`], 'resource_identifier_required');
|
|
219
|
+
}
|
|
220
|
+
function validateAttachedBundle(value) {
|
|
221
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
222
|
+
throw new TypeError('fhir_bundle_required');
|
|
223
|
+
const bundle = value;
|
|
224
|
+
if (bundle.resourceType !== 'Bundle')
|
|
225
|
+
throw new TypeError('fhir_bundle_required');
|
|
226
|
+
const type = optionalText(bundle.type);
|
|
227
|
+
if (!type || !['document', 'batch', 'collection'].includes(type))
|
|
228
|
+
throw new TypeError('fhir_inner_bundle_type_invalid');
|
|
229
|
+
const entries = Array.isArray(bundle.data) ? bundle.data : [];
|
|
230
|
+
if (type === 'document' && entries[0]?.resource?.resourceType !== 'Composition')
|
|
231
|
+
throw new TypeError('fhir_document_composition_first_required');
|
|
232
|
+
if (type === 'batch' && entries.some(entry => {
|
|
233
|
+
const request = entry.request;
|
|
234
|
+
return !optionalText(request?.method) || !optionalText(request?.url);
|
|
235
|
+
}))
|
|
236
|
+
throw new TypeError('fhir_batch_request_required');
|
|
237
|
+
}
|
|
238
|
+
function csv(value) {
|
|
239
|
+
if (Array.isArray(value))
|
|
240
|
+
return Object.freeze(value.map(String).map(item => item.trim()).filter(Boolean));
|
|
241
|
+
const normalized = optionalText(value);
|
|
242
|
+
return Object.freeze(normalized ? normalized.split(',').map(item => item.trim()).filter(Boolean) : []);
|
|
243
|
+
}
|
|
244
|
+
function uniqueReferences(values, error) { return [...new Set(values.map(value => requireText(value, error)))]; }
|
|
245
|
+
function compactClaims(input) { return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== '')); }
|
|
246
|
+
function optionalText(value) { return typeof value === 'string' && value.trim() ? value.trim() : undefined; }
|
|
247
|
+
function requireText(value, error) { const normalized = optionalText(value); if (!normalized)
|
|
248
|
+
throw new TypeError(error); return normalized; }
|
|
249
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)); }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { buildDeviceCalibrationFlatGraph, buildDeviceOperatingAuthorizationFlatResource } from 'fhir-data-utils-ts';
|
|
2
|
+
export type EquipmentComplianceAction = 'record-calibration' | 'publish-operating-authorization';
|
|
3
|
+
export type EquipmentComplianceActorContext = Readonly<{
|
|
4
|
+
actorReference: string;
|
|
5
|
+
departmentReference: string;
|
|
6
|
+
}>;
|
|
7
|
+
export type EquipmentComplianceDecision = EquipmentComplianceActorContext & Readonly<{
|
|
8
|
+
action: EquipmentComplianceAction;
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* Runtime-neutral authorization boundary for equipment compliance records.
|
|
12
|
+
* Products decide whether the authenticated assignment in its department may perform the action;
|
|
13
|
+
* the shared SDK never infers authority from a job-title string.
|
|
14
|
+
*/
|
|
15
|
+
export interface EquipmentComplianceAuthorizer {
|
|
16
|
+
authorize(decision: EquipmentComplianceDecision): boolean;
|
|
17
|
+
}
|
|
18
|
+
type CalibrationInput = Parameters<typeof buildDeviceCalibrationFlatGraph>[0];
|
|
19
|
+
type OperatingAuthorizationInput = Parameters<typeof buildDeviceOperatingAuthorizationFlatResource>[0];
|
|
20
|
+
/** Prepares standard claims-first records after an explicit product authorization decision. */
|
|
21
|
+
export declare class EquipmentComplianceManager {
|
|
22
|
+
private readonly authorizer;
|
|
23
|
+
constructor(authorizer: EquipmentComplianceAuthorizer);
|
|
24
|
+
prepareCalibration(input: CalibrationInput, actorContext: EquipmentComplianceActorContext): readonly Readonly<{
|
|
25
|
+
resourceType: "DocumentReference" | "Procedure" | "DeviceMetric" | "RegulatedAuthorization";
|
|
26
|
+
id: string;
|
|
27
|
+
meta: Readonly<{
|
|
28
|
+
claims: Readonly<Record<string, string>>;
|
|
29
|
+
}>;
|
|
30
|
+
}>[];
|
|
31
|
+
prepareOperatingAuthorization(input: OperatingAuthorizationInput, actorContext: EquipmentComplianceActorContext): Readonly<{
|
|
32
|
+
resourceType: "DocumentReference" | "Procedure" | "DeviceMetric" | "RegulatedAuthorization";
|
|
33
|
+
id: string;
|
|
34
|
+
meta: Readonly<{
|
|
35
|
+
claims: Readonly<Record<string, string>>;
|
|
36
|
+
}>;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buildDeviceCalibrationFlatGraph, buildDeviceOperatingAuthorizationFlatResource, } from 'fhir-data-utils-ts';
|
|
2
|
+
/** Prepares standard claims-first records after an explicit product authorization decision. */
|
|
3
|
+
export class EquipmentComplianceManager {
|
|
4
|
+
constructor(authorizer) {
|
|
5
|
+
this.authorizer = authorizer;
|
|
6
|
+
}
|
|
7
|
+
prepareCalibration(input, actorContext) {
|
|
8
|
+
if (!this.authorizer.authorize({ action: 'record-calibration', ...actorContext }))
|
|
9
|
+
throw new Error('equipment_calibration_forbidden');
|
|
10
|
+
return buildDeviceCalibrationFlatGraph(input);
|
|
11
|
+
}
|
|
12
|
+
prepareOperatingAuthorization(input, actorContext) {
|
|
13
|
+
if (!this.authorizer.authorize({ action: 'publish-operating-authorization', ...actorContext })) {
|
|
14
|
+
throw new Error('equipment_operating_authorization_forbidden');
|
|
15
|
+
}
|
|
16
|
+
return buildDeviceOperatingAuthorizationFlatResource(input);
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,3 +6,5 @@ export * from './emergency-authorization.js';
|
|
|
6
6
|
export * from './care-emergency-alert-orchestration.js';
|
|
7
7
|
export * from './place-card-association.js';
|
|
8
8
|
export * from './secure-message-crypto.js';
|
|
9
|
+
export * from './equipment-compliance-orchestration.js';
|
|
10
|
+
export * from './communication-bundle.js';
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,5 @@ export * from './emergency-authorization.js';
|
|
|
5
5
|
export * from './care-emergency-alert-orchestration.js';
|
|
6
6
|
export * from './place-card-association.js';
|
|
7
7
|
export * from './secure-message-crypto.js';
|
|
8
|
+
export * from './equipment-compliance-orchestration.js';
|
|
9
|
+
export * from './communication-bundle.js';
|
|
@@ -37,7 +37,8 @@ independent resources:
|
|
|
37
37
|
- a compact-coded ServiceRequest containing the care request, requester,
|
|
38
38
|
subject and performer target;
|
|
39
39
|
- a Task containing durable intake/triage ownership and linking back through
|
|
40
|
-
`Task.focus`.
|
|
40
|
+
`Task.focus`. Its initial owner is the professional Group assigned to the
|
|
41
|
+
target department/service.
|
|
41
42
|
|
|
42
43
|
The emergency console can claim and update the Task, then open an authorized
|
|
43
44
|
voice/video callback or encrypted thread. Raw phone/email destinations,
|
|
@@ -45,6 +46,39 @@ WebRTC SDP/ICE/TURN state and encryption keys never enter either FHIR resource.
|
|
|
45
46
|
The request grants no read access by itself; subject access still requires the
|
|
46
47
|
current emergency or ordinary authorization contract.
|
|
47
48
|
|
|
49
|
+
The high-level API keeps reads and writes explicit:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const triageTask = readEmergencyTriageTask(claimsFirstTaskEntry)
|
|
53
|
+
const acceptedTaskEntry = transitionEmergencyTriageTask({
|
|
54
|
+
taskEntry: claimsFirstTaskEntry,
|
|
55
|
+
action: 'acknowledge',
|
|
56
|
+
responderReference: authenticatedPractitionerRoleReference,
|
|
57
|
+
occurredAt: serverTime,
|
|
58
|
+
})
|
|
59
|
+
const callback = prepareEmergencyResponderCallback({
|
|
60
|
+
taskEntry: acceptedTaskEntry,
|
|
61
|
+
responderReference: authenticatedPractitionerRoleReference,
|
|
62
|
+
media: 'audio',
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`transitionEmergencyTriageTask(...)` changes only the Task. It never rewrites
|
|
67
|
+
the ServiceRequest. Acknowledgement assigns the authenticated responder;
|
|
68
|
+
escalation requires a server-resolved owner and returns the Task to
|
|
69
|
+
`requested`. Callback authorization requires the accepted/in-progress Task
|
|
70
|
+
owner and returns only stable references plus the protected thread
|
|
71
|
+
correlation. The product server must still resolve the current encrypted
|
|
72
|
+
recipient endpoints and signaling transport.
|
|
73
|
+
|
|
74
|
+
The department Organization is the FHIR `Communication.sender`. Once accepted,
|
|
75
|
+
`Task.owner` is the acting `PractitionerRole`. The PractitionerRole is the authenticated transport actor
|
|
76
|
+
and call initiator, not the FHIR sender. The
|
|
77
|
+
Group remains the reply queue retained by the protected thread, never the
|
|
78
|
+
sender. The legal parent Organization remains the author of any later clinical
|
|
79
|
+
document, and its professional attester remains separate from every transport
|
|
80
|
+
coordinate.
|
|
81
|
+
|
|
48
82
|
## 3. Governed alerts
|
|
49
83
|
|
|
50
84
|
The common flow supports both `blood-donation` and `public-risk` campaigns.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Communication Bundle editor and reader
|
|
2
|
+
|
|
3
|
+
`sos-sdk-core-ts` owns the high-level extension shared unchanged by SOS, Vet
|
|
4
|
+
and UHC. It extends the existing `gdc-common-utils-ts` `BundleEditor`,
|
|
5
|
+
`BundleReader` and `CommunicationAttachedBundleSession`; it does not create a
|
|
6
|
+
parallel editor lifecycle. `fhir-data-utils-ts` owns neutral FHIR types,
|
|
7
|
+
import/materialization, validation and flat-claim projections.
|
|
8
|
+
|
|
9
|
+
The invariant is always:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
outer Bundle[type=batch, entry[]]
|
|
13
|
+
└── Communication resource.meta.claims
|
|
14
|
+
└── Communication.content-attachment-data[application/fhir+json]
|
|
15
|
+
└── inner Bundle[type=document|batch|collection, data[]]
|
|
16
|
+
├── Appointment / Observation / ... flat claims
|
|
17
|
+
└── DocumentReference[PDF | image | audio | text/calendar] flat claims
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Use `document` only when the first resource is Composition. Use `batch` only
|
|
21
|
+
when every inner entry carries its executable request. Use `collection` for an
|
|
22
|
+
unordered group such as an Appointment and its calendar attachment.
|
|
23
|
+
|
|
24
|
+
## Author
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { CommunicationBundleEditor } from 'sos-sdk-core-ts'
|
|
28
|
+
|
|
29
|
+
const outerBatch = new CommunicationBundleEditor()
|
|
30
|
+
.newCommunication()
|
|
31
|
+
.setIdentifier(communicationIdentifier)
|
|
32
|
+
.setStatus('completed')
|
|
33
|
+
.setSubject(subjectReference)
|
|
34
|
+
.setSender(departmentOrganizationReference)
|
|
35
|
+
.setRecipients(recipientReferences)
|
|
36
|
+
.setInnerBundleType('collection')
|
|
37
|
+
.addResource(appointment)
|
|
38
|
+
.addAttachmentDocument({
|
|
39
|
+
identifier: calendarDocumentIdentifier,
|
|
40
|
+
subjectReference,
|
|
41
|
+
contentType: 'text/calendar',
|
|
42
|
+
dataBase64: calendarDataBase64,
|
|
43
|
+
title: 'appointment.ics',
|
|
44
|
+
eventReferences: [appointmentReference],
|
|
45
|
+
})
|
|
46
|
+
.doneCommunication()
|
|
47
|
+
.build()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`addAttachmentDocument(...)` deliberately has no direct-Communication mode.
|
|
51
|
+
The only Communication attachment is the serialized inner Bundle. At an
|
|
52
|
+
explicit FHIR R4/R5 wire boundary, `fhir-data-utils-ts` materializes these
|
|
53
|
+
canonical claims as native `Communication.payload.contentAttachment` and native
|
|
54
|
+
inner resources; the editor itself remains release-neutral and claims-first.
|
|
55
|
+
|
|
56
|
+
## Read
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { CommunicationBundleReader } from 'sos-sdk-core-ts'
|
|
60
|
+
|
|
61
|
+
const message = new CommunicationBundleReader(receivedOuterBatch)
|
|
62
|
+
.openCommunication(0)
|
|
63
|
+
|
|
64
|
+
const appointment = message.getAppointments()[0]
|
|
65
|
+
const calendarDocument = message.getDocumentReferences()[0]
|
|
66
|
+
const sender = message.getSender()
|
|
67
|
+
const recipients = message.getRecipients()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The reader fails closed for a bare Appointment, a direct PDF/calendar/audio
|
|
71
|
+
payload, an invalid document order, a batch entry without a request, or an
|
|
72
|
+
outer resource that is not a batch Bundle.
|
|
73
|
+
|
|
74
|
+
## Boundaries
|
|
75
|
+
|
|
76
|
+
- The department Organization may be the FHIR Communication sender; the
|
|
77
|
+
authenticated professional/device remains separate transport evidence.
|
|
78
|
+
- DIDComm, ML-KEM recipient encryption, WebRTC signaling, endpoint resolution,
|
|
79
|
+
retry and persistence stay in runtime packages.
|
|
80
|
+
- GW persistence receives independently projected flat-claim resources. Direct
|
|
81
|
+
claims CRUD is an internal storage boundary, not the participant wire format.
|
|
82
|
+
- The SOS editor subclasses the established GDC Bundle editor/reader and the
|
|
83
|
+
attached-Bundle session. A future antifraud-common implementation can replace
|
|
84
|
+
that base behind this shared SOS API without migrating Vet and UHC separately.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Equipment compliance orchestration
|
|
2
|
+
|
|
3
|
+
`EquipmentComplianceManager` is the shared SOS/Vet/UHC application boundary for equipment records.
|
|
4
|
+
|
|
5
|
+
- `prepareCalibration(...)` creates a claims-first FHIR R5 graph: a completed `Procedure`, the resulting `DeviceMetric` calibration state, and an optional hashed `DocumentReference`.
|
|
6
|
+
- `prepareOperatingAuthorization(...)` creates a separate claims-first `RegulatedAuthorization` issued by the competent authority.
|
|
7
|
+
- The caller supplies an authorization adapter. The SDK does not infer permission from occupation labels; products evaluate the authenticated organization, assignment, group and jurisdictional policy.
|
|
8
|
+
- These methods prepare records only. A BFF sends them through the high-level SDK to the GW; native FHIR is materialized only at an explicit export boundary.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const equipmentCompliance = new EquipmentComplianceManager({
|
|
12
|
+
authorize: action => authorizationPolicy.allows(authenticatedAssignment, action),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const calibrationRecords = equipmentCompliance.prepareCalibration(calibrationInput, {
|
|
16
|
+
actorReference: authenticatedAssignment.practitionerRoleReference,
|
|
17
|
+
departmentReference: authenticatedAssignment.departmentReference,
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A `Device` is an optional `Schedule.actor` only when the service capacity really
|
|
22
|
+
depends on that specific machine, such as an apheresis station. Most schedules
|
|
23
|
+
need only the `HealthcareService` and independently reservable `Location` or
|
|
24
|
+
professional capacity. Device failure is an equipment event/state change; the
|
|
25
|
+
booking workflow separately marks only the affected future slots unavailable
|
|
26
|
+
and reconciles their appointments. It must not cancel unrelated schedules.
|
|
27
|
+
|
|
28
|
+
## Deferred blood-bank product work
|
|
29
|
+
|
|
30
|
+
The following is backlog for Vet, UHC and SOS products when an organization has
|
|
31
|
+
a blood-bank or blood-donation department. It is not part of this SDK release:
|
|
32
|
+
|
|
33
|
+
- product policy for trained nurses and Canadian `Donor Care Associate`
|
|
34
|
+
assignments to record governed calibration checks;
|
|
35
|
+
- department panels for calibrating apheresis machines and donation scales;
|
|
36
|
+
- collection-sample labels and blood/plasma product labels using the applicable
|
|
37
|
+
ISBT 128 data structures and official ICCBBA tables;
|
|
38
|
+
- separate `Specimen` and `BiologicallyDerivedProduct` projections, scanning,
|
|
39
|
+
printing, chain-of-identity and audit tests.
|
|
40
|
+
|
|
41
|
+
`Donor Care Associate` is retained as the employer/assignment role and is not
|
|
42
|
+
silently mapped to nurse or nursing assistant. Authorization depends on the
|
|
43
|
+
verified assignment, training and department policy.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sos-sdk-core-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Runtime-neutral shared orchestration for SOS, Vet and UHC products",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Connecting Solution & Applications Ltd",
|
|
@@ -35,6 +35,14 @@
|
|
|
35
35
|
"./secure-message-crypto": {
|
|
36
36
|
"types": "./dist/secure-message-crypto.d.ts",
|
|
37
37
|
"default": "./dist/secure-message-crypto.js"
|
|
38
|
+
},
|
|
39
|
+
"./equipment-compliance-orchestration": {
|
|
40
|
+
"types": "./dist/equipment-compliance-orchestration.d.ts",
|
|
41
|
+
"default": "./dist/equipment-compliance-orchestration.js"
|
|
42
|
+
},
|
|
43
|
+
"./communication-bundle": {
|
|
44
|
+
"types": "./dist/communication-bundle.d.ts",
|
|
45
|
+
"default": "./dist/communication-bundle.js"
|
|
38
46
|
}
|
|
39
47
|
},
|
|
40
48
|
"files": [
|
|
@@ -55,8 +63,9 @@
|
|
|
55
63
|
"@noble/ciphers": "2.4.0",
|
|
56
64
|
"@noble/hashes": "2.4.0",
|
|
57
65
|
"@noble/post-quantum": "0.7.1",
|
|
58
|
-
"fhir-data-utils-ts": "0.
|
|
59
|
-
"
|
|
66
|
+
"fhir-data-utils-ts": "0.6.0",
|
|
67
|
+
"gdc-common-utils-ts": "2.9.21",
|
|
68
|
+
"sos-data-utils-ts": "0.5.1"
|
|
60
69
|
},
|
|
61
70
|
"devDependencies": {
|
|
62
71
|
"@types/node": "^22.5.0",
|