gdc-common-utils-ts 2.3.5 → 2.3.7
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 +4 -0
- package/dist/convert/convert-allergy-intolerance.js +6 -0
- package/dist/convert/schemaorg-to-gaia-x.d.ts +22 -1
- package/dist/convert/schemaorg-to-gaia-x.js +77 -1
- package/dist/examples/individual-controller.d.ts +1 -3
- package/dist/examples/individual-controller.js +3 -3
- package/dist/examples/professional.d.ts +6 -6
- package/dist/examples/professional.js +120 -19
- package/dist/examples/shared.js +1 -1
- package/dist/models/indexing.js +1 -1
- package/dist/utils/bundle-reader.js +7 -1
- package/dist/utils/did.d.ts +7 -1
- package/dist/utils/did.js +9 -3
- package/dist/utils/professional-smart.d.ts +4 -0
- package/dist/utils/professional-smart.js +4 -0
- package/package.json +5 -2
- package/dist/utils/clinical-resource-filter-editor.d.ts +0 -39
- package/dist/utils/clinical-resource-filter-editor.js +0 -84
package/README.md
CHANGED
|
@@ -59,6 +59,10 @@ boundaries used in `gdc-common-utils-ts`.
|
|
|
59
59
|
- Only define custom names when no canonical FHIR SearchParameter exists.
|
|
60
60
|
- `resource.meta.claims` is the canonical project-specific claims container and must be preserved across conversions/transports.
|
|
61
61
|
- `resource.meta.claims` is not part of base FHIR; it is a claims-first extension carried by FHIR-like resources in GDC contracts.
|
|
62
|
+
- Native FHIR resources received from EHR systems do not need that extension:
|
|
63
|
+
normalize them with `normalizeClaimsFromFhirResource(...)` at the processing
|
|
64
|
+
boundary before indexed storage. Existing `resource.meta.claims` take
|
|
65
|
+
precedence so SDK-authored semantics survive transport conversion.
|
|
62
66
|
|
|
63
67
|
## Identity Continuity
|
|
64
68
|
|
|
@@ -17,6 +17,9 @@ export function allergyIntoleranceFlatToFhirR4(claims) {
|
|
|
17
17
|
code: claims[AllergyIntoleranceClaim.Code] ? { coding: codingFromValue(claims[AllergyIntoleranceClaim.Code]) } : undefined,
|
|
18
18
|
clinicalStatus: claims[AllergyIntoleranceClaim.ClinicalStatus] ? { coding: [{ code: claims[AllergyIntoleranceClaim.ClinicalStatus] }] } : undefined,
|
|
19
19
|
verificationStatus: claims[AllergyIntoleranceClaim.VerificationStatus] ? { coding: [{ code: claims[AllergyIntoleranceClaim.VerificationStatus] }] } : undefined,
|
|
20
|
+
category: claims[AllergyIntoleranceClaim.Category] ? [claims[AllergyIntoleranceClaim.Category]] : undefined,
|
|
21
|
+
criticality: claims[AllergyIntoleranceClaim.Criticality],
|
|
22
|
+
onsetDateTime: claims[AllergyIntoleranceClaim.OnsetDateTime],
|
|
20
23
|
recorder: recorder ? { reference: recorder } : undefined,
|
|
21
24
|
};
|
|
22
25
|
}
|
|
@@ -30,6 +33,9 @@ export function allergyIntoleranceFhirR4ToFlat(resource) {
|
|
|
30
33
|
[AllergyIntoleranceClaim.Code]: codingToValue(code?.coding?.[0]) || code?.text,
|
|
31
34
|
[AllergyIntoleranceClaim.ClinicalStatus]: resource.clinicalStatus?.coding?.[0]?.code,
|
|
32
35
|
[AllergyIntoleranceClaim.VerificationStatus]: resource.verificationStatus?.coding?.[0]?.code,
|
|
36
|
+
[AllergyIntoleranceClaim.Category]: resource.category?.[0],
|
|
37
|
+
[AllergyIntoleranceClaim.Criticality]: resource.criticality,
|
|
38
|
+
[AllergyIntoleranceClaim.OnsetDateTime]: resource.onsetDateTime,
|
|
33
39
|
[AllergyIntoleranceClaim.Recorder]: resource.recorder?.reference,
|
|
34
40
|
};
|
|
35
41
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ClaimsRecord } from '../models/resource-document';
|
|
2
|
-
import { type GaiaXCredentialDraft, type GaiaXLegalPersonCredentialSubject, type GaiaXServiceOfferingCredentialSubject, type GaiaXVcJwtAttachment, type IcaMemberDiscoveryData } from '../models/gaia-x';
|
|
2
|
+
import { type GaiaXCredentialAttachmentRoleValue, type GaiaXCredentialDraft, type GaiaXLegalPersonCredentialSubject, type GaiaXServiceOfferingCredentialSubject, type GaiaXVcJwtAttachment, type IcaMemberDiscoveryData } from '../models/gaia-x';
|
|
3
3
|
import type { DidDocument } from '../models/did';
|
|
4
4
|
export interface SchemaOrgOrganizationRegistrationIdentifier {
|
|
5
5
|
additionalType: string;
|
|
@@ -94,6 +94,27 @@ export declare function buildGaiaXVcJwtAttachment(input: Readonly<{
|
|
|
94
94
|
jwt: string;
|
|
95
95
|
role: GaiaXVcJwtAttachment['role'];
|
|
96
96
|
}>): GaiaXVcJwtAttachment;
|
|
97
|
+
/**
|
|
98
|
+
* Enforces the semantic contract of a signed Gaia-X discovery VC-JWT.
|
|
99
|
+
*
|
|
100
|
+
* This assertion deliberately does not verify the cryptographic signature.
|
|
101
|
+
* Signature, issuer, status and trust-chain verification remain the verifier's
|
|
102
|
+
* responsibility. It prevents a validly shaped schema.org
|
|
103
|
+
* OrganizationCredential from being merely serialized as JWT and mislabeled
|
|
104
|
+
* as the distinct Gaia-X participant credential.
|
|
105
|
+
*
|
|
106
|
+
* Participant attachments require `gx:LegalPerson` with the ICAM 25.11 legal
|
|
107
|
+
* properties. Service-offering attachments require `gx:ServiceOffering`,
|
|
108
|
+
* `gx:providedBy` and `gx:serviceOfferingTermsAndConditions`.
|
|
109
|
+
*
|
|
110
|
+
* This validates member-level `data[].attachments[]`; it does not describe
|
|
111
|
+
* `_retrieve?format=vc+jwt` or credential-internal
|
|
112
|
+
* `credential.evidence[].attachments`.
|
|
113
|
+
*
|
|
114
|
+
* @see https://docs.gaia-x.eu/technical-committee/identity-credential-access-management/25.11/gaia-x_credentials/
|
|
115
|
+
* @see https://docs.gaia-x.eu/technical-committee/identity-credential-access-management/25.11/semantic_model/
|
|
116
|
+
*/
|
|
117
|
+
export declare function assertGaiaXDiscoveryAttachmentSemantics(jwt: string, role: GaiaXCredentialAttachmentRoleValue): void;
|
|
97
118
|
/**
|
|
98
119
|
* Assembles one ICA member discovery entry and enforces the interoperable
|
|
99
120
|
* ordering contract: schema.org OrganizationCredential first in `vc[]`, and
|
|
@@ -174,14 +174,90 @@ export function buildGaiaXParticipantAttachment(input) {
|
|
|
174
174
|
}
|
|
175
175
|
/** Wraps an exact signed Gaia-X VC-JWT without decoding or re-signing it. */
|
|
176
176
|
export function buildGaiaXVcJwtAttachment(input) {
|
|
177
|
+
const jwt = requiredInput(input.jwt, 'VC-JWT');
|
|
178
|
+
assertGaiaXDiscoveryAttachmentSemantics(jwt, input.role);
|
|
177
179
|
return {
|
|
178
180
|
id: requiredInput(input.id, 'attachment id'),
|
|
179
181
|
format: GaiaXCredentialAttachmentFormat,
|
|
180
182
|
role: input.role,
|
|
181
183
|
media_type: GaiaXCredentialMediaType.VcJwt,
|
|
182
|
-
data: { json: { jwt
|
|
184
|
+
data: { json: { jwt } },
|
|
183
185
|
};
|
|
184
186
|
}
|
|
187
|
+
function decodeVcJwtCredential(jwt) {
|
|
188
|
+
const parts = jwt.split('.');
|
|
189
|
+
if (parts.length !== 3 || parts.some((part) => !part)) {
|
|
190
|
+
throw new Error('Gaia-X attachment must contain one compact three-part VC-JWT.');
|
|
191
|
+
}
|
|
192
|
+
let payload;
|
|
193
|
+
try {
|
|
194
|
+
payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw new Error('Gaia-X attachment VC-JWT payload is not valid base64url JSON.');
|
|
198
|
+
}
|
|
199
|
+
return asObject(payload.vc) || payload;
|
|
200
|
+
}
|
|
201
|
+
function gaiaXCredentialSubject(document) {
|
|
202
|
+
const raw = document.credentialSubject;
|
|
203
|
+
if (Array.isArray(raw)) {
|
|
204
|
+
return raw.map(asObject).find((value) => Boolean(value));
|
|
205
|
+
}
|
|
206
|
+
return asObject(raw);
|
|
207
|
+
}
|
|
208
|
+
function requireGaiaXProperties(subject, names, role) {
|
|
209
|
+
const missing = names.filter((name) => !(name in subject));
|
|
210
|
+
if (missing.length) {
|
|
211
|
+
throw new Error(`Gaia-X ${role} VC-JWT is missing required semantic properties: ${missing.join(', ')}.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Enforces the semantic contract of a signed Gaia-X discovery VC-JWT.
|
|
216
|
+
*
|
|
217
|
+
* This assertion deliberately does not verify the cryptographic signature.
|
|
218
|
+
* Signature, issuer, status and trust-chain verification remain the verifier's
|
|
219
|
+
* responsibility. It prevents a validly shaped schema.org
|
|
220
|
+
* OrganizationCredential from being merely serialized as JWT and mislabeled
|
|
221
|
+
* as the distinct Gaia-X participant credential.
|
|
222
|
+
*
|
|
223
|
+
* Participant attachments require `gx:LegalPerson` with the ICAM 25.11 legal
|
|
224
|
+
* properties. Service-offering attachments require `gx:ServiceOffering`,
|
|
225
|
+
* `gx:providedBy` and `gx:serviceOfferingTermsAndConditions`.
|
|
226
|
+
*
|
|
227
|
+
* This validates member-level `data[].attachments[]`; it does not describe
|
|
228
|
+
* `_retrieve?format=vc+jwt` or credential-internal
|
|
229
|
+
* `credential.evidence[].attachments`.
|
|
230
|
+
*
|
|
231
|
+
* @see https://docs.gaia-x.eu/technical-committee/identity-credential-access-management/25.11/gaia-x_credentials/
|
|
232
|
+
* @see https://docs.gaia-x.eu/technical-committee/identity-credential-access-management/25.11/semantic_model/
|
|
233
|
+
*/
|
|
234
|
+
export function assertGaiaXDiscoveryAttachmentSemantics(jwt, role) {
|
|
235
|
+
const document = decodeVcJwtCredential(jwt);
|
|
236
|
+
const subject = gaiaXCredentialSubject(document);
|
|
237
|
+
if (!subject)
|
|
238
|
+
throw new Error(`Gaia-X ${role} VC-JWT requires one credentialSubject object.`);
|
|
239
|
+
const subjectType = asString(subject.type);
|
|
240
|
+
if (role === GaiaXCredentialAttachmentRole.Participant) {
|
|
241
|
+
if (subjectType !== 'gx:LegalPerson') {
|
|
242
|
+
throw new Error('Gaia-X participant VC-JWT credentialSubject.type must be gx:LegalPerson.');
|
|
243
|
+
}
|
|
244
|
+
requireGaiaXProperties(subject, [
|
|
245
|
+
'gx:legalName',
|
|
246
|
+
'gx:legalRegistrationNumber',
|
|
247
|
+
'gx:headquarterAddress',
|
|
248
|
+
'gx:legalAddress',
|
|
249
|
+
], 'participant');
|
|
250
|
+
}
|
|
251
|
+
if (role === GaiaXCredentialAttachmentRole.ServiceOffering) {
|
|
252
|
+
if (subjectType !== 'gx:ServiceOffering') {
|
|
253
|
+
throw new Error('Gaia-X service-offering VC-JWT credentialSubject.type must be gx:ServiceOffering.');
|
|
254
|
+
}
|
|
255
|
+
requireGaiaXProperties(subject, [
|
|
256
|
+
'gx:providedBy',
|
|
257
|
+
'gx:serviceOfferingTermsAndConditions',
|
|
258
|
+
], 'service-offering');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
185
261
|
/**
|
|
186
262
|
* Assembles one ICA member discovery entry and enforces the interoperable
|
|
187
263
|
* ordering contract: schema.org OrganizationCredential first in `vc[]`, and
|
|
@@ -88,9 +88,7 @@ export declare const EXAMPLE_CONSENT_GRANT_INPUT: {
|
|
|
88
88
|
};
|
|
89
89
|
export declare const EXAMPLE_LIVE_CONSENT_GRANT_INPUT: {
|
|
90
90
|
readonly subjectDid: string;
|
|
91
|
-
readonly
|
|
92
|
-
readonly identifier: "did:web:api.acme.org";
|
|
93
|
-
};
|
|
91
|
+
readonly actorId: string;
|
|
94
92
|
readonly actorRole: "ISCO-08|2211";
|
|
95
93
|
readonly purpose: "TREAT";
|
|
96
94
|
readonly actions: readonly [string];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
-
import { EXAMPLE_CONTROLLER_DID,
|
|
2
|
+
import { EXAMPLE_CONTROLLER_DID, EXAMPLE_CLINICAL_CODE_PROBLEM, EXAMPLE_CLINICAL_DATE_RANGE, EXAMPLE_CLINICAL_SECTION_ALLERGIES, EXAMPLE_CLINICAL_SECTION_PATIENT_SUMMARY, EXAMPLE_CONSENT_PURPOSE_TREATMENT, EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL, EXAMPLE_GENERIC_SUBJECT_DID, EXAMPLE_HEALTHCARE_ACTOR_ROLE_GENERALIST_MEDICAL_PRACTITIONER, EXAMPLE_HEALTHCARE_JURISDICTION, EXAMPLE_HEALTHCARE_ROLE_PHYSICIAN_TEXT, EXAMPLE_PRACTITIONER_DID, EXAMPLE_PROFESSIONAL_DID, EXAMPLE_SUBJECT_DID, EXAMPLE_FORM_CONTROLLER_PHONE, } from './shared.js';
|
|
3
3
|
export const EXAMPLE_INDIVIDUAL_ORGANIZATION_START_INPUT = {
|
|
4
4
|
alternateName: 'ana',
|
|
5
5
|
controllerEmail: EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL,
|
|
@@ -57,8 +57,8 @@ export const EXAMPLE_CONSENT_GRANT_INPUT = {
|
|
|
57
57
|
};
|
|
58
58
|
export const EXAMPLE_LIVE_CONSENT_GRANT_INPUT = {
|
|
59
59
|
subjectDid: EXAMPLE_SUBJECT_DID,
|
|
60
|
-
|
|
61
|
-
actorRole:
|
|
60
|
+
actorId: EXAMPLE_PROFESSIONAL_DID,
|
|
61
|
+
actorRole: EXAMPLE_HEALTHCARE_ACTOR_ROLE_GENERALIST_MEDICAL_PRACTITIONER,
|
|
62
62
|
purpose: EXAMPLE_CONSENT_PURPOSE_TREATMENT,
|
|
63
63
|
actions: [EXAMPLE_CLINICAL_SECTION_ALLERGIES],
|
|
64
64
|
};
|
|
@@ -75,36 +75,36 @@ export declare const EXAMPLE_SEARCH_CLINICAL_BUNDLE_INPUT: {
|
|
|
75
75
|
*
|
|
76
76
|
* - use `EXAMPLE_TOKEN_EXCHANGE_SMART_INPUT` or `EXAMPLE_OPENID_SMART_TOKEN_INPUT`
|
|
77
77
|
* for the first read-only examples
|
|
78
|
-
* -
|
|
79
|
-
*
|
|
78
|
+
* - do not append `organization/Consent.cruds` to a clinical read unless a
|
|
79
|
+
* separate rule explicitly grants that resource capability
|
|
80
80
|
*/
|
|
81
81
|
export declare const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS: Readonly<{
|
|
82
82
|
readonly physicianAllergiesRead: {
|
|
83
83
|
readonly actorRole: "ISCO-08|2211";
|
|
84
84
|
readonly purpose: "TREAT";
|
|
85
85
|
readonly consentActions: readonly [string];
|
|
86
|
-
readonly smartScopes: readonly [string
|
|
86
|
+
readonly smartScopes: readonly [string];
|
|
87
87
|
readonly includedTypes: readonly ["Composition", "AllergyIntolerance", "DocumentReference"];
|
|
88
88
|
};
|
|
89
89
|
readonly nursingMedicationRead: {
|
|
90
90
|
readonly actorRole: "ISCO-08|2221";
|
|
91
91
|
readonly purpose: "TREAT";
|
|
92
92
|
readonly consentActions: readonly [string];
|
|
93
|
-
readonly smartScopes: readonly [string
|
|
93
|
+
readonly smartScopes: readonly [string];
|
|
94
94
|
readonly includedTypes: readonly ["Composition", "MedicationStatement", "DocumentReference"];
|
|
95
95
|
};
|
|
96
96
|
readonly paramedicEmergencySummaryRead: {
|
|
97
97
|
readonly actorRole: "ISCO-08|2240";
|
|
98
98
|
readonly purpose: "ETREAT";
|
|
99
99
|
readonly consentActions: readonly [string];
|
|
100
|
-
readonly smartScopes: readonly [string
|
|
100
|
+
readonly smartScopes: readonly [string];
|
|
101
101
|
readonly includedTypes: readonly ["Composition", "DocumentReference", "Observation"];
|
|
102
102
|
};
|
|
103
103
|
readonly physicianResultsAndProblemsRead: {
|
|
104
104
|
readonly actorRole: "ISCO-08|2211";
|
|
105
105
|
readonly purpose: "TREAT";
|
|
106
106
|
readonly consentActions: readonly [string, string];
|
|
107
|
-
readonly smartScopes: readonly [string
|
|
107
|
+
readonly smartScopes: readonly [string];
|
|
108
108
|
readonly includedTypes: readonly ["Composition", "Condition", "DiagnosticReport", "DocumentReference"];
|
|
109
109
|
};
|
|
110
110
|
}>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
-
import { HealthcareActorRoles, HealthcareBasicSections, HealthcareConsentPurposes, } from '../constants/healthcare.js';
|
|
2
|
+
import { HealthcareActorRoles, HealthcareBasicSections, HealthcareConsentActions, HealthcareConsentPurposes, } from '../constants/healthcare.js';
|
|
3
3
|
import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
|
|
4
|
-
import { SmartGatewayScopesFhirR4 } from '../constants/smart.js';
|
|
5
4
|
import { buildSmartCompositionReadScope } from '../utils/smart-scope.js';
|
|
6
5
|
import { EXAMPLE_CLINICAL_SECTION_ALLERGIES, EXAMPLE_EMAIL_PROFESSIONAL, EXAMPLE_HEALTHCARE_JURISDICTION, EXAMPLE_PATIENT_DID, EXAMPLE_PROVIDER_ORGANIZATION_URL, EXAMPLE_SUBJECT_DID, } from './shared.js';
|
|
7
6
|
/**
|
|
@@ -12,7 +11,7 @@ import { EXAMPLE_CLINICAL_SECTION_ALLERGIES, EXAMPLE_EMAIL_PROFESSIONAL, EXAMPLE
|
|
|
12
11
|
* - the actor role of the professional (`Physician`, `NursingProfessional`, ...)
|
|
13
12
|
* - the consented action/section over an individual subject
|
|
14
13
|
* - the SMART scope ultimately requested against GW CORE
|
|
15
|
-
* -
|
|
14
|
+
* - a read scope containing only capabilities covered by the consent
|
|
16
15
|
*
|
|
17
16
|
* This differs from organization-controller and individual-owner bootstrap
|
|
18
17
|
* examples, where the main concern is identity/bootstrap rather than
|
|
@@ -28,10 +27,6 @@ const EXAMPLE_CANONICAL_SMART_READ_SCOPES = [
|
|
|
28
27
|
sections: EXAMPLE_CLINICAL_SECTION_ALLERGIES,
|
|
29
28
|
}),
|
|
30
29
|
];
|
|
31
|
-
const EXAMPLE_CANONICAL_SMART_SCOPES = [
|
|
32
|
-
...EXAMPLE_CANONICAL_SMART_READ_SCOPES,
|
|
33
|
-
SmartGatewayScopesFhirR4.ConsentCruds,
|
|
34
|
-
];
|
|
35
30
|
export const EXAMPLE_TOKEN_EXCHANGE_SMART_INPUT = {
|
|
36
31
|
idToken: 'employee-id-token-001',
|
|
37
32
|
scopes: [...EXAMPLE_CANONICAL_SMART_READ_SCOPES],
|
|
@@ -99,20 +94,19 @@ export const EXAMPLE_SEARCH_CLINICAL_BUNDLE_INPUT = {
|
|
|
99
94
|
*
|
|
100
95
|
* - use `EXAMPLE_TOKEN_EXCHANGE_SMART_INPUT` or `EXAMPLE_OPENID_SMART_TOKEN_INPUT`
|
|
101
96
|
* for the first read-only examples
|
|
102
|
-
* -
|
|
103
|
-
*
|
|
97
|
+
* - do not append `organization/Consent.cruds` to a clinical read unless a
|
|
98
|
+
* separate rule explicitly grants that resource capability
|
|
104
99
|
*/
|
|
105
100
|
export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
|
|
106
101
|
physicianAllergiesRead: {
|
|
107
|
-
actorRole: HealthcareActorRoles.
|
|
102
|
+
actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
|
|
108
103
|
purpose: HealthcareConsentPurposes.Treatment,
|
|
109
|
-
consentActions: [
|
|
104
|
+
consentActions: [HealthcareConsentActions.AllergiesAndIntolerances],
|
|
110
105
|
smartScopes: [
|
|
111
106
|
buildSmartCompositionReadScope({
|
|
112
107
|
subjectDid: EXAMPLE_SMART_SUBJECT_DID,
|
|
113
|
-
sections:
|
|
108
|
+
sections: HealthcareConsentActions.AllergiesAndIntolerances,
|
|
114
109
|
}),
|
|
115
|
-
SmartGatewayScopesFhirR4.ConsentCruds,
|
|
116
110
|
],
|
|
117
111
|
includedTypes: [
|
|
118
112
|
ResourceTypesFhirR4.Composition,
|
|
@@ -129,7 +123,6 @@ export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
|
|
|
129
123
|
subjectDid: EXAMPLE_SMART_SUBJECT_DID,
|
|
130
124
|
sections: HealthcareBasicSections.HistoryOfMedicationUse.claim,
|
|
131
125
|
}),
|
|
132
|
-
SmartGatewayScopesFhirR4.ConsentCruds,
|
|
133
126
|
],
|
|
134
127
|
includedTypes: [
|
|
135
128
|
ResourceTypesFhirR4.Composition,
|
|
@@ -146,7 +139,6 @@ export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
|
|
|
146
139
|
subjectDid: EXAMPLE_SMART_SUBJECT_DID,
|
|
147
140
|
sections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
148
141
|
}),
|
|
149
|
-
SmartGatewayScopesFhirR4.ConsentCruds,
|
|
150
142
|
],
|
|
151
143
|
includedTypes: [
|
|
152
144
|
ResourceTypesFhirR4.Composition,
|
|
@@ -155,7 +147,7 @@ export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
|
|
|
155
147
|
],
|
|
156
148
|
},
|
|
157
149
|
physicianResultsAndProblemsRead: {
|
|
158
|
-
actorRole: HealthcareActorRoles.
|
|
150
|
+
actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
|
|
159
151
|
purpose: HealthcareConsentPurposes.Treatment,
|
|
160
152
|
consentActions: [
|
|
161
153
|
HealthcareBasicSections.Results.claim,
|
|
@@ -169,7 +161,6 @@ export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
|
|
|
169
161
|
HealthcareBasicSections.ProblemList.claim,
|
|
170
162
|
],
|
|
171
163
|
}),
|
|
172
|
-
SmartGatewayScopesFhirR4.ConsentCruds,
|
|
173
164
|
],
|
|
174
165
|
includedTypes: [
|
|
175
166
|
ResourceTypesFhirR4.Composition,
|
|
@@ -190,7 +181,6 @@ function buildConsentDecisionScenario(params) {
|
|
|
190
181
|
subjectDid: EXAMPLE_SMART_SUBJECT_DID,
|
|
191
182
|
sections: params.requestedSections,
|
|
192
183
|
}),
|
|
193
|
-
SmartGatewayScopesFhirR4.ConsentCruds,
|
|
194
184
|
],
|
|
195
185
|
includedTypes: [...params.includedTypes],
|
|
196
186
|
expectedSmartTokenDecision: params.expectedSmartTokenDecision,
|
|
@@ -210,4 +200,115 @@ function buildConsentDecisionScenario(params) {
|
|
|
210
200
|
* consent state + actor target + role + purpose + requested scope.
|
|
211
201
|
*/
|
|
212
202
|
export const EXAMPLE_PROFESSIONAL_CONSENT_SCENARIOS = Object.freeze({
|
|
213
|
-
|
|
203
|
+
physicianByEmailContinuousCareAllergiesAllowed: buildConsentDecisionScenario({
|
|
204
|
+
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
205
|
+
actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
|
|
206
|
+
purpose: HealthcareConsentPurposes.Treatment,
|
|
207
|
+
consentActions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
|
|
208
|
+
requestedSections: HealthcareBasicSections.AllergiesAndIntolerances.claim,
|
|
209
|
+
includedTypes: [
|
|
210
|
+
ResourceTypesFhirR4.Composition,
|
|
211
|
+
ResourceTypesFhirR4.AllergyIntolerance,
|
|
212
|
+
ResourceTypesFhirR4.DocumentReference,
|
|
213
|
+
],
|
|
214
|
+
expectedSmartTokenDecision: 'allowed',
|
|
215
|
+
reason: 'physician is targeted directly by email and role for continuous care over allergies section',
|
|
216
|
+
}),
|
|
217
|
+
physicianByEmailEmergencySummaryAllowed: buildConsentDecisionScenario({
|
|
218
|
+
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
219
|
+
actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
|
|
220
|
+
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
221
|
+
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
222
|
+
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
223
|
+
includedTypes: [
|
|
224
|
+
ResourceTypesFhirR4.Composition,
|
|
225
|
+
ResourceTypesFhirR4.DocumentReference,
|
|
226
|
+
ResourceTypesFhirR4.Observation,
|
|
227
|
+
],
|
|
228
|
+
expectedSmartTokenDecision: 'allowed',
|
|
229
|
+
reason: 'physician is targeted directly by email and role for emergency summary access',
|
|
230
|
+
}),
|
|
231
|
+
physicianByOrganizationResultsAllowed: buildConsentDecisionScenario({
|
|
232
|
+
actorId: { organizationUrl: EXAMPLE_PROVIDER_ORG_URL },
|
|
233
|
+
actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
|
|
234
|
+
purpose: HealthcareConsentPurposes.Treatment,
|
|
235
|
+
consentActions: [HealthcareBasicSections.Results.claim],
|
|
236
|
+
requestedSections: HealthcareBasicSections.Results.claim,
|
|
237
|
+
includedTypes: [
|
|
238
|
+
ResourceTypesFhirR4.Composition,
|
|
239
|
+
ResourceTypesFhirR4.DiagnosticReport,
|
|
240
|
+
ResourceTypesFhirR4.DocumentReference,
|
|
241
|
+
],
|
|
242
|
+
expectedSmartTokenDecision: 'allowed',
|
|
243
|
+
reason: 'consent is granted to a physician role within a given organization for continuous care results access',
|
|
244
|
+
}),
|
|
245
|
+
physicianByJurisdictionEmergencySummaryAllowed: buildConsentDecisionScenario({
|
|
246
|
+
actorId: EXAMPLE_JURISDICTION,
|
|
247
|
+
actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
|
|
248
|
+
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
249
|
+
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
250
|
+
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
251
|
+
includedTypes: [
|
|
252
|
+
ResourceTypesFhirR4.Composition,
|
|
253
|
+
ResourceTypesFhirR4.DocumentReference,
|
|
254
|
+
ResourceTypesFhirR4.Observation,
|
|
255
|
+
],
|
|
256
|
+
expectedSmartTokenDecision: 'allowed',
|
|
257
|
+
reason: 'consent is granted at jurisdiction level for physician emergency access',
|
|
258
|
+
}),
|
|
259
|
+
nursingByOrganizationMedicationHistoryAllowed: buildConsentDecisionScenario({
|
|
260
|
+
actorId: { organizationUrl: EXAMPLE_PROVIDER_ORG_URL },
|
|
261
|
+
actorRole: HealthcareActorRoles.NursingProfessional,
|
|
262
|
+
purpose: HealthcareConsentPurposes.Treatment,
|
|
263
|
+
consentActions: [HealthcareBasicSections.HistoryOfMedicationUse.claim],
|
|
264
|
+
requestedSections: HealthcareBasicSections.HistoryOfMedicationUse.claim,
|
|
265
|
+
includedTypes: [
|
|
266
|
+
ResourceTypesFhirR4.Composition,
|
|
267
|
+
ResourceTypesFhirR4.MedicationStatement,
|
|
268
|
+
ResourceTypesFhirR4.DocumentReference,
|
|
269
|
+
],
|
|
270
|
+
expectedSmartTokenDecision: 'allowed',
|
|
271
|
+
reason: 'nursing professional is allowed to read medication history for treatment through organization-scoped consent',
|
|
272
|
+
}),
|
|
273
|
+
paramedicByJurisdictionEmergencySummaryAllowed: buildConsentDecisionScenario({
|
|
274
|
+
actorId: EXAMPLE_JURISDICTION,
|
|
275
|
+
actorRole: HealthcareActorRoles.Paramedic,
|
|
276
|
+
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
277
|
+
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
278
|
+
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
279
|
+
includedTypes: [
|
|
280
|
+
ResourceTypesFhirR4.Composition,
|
|
281
|
+
ResourceTypesFhirR4.DocumentReference,
|
|
282
|
+
ResourceTypesFhirR4.Observation,
|
|
283
|
+
],
|
|
284
|
+
expectedSmartTokenDecision: 'allowed',
|
|
285
|
+
reason: 'paramedic receives emergency-only jurisdiction-scoped access to patient summary',
|
|
286
|
+
}),
|
|
287
|
+
physicianObstetricianDeniedWhenOnlyAllergiesConsent: buildConsentDecisionScenario({
|
|
288
|
+
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
289
|
+
actorRole: `${HealthcareActorRoles.GeneralistMedicalPractitioner}:obstetrician`,
|
|
290
|
+
purpose: HealthcareConsentPurposes.Treatment,
|
|
291
|
+
consentActions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
|
|
292
|
+
requestedSections: HealthcareBasicSections.Results.claim,
|
|
293
|
+
includedTypes: [
|
|
294
|
+
ResourceTypesFhirR4.Composition,
|
|
295
|
+
ResourceTypesFhirR4.DiagnosticReport,
|
|
296
|
+
ResourceTypesFhirR4.DocumentReference,
|
|
297
|
+
],
|
|
298
|
+
expectedSmartTokenDecision: 'denied',
|
|
299
|
+
reason: 'requested SMART scope targets results but active consent only covers allergies section',
|
|
300
|
+
}),
|
|
301
|
+
physicianByEmailDeniedWhenConsentRevokedAndNoOrgNorJurisdictionConsentIsActive: buildConsentDecisionScenario({
|
|
302
|
+
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
303
|
+
actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
|
|
304
|
+
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
305
|
+
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
306
|
+
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
307
|
+
includedTypes: [
|
|
308
|
+
ResourceTypesFhirR4.Composition,
|
|
309
|
+
ResourceTypesFhirR4.DocumentReference,
|
|
310
|
+
],
|
|
311
|
+
expectedSmartTokenDecision: 'denied',
|
|
312
|
+
reason: 'matching consent rule existed before but is no longer active after controller deactivation/revocation',
|
|
313
|
+
}),
|
|
314
|
+
});
|
package/dist/examples/shared.js
CHANGED
|
@@ -128,7 +128,7 @@ export const EXAMPLE_SERVICE_PUBLIC_DID = 'did:web:public.acme.org';
|
|
|
128
128
|
export const EXAMPLE_PROFESSIONAL_DID = buildProfessionalDidWeb({
|
|
129
129
|
organizationDidWeb: 'did:web:api.acme.org',
|
|
130
130
|
email: 'doctor.oncall@example.org',
|
|
131
|
-
role: HealthcareActorRoles.
|
|
131
|
+
role: HealthcareActorRoles.GeneralistMedicalPractitioner,
|
|
132
132
|
});
|
|
133
133
|
export const EXAMPLE_PROVIDER_ORGANIZATION_DID = 'did:web:hospital.acme.org';
|
|
134
134
|
export const EXAMPLE_PROVIDER_ORGANIZATION_URL = 'https://hospital.acme.org';
|
package/dist/models/indexing.js
CHANGED
|
@@ -193,7 +193,7 @@ export function buildIndexParametersFromClaims(claims, allowedClaims) {
|
|
|
193
193
|
const parameter = {
|
|
194
194
|
name: claimKey,
|
|
195
195
|
value: value,
|
|
196
|
-
type:
|
|
196
|
+
type: inferParameterTypeFromClaimKey(claimKey, rawValue),
|
|
197
197
|
...(claimKey === ObservationClaim.ValueQuantityUnit && typeof rawValue === 'string'
|
|
198
198
|
? { unit: rawValue }
|
|
199
199
|
: {}),
|
|
@@ -136,7 +136,13 @@ export class BundleReader {
|
|
|
136
136
|
}
|
|
137
137
|
const entries = this.getEntries();
|
|
138
138
|
for (let index = 0; index < entries.length; index += 1) {
|
|
139
|
-
|
|
139
|
+
const resource = asRecord(entries[index].resource);
|
|
140
|
+
const resourceType = asNonEmptyString(resource.resourceType) || 'resource';
|
|
141
|
+
const candidates = new Set([
|
|
142
|
+
...this.resolveEntryReferenceCandidates(entries[index]),
|
|
143
|
+
`${resourceType}#${index}`,
|
|
144
|
+
]);
|
|
145
|
+
if (candidates.has(normalizedIdentifier)) {
|
|
140
146
|
return index;
|
|
141
147
|
}
|
|
142
148
|
}
|
package/dist/utils/did.d.ts
CHANGED
|
@@ -148,7 +148,13 @@ export declare function buildOrganizationDidWeb(input: {
|
|
|
148
148
|
/**
|
|
149
149
|
* Builds a professional/member DID under a hosted organization DID.
|
|
150
150
|
*
|
|
151
|
-
* The stable actor identifier is derived from the email using
|
|
151
|
+
* The stable actor path identifier is derived from the lower-cased email using
|
|
152
|
+
* multibase(base58btc(multihash(SHA3-256))). The raw email is never embedded
|
|
153
|
+
* in the DID.
|
|
154
|
+
*
|
|
155
|
+
* This is the same multibase payload used by the ICA-compatible credential
|
|
156
|
+
* `sameAs`; only the representation differs: the DID path uses `z...` and the
|
|
157
|
+
* credential alias uses `urn:multibase:z...`.
|
|
152
158
|
*
|
|
153
159
|
* @param input.organizationDidWeb Canonical hosted organization DID.
|
|
154
160
|
* @param input.email Professional email used to derive a stable member identifier.
|
package/dist/utils/did.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// crypto-ts/utils/did.ts
|
|
2
2
|
// Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
3
|
-
import {
|
|
3
|
+
import { multibase58MultihashSha3_256 } from './same-as.js';
|
|
4
4
|
/**
|
|
5
5
|
* Canonical DID path markers for hosted/provider individual identities.
|
|
6
6
|
*
|
|
@@ -240,7 +240,13 @@ export function buildOrganizationDidWeb(input) {
|
|
|
240
240
|
/**
|
|
241
241
|
* Builds a professional/member DID under a hosted organization DID.
|
|
242
242
|
*
|
|
243
|
-
* The stable actor identifier is derived from the email using
|
|
243
|
+
* The stable actor path identifier is derived from the lower-cased email using
|
|
244
|
+
* multibase(base58btc(multihash(SHA3-256))). The raw email is never embedded
|
|
245
|
+
* in the DID.
|
|
246
|
+
*
|
|
247
|
+
* This is the same multibase payload used by the ICA-compatible credential
|
|
248
|
+
* `sameAs`; only the representation differs: the DID path uses `z...` and the
|
|
249
|
+
* credential alias uses `urn:multibase:z...`.
|
|
244
250
|
*
|
|
245
251
|
* @param input.organizationDidWeb Canonical hosted organization DID.
|
|
246
252
|
* @param input.email Professional email used to derive a stable member identifier.
|
|
@@ -254,7 +260,7 @@ export function buildProfessionalDidWeb(input) {
|
|
|
254
260
|
throw new Error('buildProfessionalDidWeb requires email.');
|
|
255
261
|
if (!role)
|
|
256
262
|
throw new Error('buildProfessionalDidWeb requires role.');
|
|
257
|
-
const memberId =
|
|
263
|
+
const memberId = multibase58MultihashSha3_256(normalizedEmail);
|
|
258
264
|
return [
|
|
259
265
|
String(input.organizationDidWeb).trim(),
|
|
260
266
|
'employee',
|
|
@@ -29,6 +29,10 @@ export type ProfessionalSmartVpPayloadInput = Readonly<{
|
|
|
29
29
|
* - otherwise the public employee email
|
|
30
30
|
*
|
|
31
31
|
* When both are present they are merged and deduplicated after normalization.
|
|
32
|
+
* Plain emails become ICA-compatible
|
|
33
|
+
* `urn:multibase:<base58btc(multihash(SHA3-256))>` values. The multibase
|
|
34
|
+
* payload is the same value used in the professional DID actor path; `sameAs`
|
|
35
|
+
* adds only the `urn:multibase:` prefix.
|
|
32
36
|
*
|
|
33
37
|
* @param input Professional identity source values.
|
|
34
38
|
*/
|
|
@@ -11,6 +11,10 @@ import { buildUnsignedVpJwt } from './jwt.js';
|
|
|
11
11
|
* - otherwise the public employee email
|
|
12
12
|
*
|
|
13
13
|
* When both are present they are merged and deduplicated after normalization.
|
|
14
|
+
* Plain emails become ICA-compatible
|
|
15
|
+
* `urn:multibase:<base58btc(multihash(SHA3-256))>` values. The multibase
|
|
16
|
+
* payload is the same value used in the professional DID actor path; `sameAs`
|
|
17
|
+
* adds only the `urn:multibase:` prefix.
|
|
14
18
|
*
|
|
15
19
|
* @param input Professional identity source values.
|
|
16
20
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdc-common-utils-ts",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.7",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
"test": "jest",
|
|
20
20
|
"test:coverage": "jest --coverage",
|
|
21
21
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
22
|
-
"
|
|
22
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
23
|
+
"build": "npm run clean && tsc -p tsconfig.build.json && node patch-esm-imports.mjs",
|
|
24
|
+
"verify:dist": "node scripts/verify-dist-syntax.mjs",
|
|
25
|
+
"prepack": "npm run verify:dist",
|
|
23
26
|
"prepublishOnly": "npm run typecheck && npm test -- --watchman=false && npm run build"
|
|
24
27
|
},
|
|
25
28
|
"main": "./dist/index.js",
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { BundleResourceDateFilter, BundleResourceIdFilters } from './bundle-query';
|
|
2
|
-
/**
|
|
3
|
-
* Canonical high-level filter accepted by both Bundle section readers and
|
|
4
|
-
* `FhirDocumentFacade`.
|
|
5
|
-
*/
|
|
6
|
-
export type ClinicalResourceFilter = Readonly<{
|
|
7
|
-
sections?: string[];
|
|
8
|
-
types?: string[];
|
|
9
|
-
date?: BundleResourceDateFilter;
|
|
10
|
-
}>;
|
|
11
|
-
/**
|
|
12
|
-
* Chainable editor for one clinical document resource filter.
|
|
13
|
-
*
|
|
14
|
-
* The built value can be passed unchanged to:
|
|
15
|
-
*
|
|
16
|
-
* - `BundleReader.getDocumentSectionResourceIds(...)`
|
|
17
|
-
* - `BundleReader.getDocumentSectionResourceEntries(...)`
|
|
18
|
-
* - `FhirDocumentFacade.getResourcesByFilter(...)`
|
|
19
|
-
* - `FhirDocumentFacade.getResourceCount(...)`
|
|
20
|
-
*/
|
|
21
|
-
export declare class ClinicalResourceFilterEditor {
|
|
22
|
-
private sections?;
|
|
23
|
-
private types?;
|
|
24
|
-
private date?;
|
|
25
|
-
/** Replaces the selected Composition sections. An empty list means all. */
|
|
26
|
-
setSections(values: readonly string[]): this;
|
|
27
|
-
/** Replaces the selected FHIR resource types. An empty list means all. */
|
|
28
|
-
setTypes(values: readonly string[]): this;
|
|
29
|
-
/** Sets the complete clinical date interval. Either boundary may be omitted. */
|
|
30
|
-
setPeriod(start?: string, end?: string): this;
|
|
31
|
-
/** Sets or clears only the start boundary of the clinical date interval. */
|
|
32
|
-
setPeriodStart(start?: string): this;
|
|
33
|
-
/** Sets or clears only the end boundary of the clinical date interval. */
|
|
34
|
-
setPeriodEnd(end?: string): this;
|
|
35
|
-
/** Removes both clinical date boundaries. */
|
|
36
|
-
clearPeriod(): this;
|
|
37
|
-
/** Returns a detached canonical filter safe for either public reader. */
|
|
38
|
-
build(): ClinicalResourceFilter & BundleResourceIdFilters;
|
|
39
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
-
/**
|
|
3
|
-
* Chainable editor for one clinical document resource filter.
|
|
4
|
-
*
|
|
5
|
-
* The built value can be passed unchanged to:
|
|
6
|
-
*
|
|
7
|
-
* - `BundleReader.getDocumentSectionResourceIds(...)`
|
|
8
|
-
* - `BundleReader.getDocumentSectionResourceEntries(...)`
|
|
9
|
-
* - `FhirDocumentFacade.getResourcesByFilter(...)`
|
|
10
|
-
* - `FhirDocumentFacade.getResourceCount(...)`
|
|
11
|
-
*/
|
|
12
|
-
export class ClinicalResourceFilterEditor {
|
|
13
|
-
sections;
|
|
14
|
-
types;
|
|
15
|
-
date;
|
|
16
|
-
/** Replaces the selected Composition sections. An empty list means all. */
|
|
17
|
-
setSections(values) {
|
|
18
|
-
this.sections = normalizeTokens(values);
|
|
19
|
-
return this;
|
|
20
|
-
}
|
|
21
|
-
/** Replaces the selected FHIR resource types. An empty list means all. */
|
|
22
|
-
setTypes(values) {
|
|
23
|
-
this.types = normalizeTokens(values);
|
|
24
|
-
return this;
|
|
25
|
-
}
|
|
26
|
-
/** Sets the complete clinical date interval. Either boundary may be omitted. */
|
|
27
|
-
setPeriod(start, end) {
|
|
28
|
-
this.date = buildDateFilter(start, end);
|
|
29
|
-
return this;
|
|
30
|
-
}
|
|
31
|
-
/** Sets or clears only the start boundary of the clinical date interval. */
|
|
32
|
-
setPeriodStart(start) {
|
|
33
|
-
this.date = buildDateFilter(start, this.date?.end);
|
|
34
|
-
return this;
|
|
35
|
-
}
|
|
36
|
-
/** Sets or clears only the end boundary of the clinical date interval. */
|
|
37
|
-
setPeriodEnd(end) {
|
|
38
|
-
this.date = buildDateFilter(this.date?.start, end);
|
|
39
|
-
return this;
|
|
40
|
-
}
|
|
41
|
-
/** Removes both clinical date boundaries. */
|
|
42
|
-
clearPeriod() {
|
|
43
|
-
this.date = undefined;
|
|
44
|
-
return this;
|
|
45
|
-
}
|
|
46
|
-
/** Returns a detached canonical filter safe for either public reader. */
|
|
47
|
-
build() {
|
|
48
|
-
return {
|
|
49
|
-
...(this.sections?.length ? { sections: [...this.sections] } : {}),
|
|
50
|
-
...(this.types?.length ? { types: [...this.types] } : {}),
|
|
51
|
-
...(this.date ? { date: { ...this.date } } : {}),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function normalizeTokens(values) {
|
|
56
|
-
const normalized = [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
|
|
57
|
-
return normalized.length ? normalized : undefined;
|
|
58
|
-
}
|
|
59
|
-
function buildDateFilter(start, end) {
|
|
60
|
-
const normalizedStart = normalizeDate(start, 'start');
|
|
61
|
-
const normalizedEnd = normalizeDate(end, 'end');
|
|
62
|
-
if (normalizedStart && normalizedEnd) {
|
|
63
|
-
if (Date.parse(normalizedStart) > Date.parse(normalizedEnd)) {
|
|
64
|
-
throw new RangeError('Clinical resource filter period start must not be after end.');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (!normalizedStart && !normalizedEnd) {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
...(normalizedStart ? { start: normalizedStart } : {}),
|
|
72
|
-
...(normalizedEnd ? { end: normalizedEnd } : {}),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function normalizeDate(value, boundary) {
|
|
76
|
-
const normalized = String(value || '').trim();
|
|
77
|
-
if (!normalized) {
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
if (!Number.isFinite(Date.parse(normalized))) {
|
|
81
|
-
throw new TypeError(`Clinical resource filter period ${boundary} must be a valid date.`);
|
|
82
|
-
}
|
|
83
|
-
return normalized;
|
|
84
|
-
}
|