gdc-common-utils-ts 1.14.14 → 1.14.15
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/README.md +14 -7
- package/dist/examples/communication-attached-bundle-session.d.ts +22 -0
- package/dist/examples/communication-attached-bundle-session.js +79 -0
- package/dist/examples/index.d.ts +1 -1
- package/dist/examples/index.js +1 -1
- package/dist/examples/ips-bundle.js +3 -3
- package/dist/models/bundle.d.ts +20 -2
- package/dist/models/bundle.js +8 -1
- package/dist/models/comm.d.ts +29 -0
- package/dist/models/confidential-message.d.ts +30 -2
- package/dist/models/interoperable-claims/diagnostic-report-claims.d.ts +1 -1
- package/dist/models/interoperable-claims/diagnostic-report-claims.js +1 -1
- package/dist/utils/communication-attached-bundle-session.d.ts +191 -0
- package/dist/utils/communication-attached-bundle-session.js +542 -0
- package/dist/utils/employee.d.ts +12 -0
- package/dist/utils/employee.js +22 -2
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,14 @@ boundaries used in `gdc-common-utils-ts`.
|
|
|
27
27
|
- FHIR SearchParameter names must use canonical FHIR naming (lowercase, with `-` when defined by FHIR).
|
|
28
28
|
- Never use invented camelCase parameter names for FHIR claims/search keys (example: `Communication.part-of` is valid, `Communication.partOf` is not).
|
|
29
29
|
- Only define custom names when no canonical FHIR SearchParameter exists.
|
|
30
|
-
- `resource.meta.claims` is the canonical claims container and must be preserved across conversions/transports.
|
|
30
|
+
- `resource.meta.claims` is the canonical project-specific claims container and must be preserved across conversions/transports.
|
|
31
|
+
- `resource.meta.claims` is not part of base FHIR; it is a claims-first extension carried by FHIR-like resources in GDC contracts.
|
|
32
|
+
|
|
33
|
+
If you need the canonical explanation of how DIDComm envelope, batch body,
|
|
34
|
+
entry types, FHIR-like resources, and `resource.meta.claims` fit together,
|
|
35
|
+
read first:
|
|
36
|
+
|
|
37
|
+
- [`docs/101-COMMUNICATION_LAYERING.md`](docs/101-COMMUNICATION_LAYERING.md)
|
|
31
38
|
|
|
32
39
|
## Install
|
|
33
40
|
|
|
@@ -148,12 +155,12 @@ import { JweObject, JwtCompactParts } from 'gdc-common-utils-ts/models';
|
|
|
148
155
|
- [docs/DATASPACE_DISCOVERY_ROADMAP.md](docs/DATASPACE_DISCOVERY_ROADMAP.md)
|
|
149
156
|
- cross-repo contract for dataspace discovery semantics, EU coverage
|
|
150
157
|
inference, shared DTOs, and parameterized examples
|
|
151
|
-
- [docs/
|
|
158
|
+
- [docs/101-DATASPACE_DISCOVERY_DEFAULTS.md](docs/101-DATASPACE_DISCOVERY_DEFAULTS.md)
|
|
152
159
|
- portal/backend bootstrap guide for `defaults-only`, `default-first`, and
|
|
153
160
|
`internet-first` discovery seeding by `jurisdiction + version + networkType`
|
|
154
161
|
- [docs/consent-access-matrix-task.md](docs/consent-access-matrix-task.md)
|
|
155
162
|
- next-step design/task document for active consent aggregation, explicit deny precedence, controller views, permission-request communications, and SMART access evaluation
|
|
156
|
-
- [docs/
|
|
163
|
+
- [docs/101-IPS_BUNDLE.md](docs/101-IPS_BUNDLE.md)
|
|
157
164
|
- canonical 101 for requesting IPS, editing IPS-style bundles in `Communication.content-attachment-data`, and reading resources by section
|
|
158
165
|
|
|
159
166
|
## Dataspace Protocol And Discovery
|
|
@@ -177,7 +184,7 @@ Main entry points:
|
|
|
177
184
|
- [`src/examples/dataspace-discovery.ts`](src/examples/dataspace-discovery.ts)
|
|
178
185
|
- synthetic provider/operator examples that distinguish discovery URL from
|
|
179
186
|
derived catalog artifact URL
|
|
180
|
-
- [`docs/
|
|
187
|
+
- [`docs/101-DATASPACE_DISCOVERY_DEFAULTS.md`](docs/101-DATASPACE_DISCOVERY_DEFAULTS.md)
|
|
181
188
|
- copy/paste backend bootstrap guide for portal `default-first` rollout
|
|
182
189
|
- [`__tests__/dataspace-discovery-defaults.101.test.ts`](__tests__/dataspace-discovery-defaults.101.test.ts)
|
|
183
190
|
- executable defaults-registry examples for ICAs, hosting operators, and
|
|
@@ -267,7 +274,7 @@ The canonical API contract should live in JSDoc on exported code. The README act
|
|
|
267
274
|
- This is the preferred first scope to teach when the backend only needs subject-scoped read access.
|
|
268
275
|
- [`getOrganizationCredentialFromVpToken(...)`, `getLegalRepresentativeCredentialFromVpToken(...)`](src/utils/vp-token.ts)
|
|
269
276
|
- Extract typed VC objects from a VP token when GW/SDK flows carry canonical proof only in `vp_token`.
|
|
270
|
-
- [`docs/
|
|
277
|
+
- [`docs/101-VP_TOKEN.md`](docs/101-VP_TOKEN.md)
|
|
271
278
|
- Step-by-step guide for building the canonical compact `vp_token` string from organization and representative VCs.
|
|
272
279
|
- [`validateCommunicationResourceFhirR4(...)`](src/utils/communication-fhir-r4.ts)
|
|
273
280
|
- Validates FHIR R4 `Communication` resources.
|
|
@@ -336,9 +343,9 @@ The canonical API contract should live in JSDoc on exported code. The README act
|
|
|
336
343
|
- [`src/examples/shared.ts`](src/examples/shared.ts)
|
|
337
344
|
- Shared route contexts, controller binding fragments, and reusable helper builders.
|
|
338
345
|
- `tenantId` is modeled as an identifier-like route token (`acme-id`), not as a friendly alternate name.
|
|
339
|
-
- [`docs/
|
|
346
|
+
- [`docs/101-LIFECYCLE.md`](docs/101-LIFECYCLE.md)
|
|
340
347
|
- Copy/paste lifecycle `101` guide with semantic rules and reusable placeholders.
|
|
341
|
-
- [`docs/
|
|
348
|
+
- [`docs/101-HEALTHCARE_ROLES_I18N.md`](docs/101-HEALTHCARE_ROLES_I18N.md)
|
|
342
349
|
- Sector-aware healthcare role catalog and i18n `101` (ISCO-08 + HL7) for FE/BE onboarding.
|
|
343
350
|
|
|
344
351
|
## Documentation Naming Rules
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BundleEntry, BundleJsonApi } from '../models/bundle';
|
|
2
|
+
/**
|
|
3
|
+
* First developer use case:
|
|
4
|
+
* - edit a Consent entry inside a Communication-attached Bundle
|
|
5
|
+
* - keep active entry in memory
|
|
6
|
+
* - serialize full Bundle to Communication.content-attachment-data on save
|
|
7
|
+
* - release active entry memory when finished
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildConsentEditingCommunicationSessionExample(): {
|
|
10
|
+
communicationClaims: Record<string, unknown>;
|
|
11
|
+
bundleInMemory: BundleJsonApi<BundleEntry>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Second developer use case:
|
|
15
|
+
* - edit MedicationStatement claims to drive a health view
|
|
16
|
+
* - keep bundle attached to Communication as base64 payload
|
|
17
|
+
* - apply save/release lifecycle consistently
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildMedicationEditingCommunicationSessionExample(): {
|
|
20
|
+
communicationClaims: Record<string, unknown>;
|
|
21
|
+
bundleInMemory: BundleJsonApi<BundleEntry>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Copyright 2026 Conectate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
|
|
2
|
+
// Always create JSDoc, do not use strings inline in keys nor values, use types instead, and reuse the data test examples.
|
|
3
|
+
import { HealthcareBasicSections } from '../constants/healthcare.js';
|
|
4
|
+
import { CommunicationCategoryCodes } from '../constants/communication.js';
|
|
5
|
+
import { MedicationStatementClaim } from '../models/interoperable-claims/medication-statement-claims.js';
|
|
6
|
+
import { EXAMPLE_COMMUNICATION_IDENTIFIER, EXAMPLE_CONSENT_DATE, EXAMPLE_CONSENT_IDENTIFIER, EXAMPLE_CONSENT_PERIOD_END, EXAMPLE_CONSENT_PERIOD_START, EXAMPLE_CONSENT_PURPOSE_TREATMENT, EXAMPLE_EMAIL_PROFESSIONAL, EXAMPLE_HEALTHCARE_ACTOR_ROLE_PHYSICIAN, EXAMPLE_IPS_BUNDLE_NOTE_TEXT, EXAMPLE_MEDICATION_STATEMENT_IDENTIFIER, EXAMPLE_MEDICATION_STATEMENT_STATUS, EXAMPLE_MEDICATION_STATEMENT_TEXT, EXAMPLE_SUBJECT_DID, } from './shared.js';
|
|
7
|
+
import { CommunicationAttachedBundleSession } from '../utils/communication-attached-bundle-session.js';
|
|
8
|
+
import { setCommunicationCategory, setCommunicationIdentifier, setCommunicationSubject, setCommunicationText, } from '../claims/claims-helpers-communication.js';
|
|
9
|
+
import { setActorIdentifierList, setActorRoleList, setConsentDate, setConsentDecision, setConsentIdentifier, setConsentPeriodEnd, setConsentPeriodStart, setConsentSubject, setPurposeList, setSectionList, } from '../claims/claims-helpers-consent.js';
|
|
10
|
+
/**
|
|
11
|
+
* First developer use case:
|
|
12
|
+
* - edit a Consent entry inside a Communication-attached Bundle
|
|
13
|
+
* - keep active entry in memory
|
|
14
|
+
* - serialize full Bundle to Communication.content-attachment-data on save
|
|
15
|
+
* - release active entry memory when finished
|
|
16
|
+
*/
|
|
17
|
+
export function buildConsentEditingCommunicationSessionExample() {
|
|
18
|
+
let communicationClaims = { '@context': 'org.hl7.fhir.r4' };
|
|
19
|
+
communicationClaims = setCommunicationIdentifier(communicationClaims, EXAMPLE_COMMUNICATION_IDENTIFIER);
|
|
20
|
+
communicationClaims = setCommunicationSubject(communicationClaims, EXAMPLE_SUBJECT_DID);
|
|
21
|
+
communicationClaims = setCommunicationCategory(communicationClaims, CommunicationCategoryCodes.Notification.attributeValue);
|
|
22
|
+
communicationClaims = setCommunicationText(communicationClaims, EXAMPLE_IPS_BUNDLE_NOTE_TEXT);
|
|
23
|
+
const bundleEditor = new CommunicationAttachedBundleSession({
|
|
24
|
+
communicationClaims,
|
|
25
|
+
});
|
|
26
|
+
let consentClaims = { '@context': 'org.hl7.fhir.api' };
|
|
27
|
+
consentClaims = setConsentDecision(consentClaims, 'permit');
|
|
28
|
+
consentClaims = setConsentSubject(consentClaims, EXAMPLE_SUBJECT_DID);
|
|
29
|
+
consentClaims = setConsentIdentifier(consentClaims, EXAMPLE_CONSENT_IDENTIFIER);
|
|
30
|
+
consentClaims = setConsentDate(consentClaims, EXAMPLE_CONSENT_DATE);
|
|
31
|
+
consentClaims = setConsentPeriodStart(consentClaims, EXAMPLE_CONSENT_PERIOD_START);
|
|
32
|
+
consentClaims = setConsentPeriodEnd(consentClaims, EXAMPLE_CONSENT_PERIOD_END);
|
|
33
|
+
consentClaims = setPurposeList(consentClaims, [EXAMPLE_CONSENT_PURPOSE_TREATMENT]);
|
|
34
|
+
consentClaims = setSectionList(consentClaims, [
|
|
35
|
+
HealthcareBasicSections.AllergiesAndIntolerances.attributeValue,
|
|
36
|
+
]);
|
|
37
|
+
consentClaims = setActorIdentifierList(consentClaims, [EXAMPLE_EMAIL_PROFESSIONAL]);
|
|
38
|
+
consentClaims = setActorRoleList(consentClaims, [EXAMPLE_HEALTHCARE_ACTOR_ROLE_PHYSICIAN]);
|
|
39
|
+
bundleEditor.upsertActiveConsentEntry({
|
|
40
|
+
claims: consentClaims,
|
|
41
|
+
fullUrl: `urn:uuid:${EXAMPLE_CONSENT_IDENTIFIER}`,
|
|
42
|
+
});
|
|
43
|
+
bundleEditor.saveAndReleaseActiveEntry();
|
|
44
|
+
return {
|
|
45
|
+
communicationClaims: bundleEditor.getCommunicationClaims(),
|
|
46
|
+
bundleInMemory: bundleEditor.getBundleInMemory(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Second developer use case:
|
|
51
|
+
* - edit MedicationStatement claims to drive a health view
|
|
52
|
+
* - keep bundle attached to Communication as base64 payload
|
|
53
|
+
* - apply save/release lifecycle consistently
|
|
54
|
+
*/
|
|
55
|
+
export function buildMedicationEditingCommunicationSessionExample() {
|
|
56
|
+
let communicationClaims = { '@context': 'org.hl7.fhir.r4' };
|
|
57
|
+
communicationClaims = setCommunicationIdentifier(communicationClaims, EXAMPLE_COMMUNICATION_IDENTIFIER);
|
|
58
|
+
communicationClaims = setCommunicationSubject(communicationClaims, EXAMPLE_SUBJECT_DID);
|
|
59
|
+
communicationClaims = setCommunicationCategory(communicationClaims, CommunicationCategoryCodes.Reminder.attributeValue);
|
|
60
|
+
communicationClaims = setCommunicationText(communicationClaims, EXAMPLE_IPS_BUNDLE_NOTE_TEXT);
|
|
61
|
+
const bundleEditor = new CommunicationAttachedBundleSession({
|
|
62
|
+
communicationClaims,
|
|
63
|
+
});
|
|
64
|
+
bundleEditor.upsertActiveMedicationStatementEntry({
|
|
65
|
+
claims: {
|
|
66
|
+
'@context': 'org.hl7.fhir.api',
|
|
67
|
+
[MedicationStatementClaim.Identifier]: EXAMPLE_MEDICATION_STATEMENT_IDENTIFIER,
|
|
68
|
+
[MedicationStatementClaim.Subject]: EXAMPLE_SUBJECT_DID,
|
|
69
|
+
[MedicationStatementClaim.Status]: EXAMPLE_MEDICATION_STATEMENT_STATUS,
|
|
70
|
+
[MedicationStatementClaim.MedicationText]: EXAMPLE_MEDICATION_STATEMENT_TEXT,
|
|
71
|
+
},
|
|
72
|
+
fullUrl: `urn:uuid:${EXAMPLE_MEDICATION_STATEMENT_IDENTIFIER}`,
|
|
73
|
+
});
|
|
74
|
+
bundleEditor.saveAndReleaseActiveEntry();
|
|
75
|
+
return {
|
|
76
|
+
communicationClaims: bundleEditor.getCommunicationClaims(),
|
|
77
|
+
bundleInMemory: bundleEditor.getBundleInMemory(),
|
|
78
|
+
};
|
|
79
|
+
}
|
package/dist/examples/index.d.ts
CHANGED
|
@@ -12,6 +12,6 @@ export * from './frontend-session';
|
|
|
12
12
|
export * from './lifecycle';
|
|
13
13
|
export * from './api-flow-examples';
|
|
14
14
|
export * from './contract-examples';
|
|
15
|
-
export * from './communication-bundle-session';
|
|
15
|
+
export * from './communication-attached-bundle-session';
|
|
16
16
|
export * from './communication-bundle-document-request';
|
|
17
17
|
export * from './ips-bundle';
|
package/dist/examples/index.js
CHANGED
|
@@ -12,6 +12,6 @@ export * from './frontend-session.js';
|
|
|
12
12
|
export * from './lifecycle.js';
|
|
13
13
|
export * from './api-flow-examples.js';
|
|
14
14
|
export * from './contract-examples.js';
|
|
15
|
-
export * from './communication-bundle-session.js';
|
|
15
|
+
export * from './communication-attached-bundle-session.js';
|
|
16
16
|
export * from './communication-bundle-document-request.js';
|
|
17
17
|
export * from './ips-bundle.js';
|
|
@@ -8,13 +8,13 @@ import { ConditionClaim } from '../models/interoperable-claims/condition-claims.
|
|
|
8
8
|
import { MedicationStatementClaim } from '../models/interoperable-claims/medication-statement-claims.js';
|
|
9
9
|
import { EXAMPLE_COMMUNICATION_IDENTIFIER, EXAMPLE_DOCUMENT_REFERENCE_CONTENT_TYPE_PDF, EXAMPLE_DOCUMENT_REFERENCE_DATE, EXAMPLE_DOCUMENT_REFERENCE_DESCRIPTION, EXAMPLE_DOCUMENT_REFERENCE_IDENTIFIER, EXAMPLE_DOCUMENT_REFERENCE_URL, EXAMPLE_IPS_BUNDLE_NOTE_TEXT, EXAMPLE_MEDICATION_STATEMENT_IDENTIFIER, EXAMPLE_MEDICATION_STATEMENT_STATUS, EXAMPLE_MEDICATION_STATEMENT_TEXT, EXAMPLE_SUBJECT_DID, } from './shared.js';
|
|
10
10
|
import { toClinicalResourceCardViews, toClinicalResourceCommonViews, } from '../utils/clinical-resource-view.js';
|
|
11
|
-
import {
|
|
11
|
+
import { CommunicationAttachedBundleSession } from '../utils/communication-attached-bundle-session.js';
|
|
12
12
|
/**
|
|
13
13
|
* Builds a minimal IPS-like history bundle with common clinical resource types
|
|
14
14
|
* authored as `resource.meta.claims`.
|
|
15
15
|
*/
|
|
16
16
|
export function buildIpsClinicalHistoryBundleExample() {
|
|
17
|
-
const bundleEditor = new
|
|
17
|
+
const bundleEditor = new CommunicationAttachedBundleSession({
|
|
18
18
|
communicationClaims: {
|
|
19
19
|
'@context': 'org.hl7.fhir.r4',
|
|
20
20
|
[CommunicationClaim.Identifier]: EXAMPLE_COMMUNICATION_IDENTIFIER,
|
|
@@ -81,7 +81,7 @@ export function buildIpsClinicalHistoryBundleExample() {
|
|
|
81
81
|
*/
|
|
82
82
|
export function buildIpsBundleFrontCardsExample() {
|
|
83
83
|
const { communicationClaims, bundleInMemory } = buildIpsClinicalHistoryBundleExample();
|
|
84
|
-
const bundleEditor = new
|
|
84
|
+
const bundleEditor = new CommunicationAttachedBundleSession({
|
|
85
85
|
communicationClaims,
|
|
86
86
|
initialBundle: bundleInMemory,
|
|
87
87
|
});
|
package/dist/models/bundle.d.ts
CHANGED
|
@@ -16,16 +16,28 @@
|
|
|
16
16
|
* 2. **`BundleEntry`**: The core component of a Bundle. Represents a single unit of work,
|
|
17
17
|
* such as registering one organization or creating one employee. It has a strict
|
|
18
18
|
* structure:
|
|
19
|
-
* - `type`: A string that defines the business
|
|
19
|
+
* - `type`: A string that defines the internal business/message kind
|
|
20
|
+
* (e.g., 'Organization-registration-offer-v1.0').
|
|
20
21
|
* - `resource.meta.claims`: Canonical location for claims in request entries.
|
|
21
22
|
* - `meta.claims`: Legacy compatibility location accepted during migration.
|
|
22
23
|
* - `resource`: A FHIR-like resource object that is the subject of the action.
|
|
23
24
|
* - `request`/`response`: Contextual objects indicating the operation's details or result.
|
|
25
|
+
*
|
|
26
|
+
* Important distinction:
|
|
27
|
+
* - `BundleEntry.type` is not a FHIR field
|
|
28
|
+
* - `BundleEntry.resource.resourceType` is the actual FHIR-like resource type
|
|
29
|
+
* - `BundleJsonApi.type` is the batch container type
|
|
30
|
+
* - DIDComm envelope `type` is yet another outer field
|
|
24
31
|
*/
|
|
25
32
|
import { OperationOutcome } from "./operation-outcome";
|
|
26
33
|
/**
|
|
27
34
|
* Defines the `request` property for an entry in a request Bundle.
|
|
28
35
|
* This indicates the intended action for the entry.
|
|
36
|
+
*
|
|
37
|
+
* FHIR note:
|
|
38
|
+
* - this is the closest layer to FHIR batch semantics
|
|
39
|
+
* - `request.method` and `request.url` describe the operation
|
|
40
|
+
* - `BundleEntry.type` remains a separate internal business/message label
|
|
29
41
|
*/
|
|
30
42
|
export interface BundleRequest {
|
|
31
43
|
method: 'POST' | 'PUT' | 'DELETE' | 'GET';
|
|
@@ -131,7 +143,8 @@ export interface ErrorEntry {
|
|
|
131
143
|
* Represents a single, canonical entry within a `BundleJsonApi`.
|
|
132
144
|
* This structure is used for both requests and successful responses.
|
|
133
145
|
*
|
|
134
|
-
* @property {string} type -
|
|
146
|
+
* @property {string} type - Internal business/message kind for this entry.
|
|
147
|
+
* This is not the FHIR `resourceType`.
|
|
135
148
|
* @property {object} resource - The primary FHIR-like resource that is the subject of the action.
|
|
136
149
|
* Canonical claims location is `resource.meta.claims`.
|
|
137
150
|
* @property {BundleEntryMeta} [meta] - Legacy top-level claims location for backward compatibility.
|
|
@@ -153,6 +166,11 @@ export type BundleEntry = {
|
|
|
153
166
|
* The generic type `T` allows for specifying whether the `data` array contains request
|
|
154
167
|
* entries or response entries, providing strong type safety.
|
|
155
168
|
*
|
|
169
|
+
* Layering note:
|
|
170
|
+
* - this is the business batch container
|
|
171
|
+
* - it is often placed inside the `body` of a DIDComm/FAPI envelope
|
|
172
|
+
* - entries inside it carry FHIR-like resources plus internal business labels
|
|
173
|
+
*
|
|
156
174
|
* @example
|
|
157
175
|
* const requestBundle: BundleJsonApi<BundleEntry> = { ... };
|
|
158
176
|
* const responseBundle: BundleJsonApi<BundleEntry | ErrorEntry> = { ... };
|
package/dist/models/bundle.js
CHANGED
|
@@ -16,10 +16,17 @@
|
|
|
16
16
|
* 2. **`BundleEntry`**: The core component of a Bundle. Represents a single unit of work,
|
|
17
17
|
* such as registering one organization or creating one employee. It has a strict
|
|
18
18
|
* structure:
|
|
19
|
-
* - `type`: A string that defines the business
|
|
19
|
+
* - `type`: A string that defines the internal business/message kind
|
|
20
|
+
* (e.g., 'Organization-registration-offer-v1.0').
|
|
20
21
|
* - `resource.meta.claims`: Canonical location for claims in request entries.
|
|
21
22
|
* - `meta.claims`: Legacy compatibility location accepted during migration.
|
|
22
23
|
* - `resource`: A FHIR-like resource object that is the subject of the action.
|
|
23
24
|
* - `request`/`response`: Contextual objects indicating the operation's details or result.
|
|
25
|
+
*
|
|
26
|
+
* Important distinction:
|
|
27
|
+
* - `BundleEntry.type` is not a FHIR field
|
|
28
|
+
* - `BundleEntry.resource.resourceType` is the actual FHIR-like resource type
|
|
29
|
+
* - `BundleJsonApi.type` is the batch container type
|
|
30
|
+
* - DIDComm envelope `type` is yet another outer field
|
|
24
31
|
*/
|
|
25
32
|
export {};
|
package/dist/models/comm.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Represents the standard payload of a DIDComm v2 message.
|
|
3
|
+
*
|
|
4
|
+
* Layering rule:
|
|
5
|
+
* - `type` here is the DIDComm/protocol envelope type
|
|
6
|
+
* - it is not the FHIR `resourceType`
|
|
7
|
+
* - it is not the internal GW batch-entry business type
|
|
8
|
+
* - `body` carries the next business layer (often a `BundleJsonApi`)
|
|
9
|
+
*
|
|
10
|
+
* Read together with:
|
|
11
|
+
* - `docs/101-COMMUNICATION_LAYERING.md`
|
|
12
|
+
* - `src/models/bundle.ts`
|
|
13
|
+
*
|
|
3
14
|
* @see https://identity.foundation/didcomm-messaging/spec/v2.0/#plaintext-message-structure
|
|
4
15
|
*/
|
|
5
16
|
export interface DidCommPayload {
|
|
@@ -18,6 +29,11 @@ export interface DidCommPayload {
|
|
|
18
29
|
/**
|
|
19
30
|
* Represents a data entry in the `body` of a CommMsgExtended,
|
|
20
31
|
* following a hybrid JSON:API and FHIR structure.
|
|
32
|
+
*
|
|
33
|
+
* Important:
|
|
34
|
+
* - `type` is an internal entry/data kind such as `Reference` or `Attachment`
|
|
35
|
+
* - it is not the DIDComm envelope type
|
|
36
|
+
* - it is not a FHIR `resourceType`
|
|
21
37
|
*/
|
|
22
38
|
export interface DataEntry {
|
|
23
39
|
id: string;
|
|
@@ -44,6 +60,19 @@ export interface DidCommAttachment {
|
|
|
44
60
|
/**
|
|
45
61
|
* The canonical, internal representation of a secure message, extending
|
|
46
62
|
* the standard DIDComm payload with FHIR-specific, flattened metadata.
|
|
63
|
+
*
|
|
64
|
+
* Intended usage:
|
|
65
|
+
* - internal GW/runtime representation
|
|
66
|
+
* - derived from FHIR-like `Communication` payloads when needed
|
|
67
|
+
*
|
|
68
|
+
* Not intended as:
|
|
69
|
+
* - the primary client-facing payload new integrators should author first
|
|
70
|
+
*
|
|
71
|
+
* New client code should usually send:
|
|
72
|
+
* - DIDComm/FAPI-style envelope
|
|
73
|
+
* - carrying a batch body
|
|
74
|
+
* - carrying FHIR-like resources such as `Communication`
|
|
75
|
+
* - with canonical business semantics in `resource.meta.claims`
|
|
47
76
|
*/
|
|
48
77
|
export interface CommMsgExtended extends DidCommPayload {
|
|
49
78
|
body: {
|
|
@@ -29,6 +29,15 @@ export interface DidCommDecodedMetadata {
|
|
|
29
29
|
* Represents the plaintext of a decoded DIDComm message.
|
|
30
30
|
* This is the core business-level "input" for a job.
|
|
31
31
|
* For FAPI compliance, this entire object is typically the payload of a signed JWS.
|
|
32
|
+
*
|
|
33
|
+
* Layering rule:
|
|
34
|
+
* - `type` is the outer envelope/protocol type
|
|
35
|
+
* - `body` carries the business payload
|
|
36
|
+
* - that `body` is often a batch container such as `BundleJsonApi`
|
|
37
|
+
* - resources inside that batch may be FHIR-like resources with
|
|
38
|
+
* `resource.meta.claims`
|
|
39
|
+
*
|
|
40
|
+
* This object is not itself a FHIR resource.
|
|
32
41
|
*/
|
|
33
42
|
export interface IDecodedDidcommPayload {
|
|
34
43
|
/** Relevant information available through the decryption and verification process */
|
|
@@ -66,10 +75,24 @@ export interface IDecodedDidcommPayload {
|
|
|
66
75
|
from?: string;
|
|
67
76
|
/**
|
|
68
77
|
* The Message Type URI, identifying the type of data in the body or protocol used.
|
|
69
|
-
*
|
|
78
|
+
*
|
|
79
|
+
* Examples:
|
|
80
|
+
* - DIDComm/business envelope type
|
|
81
|
+
* - transport protocol hint
|
|
82
|
+
*
|
|
83
|
+
* This is not:
|
|
84
|
+
* - a FHIR `resourceType`
|
|
85
|
+
* - a GW batch-entry business type
|
|
70
86
|
*/
|
|
71
87
|
type: string;
|
|
72
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* The main business payload of the message.
|
|
90
|
+
*
|
|
91
|
+
* In GW/SDK flows this is often:
|
|
92
|
+
* - a hybrid batch container (`BundleJsonApi`)
|
|
93
|
+
* - carrying entries
|
|
94
|
+
* - carrying FHIR-like resources
|
|
95
|
+
*/
|
|
73
96
|
body: any;
|
|
74
97
|
}
|
|
75
98
|
/**
|
|
@@ -89,6 +112,11 @@ export interface CommDataEntry {
|
|
|
89
112
|
/**
|
|
90
113
|
* The canonical, internal representation of a secure message, extending
|
|
91
114
|
* the standard DIDComm payload with FHIR-specific, flattened metadata.
|
|
115
|
+
*
|
|
116
|
+
* Internal model note:
|
|
117
|
+
* - this is a gateway/runtime representation
|
|
118
|
+
* - it is not the main payload shape that new SDK/frontend/backend integrators
|
|
119
|
+
* should author directly
|
|
92
120
|
*/
|
|
93
121
|
export interface ICommPayloadExtended extends IDecodedDidcommPayload {
|
|
94
122
|
body: {
|
|
@@ -91,6 +91,6 @@ export declare const DiagnosticReportSearchParamToClaimKey: Record<DiagnosticRep
|
|
|
91
91
|
*
|
|
92
92
|
* Keep `DiagnosticReport` paused at the typing/mapping layer until:
|
|
93
93
|
* 1. `claims-helpers-diagnostic-report.ts` exists
|
|
94
|
-
* 2. `
|
|
94
|
+
* 2. `CommunicationAttachedBundleSession.upsertActiveDiagnosticReportEntry(...)` exists
|
|
95
95
|
* 3. GW Core readers/tests consume the same shared claim keys
|
|
96
96
|
*/
|
|
@@ -100,6 +100,6 @@ export const DiagnosticReportSearchParamToClaimKey = {
|
|
|
100
100
|
*
|
|
101
101
|
* Keep `DiagnosticReport` paused at the typing/mapping layer until:
|
|
102
102
|
* 1. `claims-helpers-diagnostic-report.ts` exists
|
|
103
|
-
* 2. `
|
|
103
|
+
* 2. `CommunicationAttachedBundleSession.upsertActiveDiagnosticReportEntry(...)` exists
|
|
104
104
|
* 3. GW Core readers/tests consume the same shared claim keys
|
|
105
105
|
*/
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { BundleEntry, BundleJsonApi, BundleRequest } from '../models/bundle';
|
|
2
|
+
import { type BundleResourceIdFilters } from './bundle-query';
|
|
3
|
+
import { type MedicationStatementClaimsFlat } from '../models/interoperable-claims/medication-statement-claims';
|
|
4
|
+
export type CommunicationAttachedBundleSessionMode = 'strict' | 'normalize';
|
|
5
|
+
export type CommunicationAttachedBundleSessionOptions = Readonly<{
|
|
6
|
+
communicationClaims?: Record<string, unknown>;
|
|
7
|
+
initialBundle?: BundleJsonApi<BundleEntry>;
|
|
8
|
+
mode?: CommunicationAttachedBundleSessionMode;
|
|
9
|
+
}>;
|
|
10
|
+
export type ActiveEntrySelection = Readonly<{
|
|
11
|
+
index?: number;
|
|
12
|
+
fullUrl?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export type UpsertEntryInput = Readonly<{
|
|
15
|
+
resourceType: string;
|
|
16
|
+
claims: Record<string, unknown>;
|
|
17
|
+
type?: string;
|
|
18
|
+
fullUrl?: string;
|
|
19
|
+
request?: BundleRequest;
|
|
20
|
+
}>;
|
|
21
|
+
export type AddContainedDocumentToActiveEntryInput = Readonly<{
|
|
22
|
+
identifier?: string;
|
|
23
|
+
fullUrl?: string;
|
|
24
|
+
claims?: Record<string, unknown>;
|
|
25
|
+
attachmentContentType?: string;
|
|
26
|
+
attachmentDataBase64?: string;
|
|
27
|
+
attachmentUrl?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
date?: string;
|
|
30
|
+
language?: string;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Communication editing session with bundle-in-memory as source of truth.
|
|
34
|
+
*
|
|
35
|
+
* Design contract:
|
|
36
|
+
* - `activeEntry` is the real editing unit (not only `activeResource`), because
|
|
37
|
+
* it can include `fullUrl`, `request`, and entry-level context.
|
|
38
|
+
* - `Communication.content-attachment-data` is always derived from the
|
|
39
|
+
* in-memory bundle after each committed update.
|
|
40
|
+
* - saving can release active entry memory via `saveAndReleaseActiveEntry()`.
|
|
41
|
+
*/
|
|
42
|
+
export declare class CommunicationAttachedBundleSession {
|
|
43
|
+
private communicationClaims;
|
|
44
|
+
private bundleInMemory;
|
|
45
|
+
private activeEntryIndex;
|
|
46
|
+
private mode;
|
|
47
|
+
constructor(options?: CommunicationAttachedBundleSessionOptions);
|
|
48
|
+
/** Returns a deep copy of communication claims. */
|
|
49
|
+
getCommunicationClaims(): Record<string, unknown>;
|
|
50
|
+
/** Returns a deep copy of the current in-memory bundle. */
|
|
51
|
+
getBundleInMemory(): BundleJsonApi<BundleEntry>;
|
|
52
|
+
/** Returns the active entry index, or null when no entry is selected. */
|
|
53
|
+
getActiveEntryIndex(): number | null;
|
|
54
|
+
/** Returns a deep copy of the active entry when selected. */
|
|
55
|
+
getActiveEntry(): BundleEntry | null;
|
|
56
|
+
/** Selects an active entry by index or fullUrl. */
|
|
57
|
+
selectActiveEntry(selection: ActiveEntrySelection): this;
|
|
58
|
+
/** Clears active entry selection from memory. */
|
|
59
|
+
clearActiveEntry(): this;
|
|
60
|
+
/**
|
|
61
|
+
* Upserts an entry in bundle memory and marks it as active.
|
|
62
|
+
* Matching priority: `fullUrl` if present, then resource claim identifier.
|
|
63
|
+
*/
|
|
64
|
+
upsertActiveEntry(input: UpsertEntryInput): this;
|
|
65
|
+
/**
|
|
66
|
+
* Consent-first helper for developer onboarding.
|
|
67
|
+
*
|
|
68
|
+
* Expected keys should come from `ClaimConsent` in caller code.
|
|
69
|
+
*/
|
|
70
|
+
upsertActiveConsentEntry(input: Readonly<{
|
|
71
|
+
claims: Record<string, unknown>;
|
|
72
|
+
fullUrl?: string;
|
|
73
|
+
type?: string;
|
|
74
|
+
request?: BundleRequest;
|
|
75
|
+
}>): this;
|
|
76
|
+
/**
|
|
77
|
+
* MedicationStatement helper for IPS-in-Communication use cases.
|
|
78
|
+
*
|
|
79
|
+
* Expected keys should come from MedicationStatement claims constants.
|
|
80
|
+
*/
|
|
81
|
+
upsertActiveMedicationStatementEntry(input: Readonly<{
|
|
82
|
+
claims: MedicationStatementClaimsFlat | Record<string, unknown>;
|
|
83
|
+
fullUrl?: string;
|
|
84
|
+
type?: string;
|
|
85
|
+
request?: BundleRequest;
|
|
86
|
+
}>): this;
|
|
87
|
+
/**
|
|
88
|
+
* DocumentReference helper for bundle-contained attachments linked from
|
|
89
|
+
* other clinical resources through `*.contained-documents`.
|
|
90
|
+
*/
|
|
91
|
+
upsertActiveDocumentReferenceEntry(input: Readonly<{
|
|
92
|
+
claims: Record<string, unknown>;
|
|
93
|
+
fullUrl?: string;
|
|
94
|
+
type?: string;
|
|
95
|
+
request?: BundleRequest;
|
|
96
|
+
}>): this;
|
|
97
|
+
/**
|
|
98
|
+
* Condition helper for IPS-in-Communication use cases.
|
|
99
|
+
*
|
|
100
|
+
* Expected keys should come from Condition claims constants.
|
|
101
|
+
*/
|
|
102
|
+
upsertActiveConditionEntry(input: Readonly<{
|
|
103
|
+
claims: Record<string, unknown>;
|
|
104
|
+
fullUrl?: string;
|
|
105
|
+
type?: string;
|
|
106
|
+
request?: BundleRequest;
|
|
107
|
+
}>): this;
|
|
108
|
+
/**
|
|
109
|
+
* AllergyIntolerance helper for IPS-in-Communication use cases.
|
|
110
|
+
*
|
|
111
|
+
* Expected keys should come from AllergyIntolerance claims constants.
|
|
112
|
+
*/
|
|
113
|
+
upsertActiveAllergyIntoleranceEntry(input: Readonly<{
|
|
114
|
+
claims: Record<string, unknown>;
|
|
115
|
+
fullUrl?: string;
|
|
116
|
+
type?: string;
|
|
117
|
+
request?: BundleRequest;
|
|
118
|
+
}>): this;
|
|
119
|
+
/**
|
|
120
|
+
* TODO(ips-next):
|
|
121
|
+
* Add `upsertActiveDiagnosticReportEntry(...)` once the shared claim helpers
|
|
122
|
+
* for `DiagnosticReport` are in place.
|
|
123
|
+
*
|
|
124
|
+
* Expected shape should mirror the existing resource helpers:
|
|
125
|
+
* - `claims` authored with `@context = org.hl7.fhir.api`
|
|
126
|
+
* - matching priority by `DiagnosticReport.identifier`
|
|
127
|
+
* - support for linked `DocumentReference` ids through
|
|
128
|
+
* `DiagnosticReport.contained-documents`
|
|
129
|
+
*
|
|
130
|
+
* Intentionally not implemented in this pass:
|
|
131
|
+
* - IPS authoring already works for the currently documented resources
|
|
132
|
+
* - GW Core can already consume bundle-contained `DocumentReference` rows
|
|
133
|
+
* - adding the DiagnosticReport editing surface now would expand the IPS
|
|
134
|
+
* contract further than intended for this release slice
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Creates or updates a linked `DocumentReference` entry and stores its
|
|
138
|
+
* identifier under the active resource `*.contained-documents` claim.
|
|
139
|
+
*/
|
|
140
|
+
addContainedDocumentToActiveEntry(input: AddContainedDocumentToActiveEntryInput): this;
|
|
141
|
+
/**
|
|
142
|
+
* Patches active entry `resource.meta.claims` and synchronizes attachment data.
|
|
143
|
+
*/
|
|
144
|
+
patchActiveEntryClaims(claimPatch: Record<string, unknown>): this;
|
|
145
|
+
/**
|
|
146
|
+
* Persists current memory state into communication claims attachment.
|
|
147
|
+
* No-op for active entry pointer.
|
|
148
|
+
*/
|
|
149
|
+
saveActiveEntry(): this;
|
|
150
|
+
/**
|
|
151
|
+
* Persists and releases active entry memory pointer.
|
|
152
|
+
* This is the recommended step after a successful save operation.
|
|
153
|
+
*/
|
|
154
|
+
saveAndReleaseActiveEntry(): this;
|
|
155
|
+
/**
|
|
156
|
+
* Returns stable resource IDs from bundle entries with optional filters.
|
|
157
|
+
*/
|
|
158
|
+
getResourceIds(filters?: BundleResourceIdFilters): string[];
|
|
159
|
+
/**
|
|
160
|
+
* Returns bundle entries matching resource IDs produced by `getResourceIds`.
|
|
161
|
+
*/
|
|
162
|
+
getResourceEntriesByIds(resourceIds: readonly string[]): BundleEntry[];
|
|
163
|
+
/**
|
|
164
|
+
* Resolves the entry URL (`fullUrl`) for a given entry/resource identifier.
|
|
165
|
+
*/
|
|
166
|
+
getEntryUrl(entryId: string): string | undefined;
|
|
167
|
+
private decodeBundleFromClaims;
|
|
168
|
+
private syncAttachmentFromBundle;
|
|
169
|
+
private resolveCurrentSubject;
|
|
170
|
+
private findUpsertIndex;
|
|
171
|
+
private resolveEntryIdentifier;
|
|
172
|
+
private createBundleEntry;
|
|
173
|
+
private resolveEntryCanonicalIdValue;
|
|
174
|
+
private assertEntryIndex;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* High-level consent-access editor alias for onboarding and app-facing code.
|
|
178
|
+
*
|
|
179
|
+
* This keeps the business intent explicit for developers who are editing
|
|
180
|
+
* Consent access rules inside a Communication-carried bundle and should not
|
|
181
|
+
* need to start from the lower-level generic session name.
|
|
182
|
+
*/
|
|
183
|
+
export declare class ConsentAccessEditor extends CommunicationAttachedBundleSession {
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* High-level factory for consent-access editing.
|
|
187
|
+
*
|
|
188
|
+
* Prefer this name in onboarding docs when the developer intent is:
|
|
189
|
+
* "edit a Consent access bundle carried by a Communication".
|
|
190
|
+
*/
|
|
191
|
+
export declare function createConsentAccessEditor(options?: CommunicationAttachedBundleSessionOptions): ConsentAccessEditor;
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
// Copyright 2026 Conectate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
|
|
2
|
+
// Always create JSDoc, do not use strings inline in keys nor values, use types instead, and reuse the data test examples.
|
|
3
|
+
import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
|
|
4
|
+
import { ClaimConsent } from '../models/consent-rule.js';
|
|
5
|
+
import { AllergyIntoleranceClaim } from '../models/interoperable-claims/allergy-intolerance-claims.js';
|
|
6
|
+
import { CommunicationClaim } from '../models/interoperable-claims/communication-claims.js';
|
|
7
|
+
import { ConditionClaim } from '../models/interoperable-claims/condition-claims.js';
|
|
8
|
+
import { DocumentReferenceClaim } from '../models/interoperable-claims/document-reference-claims.js';
|
|
9
|
+
import { BundleQuery } from './bundle-query.js';
|
|
10
|
+
import { addClaimValues } from '../claims/claim-list-helpers.js';
|
|
11
|
+
import { MedicationStatementClaim, } from '../models/interoperable-claims/medication-statement-claims.js';
|
|
12
|
+
/**
|
|
13
|
+
* Communication editing session with bundle-in-memory as source of truth.
|
|
14
|
+
*
|
|
15
|
+
* Design contract:
|
|
16
|
+
* - `activeEntry` is the real editing unit (not only `activeResource`), because
|
|
17
|
+
* it can include `fullUrl`, `request`, and entry-level context.
|
|
18
|
+
* - `Communication.content-attachment-data` is always derived from the
|
|
19
|
+
* in-memory bundle after each committed update.
|
|
20
|
+
* - saving can release active entry memory via `saveAndReleaseActiveEntry()`.
|
|
21
|
+
*/
|
|
22
|
+
export class CommunicationAttachedBundleSession {
|
|
23
|
+
communicationClaims;
|
|
24
|
+
bundleInMemory;
|
|
25
|
+
activeEntryIndex;
|
|
26
|
+
mode;
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.mode = options.mode || 'strict';
|
|
29
|
+
this.communicationClaims = {
|
|
30
|
+
...options.communicationClaims,
|
|
31
|
+
};
|
|
32
|
+
this.activeEntryIndex = null;
|
|
33
|
+
const providedBundle = options.initialBundle ? cloneBundle(options.initialBundle) : undefined;
|
|
34
|
+
this.bundleInMemory = providedBundle || this.decodeBundleFromClaims(this.communicationClaims);
|
|
35
|
+
this.syncAttachmentFromBundle();
|
|
36
|
+
}
|
|
37
|
+
/** Returns a deep copy of communication claims. */
|
|
38
|
+
getCommunicationClaims() {
|
|
39
|
+
return {
|
|
40
|
+
...this.communicationClaims,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Returns a deep copy of the current in-memory bundle. */
|
|
44
|
+
getBundleInMemory() {
|
|
45
|
+
return cloneBundle(this.bundleInMemory);
|
|
46
|
+
}
|
|
47
|
+
/** Returns the active entry index, or null when no entry is selected. */
|
|
48
|
+
getActiveEntryIndex() {
|
|
49
|
+
return this.activeEntryIndex;
|
|
50
|
+
}
|
|
51
|
+
/** Returns a deep copy of the active entry when selected. */
|
|
52
|
+
getActiveEntry() {
|
|
53
|
+
if (this.activeEntryIndex === null) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return cloneEntry(this.bundleInMemory.data[this.activeEntryIndex]);
|
|
57
|
+
}
|
|
58
|
+
/** Selects an active entry by index or fullUrl. */
|
|
59
|
+
selectActiveEntry(selection) {
|
|
60
|
+
if (typeof selection.index === 'number') {
|
|
61
|
+
this.assertEntryIndex(selection.index);
|
|
62
|
+
this.activeEntryIndex = selection.index;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
if (selection.fullUrl) {
|
|
66
|
+
const foundIndex = this.bundleInMemory.data.findIndex((entry) => String(entry?.fullUrl || '').trim() === selection.fullUrl);
|
|
67
|
+
if (foundIndex < 0) {
|
|
68
|
+
throw new Error(`Active entry not found for fullUrl: ${selection.fullUrl}`);
|
|
69
|
+
}
|
|
70
|
+
this.activeEntryIndex = foundIndex;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
throw new Error('selectActiveEntry requires either index or fullUrl.');
|
|
74
|
+
}
|
|
75
|
+
/** Clears active entry selection from memory. */
|
|
76
|
+
clearActiveEntry() {
|
|
77
|
+
this.activeEntryIndex = null;
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Upserts an entry in bundle memory and marks it as active.
|
|
82
|
+
* Matching priority: `fullUrl` if present, then resource claim identifier.
|
|
83
|
+
*/
|
|
84
|
+
upsertActiveEntry(input) {
|
|
85
|
+
const entry = this.createBundleEntry(input);
|
|
86
|
+
const nextIndex = this.findUpsertIndex(entry, input.fullUrl);
|
|
87
|
+
if (nextIndex >= 0) {
|
|
88
|
+
this.bundleInMemory.data[nextIndex] = entry;
|
|
89
|
+
this.activeEntryIndex = nextIndex;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.bundleInMemory.data.push(entry);
|
|
93
|
+
this.activeEntryIndex = this.bundleInMemory.data.length - 1;
|
|
94
|
+
}
|
|
95
|
+
this.syncAttachmentFromBundle();
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Consent-first helper for developer onboarding.
|
|
100
|
+
*
|
|
101
|
+
* Expected keys should come from `ClaimConsent` in caller code.
|
|
102
|
+
*/
|
|
103
|
+
upsertActiveConsentEntry(input) {
|
|
104
|
+
return this.upsertActiveEntry({
|
|
105
|
+
resourceType: ResourceTypesFhirR4.Consent,
|
|
106
|
+
claims: input.claims,
|
|
107
|
+
fullUrl: input.fullUrl,
|
|
108
|
+
type: input.type,
|
|
109
|
+
request: input.request,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* MedicationStatement helper for IPS-in-Communication use cases.
|
|
114
|
+
*
|
|
115
|
+
* Expected keys should come from MedicationStatement claims constants.
|
|
116
|
+
*/
|
|
117
|
+
upsertActiveMedicationStatementEntry(input) {
|
|
118
|
+
return this.upsertActiveEntry({
|
|
119
|
+
resourceType: ResourceTypesFhirR4.MedicationStatement,
|
|
120
|
+
claims: {
|
|
121
|
+
...input.claims,
|
|
122
|
+
},
|
|
123
|
+
fullUrl: input.fullUrl,
|
|
124
|
+
type: input.type,
|
|
125
|
+
request: input.request,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* DocumentReference helper for bundle-contained attachments linked from
|
|
130
|
+
* other clinical resources through `*.contained-documents`.
|
|
131
|
+
*/
|
|
132
|
+
upsertActiveDocumentReferenceEntry(input) {
|
|
133
|
+
return this.upsertActiveEntry({
|
|
134
|
+
resourceType: ResourceTypesFhirR4.DocumentReference,
|
|
135
|
+
claims: {
|
|
136
|
+
...input.claims,
|
|
137
|
+
},
|
|
138
|
+
fullUrl: input.fullUrl,
|
|
139
|
+
type: input.type,
|
|
140
|
+
request: input.request,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Condition helper for IPS-in-Communication use cases.
|
|
145
|
+
*
|
|
146
|
+
* Expected keys should come from Condition claims constants.
|
|
147
|
+
*/
|
|
148
|
+
upsertActiveConditionEntry(input) {
|
|
149
|
+
return this.upsertActiveEntry({
|
|
150
|
+
resourceType: ResourceTypesFhirR4.Condition,
|
|
151
|
+
claims: {
|
|
152
|
+
...input.claims,
|
|
153
|
+
},
|
|
154
|
+
fullUrl: input.fullUrl,
|
|
155
|
+
type: input.type,
|
|
156
|
+
request: input.request,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* AllergyIntolerance helper for IPS-in-Communication use cases.
|
|
161
|
+
*
|
|
162
|
+
* Expected keys should come from AllergyIntolerance claims constants.
|
|
163
|
+
*/
|
|
164
|
+
upsertActiveAllergyIntoleranceEntry(input) {
|
|
165
|
+
return this.upsertActiveEntry({
|
|
166
|
+
resourceType: ResourceTypesFhirR4.AllergyIntolerance,
|
|
167
|
+
claims: {
|
|
168
|
+
...input.claims,
|
|
169
|
+
},
|
|
170
|
+
fullUrl: input.fullUrl,
|
|
171
|
+
type: input.type,
|
|
172
|
+
request: input.request,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* TODO(ips-next):
|
|
177
|
+
* Add `upsertActiveDiagnosticReportEntry(...)` once the shared claim helpers
|
|
178
|
+
* for `DiagnosticReport` are in place.
|
|
179
|
+
*
|
|
180
|
+
* Expected shape should mirror the existing resource helpers:
|
|
181
|
+
* - `claims` authored with `@context = org.hl7.fhir.api`
|
|
182
|
+
* - matching priority by `DiagnosticReport.identifier`
|
|
183
|
+
* - support for linked `DocumentReference` ids through
|
|
184
|
+
* `DiagnosticReport.contained-documents`
|
|
185
|
+
*
|
|
186
|
+
* Intentionally not implemented in this pass:
|
|
187
|
+
* - IPS authoring already works for the currently documented resources
|
|
188
|
+
* - GW Core can already consume bundle-contained `DocumentReference` rows
|
|
189
|
+
* - adding the DiagnosticReport editing surface now would expand the IPS
|
|
190
|
+
* contract further than intended for this release slice
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* Creates or updates a linked `DocumentReference` entry and stores its
|
|
194
|
+
* identifier under the active resource `*.contained-documents` claim.
|
|
195
|
+
*/
|
|
196
|
+
addContainedDocumentToActiveEntry(input) {
|
|
197
|
+
if (this.activeEntryIndex === null) {
|
|
198
|
+
throw new Error('No active entry selected.');
|
|
199
|
+
}
|
|
200
|
+
const parentIndex = this.activeEntryIndex;
|
|
201
|
+
const parentEntry = cloneEntry(this.bundleInMemory.data[parentIndex]);
|
|
202
|
+
const parentResource = ensureEntryResource(parentEntry, this.mode);
|
|
203
|
+
const parentClaims = {
|
|
204
|
+
...(parentResource.meta?.claims || {}),
|
|
205
|
+
};
|
|
206
|
+
const parentResourceType = asTrimmedString(parentResource.resourceType);
|
|
207
|
+
const containedDocumentsClaimKey = resolveContainedDocumentsClaimKey(parentResourceType);
|
|
208
|
+
if (!containedDocumentsClaimKey) {
|
|
209
|
+
throw new Error(`Contained documents are not supported for resourceType: ${parentResourceType || 'unknown'}`);
|
|
210
|
+
}
|
|
211
|
+
const documentIdentifier = asTrimmedString(input.identifier)
|
|
212
|
+
|| asTrimmedString(input.claims?.[DocumentReferenceClaim.Identifier])
|
|
213
|
+
|| runtimeUuid('docref');
|
|
214
|
+
const documentSubject = asTrimmedString(input.claims?.[DocumentReferenceClaim.Subject])
|
|
215
|
+
|| resolveSubjectFromClaims(parentClaims)
|
|
216
|
+
|| asTrimmedString(this.communicationClaims[CommunicationClaim.Subject]);
|
|
217
|
+
const documentClaims = {
|
|
218
|
+
'@context': 'org.hl7.fhir.api',
|
|
219
|
+
...(input.claims || {}),
|
|
220
|
+
[DocumentReferenceClaim.Identifier]: documentIdentifier,
|
|
221
|
+
};
|
|
222
|
+
if (documentSubject) {
|
|
223
|
+
documentClaims[DocumentReferenceClaim.Subject] = documentSubject;
|
|
224
|
+
}
|
|
225
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.ContentType, input.attachmentContentType);
|
|
226
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.ContentData, input.attachmentDataBase64);
|
|
227
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.Location, input.attachmentUrl);
|
|
228
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.Description, input.description);
|
|
229
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.Date, input.date);
|
|
230
|
+
setIfMissing(documentClaims, DocumentReferenceClaim.Language, input.language);
|
|
231
|
+
this.upsertActiveDocumentReferenceEntry({
|
|
232
|
+
claims: documentClaims,
|
|
233
|
+
fullUrl: input.fullUrl || `urn:uuid:${documentIdentifier}`,
|
|
234
|
+
});
|
|
235
|
+
parentResource.meta = parentResource.meta || {};
|
|
236
|
+
parentResource.meta.claims = addClaimValues(parentClaims, containedDocumentsClaimKey, [documentIdentifier]);
|
|
237
|
+
parentEntry.resource = parentResource;
|
|
238
|
+
this.bundleInMemory.data[parentIndex] = parentEntry;
|
|
239
|
+
this.activeEntryIndex = parentIndex;
|
|
240
|
+
this.syncAttachmentFromBundle();
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Patches active entry `resource.meta.claims` and synchronizes attachment data.
|
|
245
|
+
*/
|
|
246
|
+
patchActiveEntryClaims(claimPatch) {
|
|
247
|
+
if (this.activeEntryIndex === null) {
|
|
248
|
+
throw new Error('No active entry selected.');
|
|
249
|
+
}
|
|
250
|
+
const current = cloneEntry(this.bundleInMemory.data[this.activeEntryIndex]);
|
|
251
|
+
const resource = ensureEntryResource(current, this.mode);
|
|
252
|
+
resource.meta = resource.meta || {};
|
|
253
|
+
resource.meta.claims = {
|
|
254
|
+
...(resource.meta.claims || {}),
|
|
255
|
+
...claimPatch,
|
|
256
|
+
};
|
|
257
|
+
current.resource = resource;
|
|
258
|
+
this.bundleInMemory.data[this.activeEntryIndex] = current;
|
|
259
|
+
this.syncAttachmentFromBundle();
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Persists current memory state into communication claims attachment.
|
|
264
|
+
* No-op for active entry pointer.
|
|
265
|
+
*/
|
|
266
|
+
saveActiveEntry() {
|
|
267
|
+
this.syncAttachmentFromBundle();
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Persists and releases active entry memory pointer.
|
|
272
|
+
* This is the recommended step after a successful save operation.
|
|
273
|
+
*/
|
|
274
|
+
saveAndReleaseActiveEntry() {
|
|
275
|
+
this.syncAttachmentFromBundle();
|
|
276
|
+
this.clearActiveEntry();
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Returns stable resource IDs from bundle entries with optional filters.
|
|
281
|
+
*/
|
|
282
|
+
getResourceIds(filters = {}) {
|
|
283
|
+
const query = new BundleQuery(this.bundleInMemory);
|
|
284
|
+
return query.getResourceIds(filters);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Returns bundle entries matching resource IDs produced by `getResourceIds`.
|
|
288
|
+
*/
|
|
289
|
+
getResourceEntriesByIds(resourceIds) {
|
|
290
|
+
const query = new BundleQuery(this.bundleInMemory);
|
|
291
|
+
return query.getResourceEntriesByIds(resourceIds);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Resolves the entry URL (`fullUrl`) for a given entry/resource identifier.
|
|
295
|
+
*/
|
|
296
|
+
getEntryUrl(entryId) {
|
|
297
|
+
const query = new BundleQuery(this.bundleInMemory);
|
|
298
|
+
return query.getEntryUrl(entryId);
|
|
299
|
+
}
|
|
300
|
+
decodeBundleFromClaims(claims) {
|
|
301
|
+
const encoded = asTrimmedString(claims[CommunicationClaim.ContentAttachmentData]);
|
|
302
|
+
if (!encoded) {
|
|
303
|
+
return createEmptyBundle();
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const text = Buffer.from(encoded, 'base64').toString('utf8');
|
|
307
|
+
const parsed = JSON.parse(text);
|
|
308
|
+
validateBundleLike(parsed, this.mode);
|
|
309
|
+
return cloneBundle(parsed);
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (this.mode === 'normalize') {
|
|
313
|
+
return createEmptyBundle();
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`Invalid ${CommunicationClaim.ContentAttachmentData}: ${error.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
syncAttachmentFromBundle() {
|
|
319
|
+
this.communicationClaims[CommunicationClaim.ContentAttachmentType] = 'application/fhir+json';
|
|
320
|
+
this.communicationClaims[CommunicationClaim.ContentAttachmentData] = encodeBundleToBase64(this.bundleInMemory);
|
|
321
|
+
const activeSubject = this.resolveCurrentSubject();
|
|
322
|
+
if (activeSubject) {
|
|
323
|
+
this.communicationClaims[CommunicationClaim.Subject] = activeSubject;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
resolveCurrentSubject() {
|
|
327
|
+
if (this.activeEntryIndex !== null) {
|
|
328
|
+
const claims = this.bundleInMemory.data[this.activeEntryIndex]?.resource?.meta?.claims || {};
|
|
329
|
+
const consentSubject = asTrimmedString(claims[ClaimConsent.subject]);
|
|
330
|
+
if (consentSubject) {
|
|
331
|
+
return consentSubject;
|
|
332
|
+
}
|
|
333
|
+
const medicationSubject = asTrimmedString(claims[MedicationStatementClaim.Subject]);
|
|
334
|
+
if (medicationSubject) {
|
|
335
|
+
return medicationSubject;
|
|
336
|
+
}
|
|
337
|
+
const conditionSubject = asTrimmedString(claims[ConditionClaim.Subject]);
|
|
338
|
+
if (conditionSubject) {
|
|
339
|
+
return conditionSubject;
|
|
340
|
+
}
|
|
341
|
+
const allergySubject = asTrimmedString(claims[AllergyIntoleranceClaim.Subject] || claims[AllergyIntoleranceClaim.Patient]);
|
|
342
|
+
if (allergySubject) {
|
|
343
|
+
return allergySubject;
|
|
344
|
+
}
|
|
345
|
+
const documentReferenceSubject = asTrimmedString(claims[DocumentReferenceClaim.Subject]);
|
|
346
|
+
if (documentReferenceSubject) {
|
|
347
|
+
return documentReferenceSubject;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const fromClaims = asTrimmedString(this.communicationClaims[CommunicationClaim.Subject]);
|
|
351
|
+
return fromClaims || undefined;
|
|
352
|
+
}
|
|
353
|
+
findUpsertIndex(entry, fullUrl) {
|
|
354
|
+
if (fullUrl) {
|
|
355
|
+
const byFullUrl = this.bundleInMemory.data.findIndex((item) => String(item?.fullUrl || '').trim() === fullUrl);
|
|
356
|
+
if (byFullUrl >= 0) {
|
|
357
|
+
return byFullUrl;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const incomingClaims = entry.resource?.meta?.claims || {};
|
|
361
|
+
const incomingIdentifier = this.resolveEntryIdentifier(incomingClaims);
|
|
362
|
+
if (!incomingIdentifier) {
|
|
363
|
+
return -1;
|
|
364
|
+
}
|
|
365
|
+
return this.bundleInMemory.data.findIndex((item) => {
|
|
366
|
+
const itemClaims = item?.resource?.meta?.claims || {};
|
|
367
|
+
return this.resolveEntryIdentifier(itemClaims) === incomingIdentifier;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
resolveEntryIdentifier(claims) {
|
|
371
|
+
const consentIdentifier = asTrimmedString(claims[ClaimConsent.identifier]);
|
|
372
|
+
if (consentIdentifier) {
|
|
373
|
+
return `${ResourceTypesFhirR4.Consent}:${consentIdentifier}`;
|
|
374
|
+
}
|
|
375
|
+
const medicationIdentifier = asTrimmedString(claims[MedicationStatementClaim.Identifier]);
|
|
376
|
+
if (medicationIdentifier) {
|
|
377
|
+
return `${ResourceTypesFhirR4.MedicationStatement}:${medicationIdentifier}`;
|
|
378
|
+
}
|
|
379
|
+
const conditionIdentifier = asTrimmedString(claims[ConditionClaim.Identifier]);
|
|
380
|
+
if (conditionIdentifier) {
|
|
381
|
+
return `${ResourceTypesFhirR4.Condition}:${conditionIdentifier}`;
|
|
382
|
+
}
|
|
383
|
+
const allergyIdentifier = asTrimmedString(claims[AllergyIntoleranceClaim.Identifier]);
|
|
384
|
+
if (allergyIdentifier) {
|
|
385
|
+
return `${ResourceTypesFhirR4.AllergyIntolerance}:${allergyIdentifier}`;
|
|
386
|
+
}
|
|
387
|
+
const documentReferenceIdentifier = asTrimmedString(claims[DocumentReferenceClaim.Identifier]);
|
|
388
|
+
if (documentReferenceIdentifier) {
|
|
389
|
+
return `${ResourceTypesFhirR4.DocumentReference}:${documentReferenceIdentifier}`;
|
|
390
|
+
}
|
|
391
|
+
return '';
|
|
392
|
+
}
|
|
393
|
+
createBundleEntry(input) {
|
|
394
|
+
const resourceClaims = {
|
|
395
|
+
...input.claims,
|
|
396
|
+
};
|
|
397
|
+
const canonicalEntryIdentifier = this.resolveEntryCanonicalIdValue(resourceClaims);
|
|
398
|
+
return {
|
|
399
|
+
id: canonicalEntryIdentifier || undefined,
|
|
400
|
+
type: input.type || `${input.resourceType}-edit-request-v1.0`,
|
|
401
|
+
fullUrl: input.fullUrl,
|
|
402
|
+
request: input.request,
|
|
403
|
+
resource: {
|
|
404
|
+
resourceType: input.resourceType,
|
|
405
|
+
meta: {
|
|
406
|
+
claims: resourceClaims,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
resolveEntryCanonicalIdValue(claims) {
|
|
412
|
+
const consentIdentifier = asTrimmedString(claims[ClaimConsent.identifier]);
|
|
413
|
+
if (consentIdentifier) {
|
|
414
|
+
return consentIdentifier;
|
|
415
|
+
}
|
|
416
|
+
const medicationIdentifier = asTrimmedString(claims[MedicationStatementClaim.Identifier]);
|
|
417
|
+
if (medicationIdentifier) {
|
|
418
|
+
return medicationIdentifier;
|
|
419
|
+
}
|
|
420
|
+
const conditionIdentifier = asTrimmedString(claims[ConditionClaim.Identifier]);
|
|
421
|
+
if (conditionIdentifier) {
|
|
422
|
+
return conditionIdentifier;
|
|
423
|
+
}
|
|
424
|
+
const allergyIdentifier = asTrimmedString(claims[AllergyIntoleranceClaim.Identifier]);
|
|
425
|
+
if (allergyIdentifier) {
|
|
426
|
+
return allergyIdentifier;
|
|
427
|
+
}
|
|
428
|
+
const documentReferenceIdentifier = asTrimmedString(claims[DocumentReferenceClaim.Identifier]);
|
|
429
|
+
if (documentReferenceIdentifier) {
|
|
430
|
+
return documentReferenceIdentifier;
|
|
431
|
+
}
|
|
432
|
+
const communicationIdentifier = asTrimmedString(claims[CommunicationClaim.Identifier]);
|
|
433
|
+
if (communicationIdentifier) {
|
|
434
|
+
return communicationIdentifier;
|
|
435
|
+
}
|
|
436
|
+
return '';
|
|
437
|
+
}
|
|
438
|
+
assertEntryIndex(index) {
|
|
439
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.bundleInMemory.data.length) {
|
|
440
|
+
throw new Error(`Entry index out of range: ${index}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* High-level consent-access editor alias for onboarding and app-facing code.
|
|
446
|
+
*
|
|
447
|
+
* This keeps the business intent explicit for developers who are editing
|
|
448
|
+
* Consent access rules inside a Communication-carried bundle and should not
|
|
449
|
+
* need to start from the lower-level generic session name.
|
|
450
|
+
*/
|
|
451
|
+
export class ConsentAccessEditor extends CommunicationAttachedBundleSession {
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* High-level factory for consent-access editing.
|
|
455
|
+
*
|
|
456
|
+
* Prefer this name in onboarding docs when the developer intent is:
|
|
457
|
+
* "edit a Consent access bundle carried by a Communication".
|
|
458
|
+
*/
|
|
459
|
+
export function createConsentAccessEditor(options = {}) {
|
|
460
|
+
return new ConsentAccessEditor(options);
|
|
461
|
+
}
|
|
462
|
+
function ensureEntryResource(entry, mode) {
|
|
463
|
+
const resource = entry.resource;
|
|
464
|
+
if (resource && typeof resource === 'object') {
|
|
465
|
+
return resource;
|
|
466
|
+
}
|
|
467
|
+
if (mode === 'normalize') {
|
|
468
|
+
return { meta: { claims: {} } };
|
|
469
|
+
}
|
|
470
|
+
throw new Error('Active entry does not contain a valid resource object.');
|
|
471
|
+
}
|
|
472
|
+
function validateBundleLike(bundle, mode) {
|
|
473
|
+
const looksLikeBundle = bundle && bundle.resourceType === ResourceTypesFhirR4.Bundle && Array.isArray(bundle.data);
|
|
474
|
+
if (looksLikeBundle) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (mode === 'normalize') {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
throw new Error('Decoded attachment is not a valid BundleJsonApi payload.');
|
|
481
|
+
}
|
|
482
|
+
function createEmptyBundle() {
|
|
483
|
+
return {
|
|
484
|
+
resourceType: ResourceTypesFhirR4.Bundle,
|
|
485
|
+
type: 'batch',
|
|
486
|
+
data: [],
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function encodeBundleToBase64(bundle) {
|
|
490
|
+
return Buffer.from(JSON.stringify(bundle), 'utf8').toString('base64');
|
|
491
|
+
}
|
|
492
|
+
function cloneBundle(bundle) {
|
|
493
|
+
return JSON.parse(JSON.stringify(bundle));
|
|
494
|
+
}
|
|
495
|
+
function cloneEntry(entry) {
|
|
496
|
+
return JSON.parse(JSON.stringify(entry));
|
|
497
|
+
}
|
|
498
|
+
function asTrimmedString(value) {
|
|
499
|
+
if (value === undefined || value === null) {
|
|
500
|
+
return '';
|
|
501
|
+
}
|
|
502
|
+
return String(value).trim();
|
|
503
|
+
}
|
|
504
|
+
function resolveContainedDocumentsClaimKey(resourceType) {
|
|
505
|
+
if (resourceType === ResourceTypesFhirR4.Consent) {
|
|
506
|
+
return ClaimConsent.containedDocuments;
|
|
507
|
+
}
|
|
508
|
+
if (resourceType === ResourceTypesFhirR4.MedicationStatement) {
|
|
509
|
+
return MedicationStatementClaim.ContainedDocuments;
|
|
510
|
+
}
|
|
511
|
+
if (resourceType === ResourceTypesFhirR4.Condition) {
|
|
512
|
+
return ConditionClaim.ContainedDocuments;
|
|
513
|
+
}
|
|
514
|
+
if (resourceType === ResourceTypesFhirR4.AllergyIntolerance) {
|
|
515
|
+
return AllergyIntoleranceClaim.ContainedDocuments;
|
|
516
|
+
}
|
|
517
|
+
return '';
|
|
518
|
+
}
|
|
519
|
+
function resolveSubjectFromClaims(claims) {
|
|
520
|
+
return asTrimmedString(claims[ClaimConsent.subject]
|
|
521
|
+
|| claims[MedicationStatementClaim.Subject]
|
|
522
|
+
|| claims[ConditionClaim.Subject]
|
|
523
|
+
|| claims[AllergyIntoleranceClaim.Subject]
|
|
524
|
+
|| claims[AllergyIntoleranceClaim.Patient]
|
|
525
|
+
|| claims[DocumentReferenceClaim.Subject]);
|
|
526
|
+
}
|
|
527
|
+
function setIfMissing(target, key, value) {
|
|
528
|
+
if (target[key] !== undefined) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (value === undefined || value === null || String(value).trim() === '') {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
target[key] = value;
|
|
535
|
+
}
|
|
536
|
+
function runtimeUuid(prefix) {
|
|
537
|
+
const cryptoLike = globalThis;
|
|
538
|
+
if (typeof cryptoLike.crypto?.randomUUID === 'function') {
|
|
539
|
+
return cryptoLike.crypto.randomUUID();
|
|
540
|
+
}
|
|
541
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
542
|
+
}
|
package/dist/utils/employee.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ export type EmployeeBatchEntryInput = Readonly<{
|
|
|
18
18
|
resourceType?: 'Employee';
|
|
19
19
|
type?: string;
|
|
20
20
|
}>;
|
|
21
|
+
export type EmployeeBatchBundleInput = Readonly<{
|
|
22
|
+
entries: readonly EmployeeBatchEntryInput[];
|
|
23
|
+
}>;
|
|
21
24
|
export type EmployeeSearchBundleInput = Readonly<{
|
|
22
25
|
claims?: Record<string, EmployeeSearchValue | undefined>;
|
|
23
26
|
method?: 'GET' | 'POST';
|
|
@@ -48,6 +51,15 @@ export declare function buildEmployeeBatchEntry(input: EmployeeBatchEntryInput):
|
|
|
48
51
|
};
|
|
49
52
|
};
|
|
50
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Builds a canonical employee `_batch` bundle from one or more employee batch
|
|
56
|
+
* entries.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildEmployeeBatchBundle(input: EmployeeBatchBundleInput): {
|
|
59
|
+
resourceType: 'Bundle';
|
|
60
|
+
type: 'batch';
|
|
61
|
+
entry: Array<ReturnType<typeof buildEmployeeBatchEntry>>;
|
|
62
|
+
};
|
|
51
63
|
/**
|
|
52
64
|
* Builds the legacy query-string employee search target kept for compatibility
|
|
53
65
|
* with older `_search` wrappers.
|
package/dist/utils/employee.js
CHANGED
|
@@ -3,6 +3,15 @@ import { buildFhirParametersResourceFromSearchParams, buildSearchQueryString, }
|
|
|
3
3
|
function cloneClaims(claims) {
|
|
4
4
|
return { ...(claims || {}) };
|
|
5
5
|
}
|
|
6
|
+
function normalizeEmployeeSearchClaims(claims) {
|
|
7
|
+
const normalized = {};
|
|
8
|
+
for (const [key, value] of Object.entries(claims || {})) {
|
|
9
|
+
if (key === '@context')
|
|
10
|
+
continue;
|
|
11
|
+
normalized[key] = value;
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
6
15
|
function inferEmployeeEntryType(method) {
|
|
7
16
|
switch (method) {
|
|
8
17
|
case 'DELETE':
|
|
@@ -65,13 +74,24 @@ export function buildEmployeeBatchEntry(input) {
|
|
|
65
74
|
},
|
|
66
75
|
};
|
|
67
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Builds a canonical employee `_batch` bundle from one or more employee batch
|
|
79
|
+
* entries.
|
|
80
|
+
*/
|
|
81
|
+
export function buildEmployeeBatchBundle(input) {
|
|
82
|
+
return {
|
|
83
|
+
resourceType: 'Bundle',
|
|
84
|
+
type: 'batch',
|
|
85
|
+
entry: [...input.entries].map((entry) => buildEmployeeBatchEntry(entry)),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
68
88
|
/**
|
|
69
89
|
* Builds the legacy query-string employee search target kept for compatibility
|
|
70
90
|
* with older `_search` wrappers.
|
|
71
91
|
*/
|
|
72
92
|
export function buildEmployeeSearchQuery(input = {}) {
|
|
73
93
|
const resourceType = input.resourceType || 'Employee';
|
|
74
|
-
const query = buildSearchQueryString(input.claims
|
|
94
|
+
const query = buildSearchQueryString(normalizeEmployeeSearchClaims(input.claims));
|
|
75
95
|
return query ? `${resourceType}?${query}` : resourceType;
|
|
76
96
|
}
|
|
77
97
|
/**
|
|
@@ -82,7 +102,7 @@ export function buildEmployeeSearchQuery(input = {}) {
|
|
|
82
102
|
*/
|
|
83
103
|
export function buildEmployeeSearchBundle(input = {}) {
|
|
84
104
|
const resourceType = input.resourceType || 'Employee';
|
|
85
|
-
const claims = input.claims
|
|
105
|
+
const claims = normalizeEmployeeSearchClaims(input.claims);
|
|
86
106
|
const method = input.method || (input.encoding === 'get-query' ? 'GET' : 'POST');
|
|
87
107
|
if (method === 'GET') {
|
|
88
108
|
return {
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export * from './communication-fhir-r4';
|
|
|
26
26
|
export * from './communication-document-reference';
|
|
27
27
|
export * from './communication-bundle-document-request';
|
|
28
28
|
export * from './communication-identity';
|
|
29
|
-
export * from './communication-bundle-session';
|
|
29
|
+
export * from './communication-attached-bundle-session';
|
|
30
30
|
export * from './clinical-resource-converters';
|
|
31
31
|
export * from './clinical-resource-view';
|
|
32
32
|
export * from './fhir-validator';
|
package/dist/utils/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export * from './communication-fhir-r4.js';
|
|
|
26
26
|
export * from './communication-document-reference.js';
|
|
27
27
|
export * from './communication-bundle-document-request.js';
|
|
28
28
|
export * from './communication-identity.js';
|
|
29
|
-
export * from './communication-bundle-session.js';
|
|
29
|
+
export * from './communication-attached-bundle-session.js';
|
|
30
30
|
export * from './clinical-resource-converters.js';
|
|
31
31
|
export * from './clinical-resource-view.js';
|
|
32
32
|
export * from './fhir-validator.js';
|