gdc-sdk-node-ts 2.9.19 → 2.9.21
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 +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/organization-registration-authorization.d.ts +16 -0
- package/dist/organization-registration-authorization.js +34 -0
- package/dist/server-profile-session.d.ts +2 -0
- package/dist/server-profile-session.js +49 -6
- package/dist/terminology-service-client.d.ts +98 -0
- package/dist/terminology-service-client.js +225 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -663,6 +663,48 @@ export async function GET(request: Request) {
|
|
|
663
663
|
The application validates which systems are allowed for the specific coded
|
|
664
664
|
form field. This MVP service performs no external terminology request.
|
|
665
665
|
|
|
666
|
+
### Governed ValueSet options through the terminology service
|
|
667
|
+
|
|
668
|
+
For creation controls, the authenticated BFF can resolve the ValueSet bound to
|
|
669
|
+
a canonical flat claim without exposing its service token or terminology
|
|
670
|
+
server to the browser:
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
import {
|
|
674
|
+
AllergyIntoleranceClaim,
|
|
675
|
+
MemoryTerminologyValueSetCache,
|
|
676
|
+
TerminologyServiceClient,
|
|
677
|
+
} from 'gdc-sdk-node-ts';
|
|
678
|
+
|
|
679
|
+
const terminology = new TerminologyServiceClient({
|
|
680
|
+
baseUrl: process.env.TERMINOLOGY_SERVICE_URL!,
|
|
681
|
+
serviceToken: process.env.TERMINOLOGY_SERVICE_TOKEN!,
|
|
682
|
+
cache: new MemoryTerminologyValueSetCache(),
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const { document, cacheStatus } = await terminology.expandValueSetForClaim({
|
|
686
|
+
sector: 'health-care',
|
|
687
|
+
resourceType: 'AllergyIntolerance',
|
|
688
|
+
claim: AllergyIntoleranceClaim.Manifestation,
|
|
689
|
+
// Optional: language, jurisdiction, version, fhirVersion, offset and count.
|
|
690
|
+
});
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Omission means English, international/default terminology, the latest
|
|
694
|
+
installed terminology version and FHIR R4. Jurisdiction is a SNOMED edition
|
|
695
|
+
selector; it does not become part of LOINC or WHO ATC identity. The cache key
|
|
696
|
+
still separates language, SNOMED edition/version, claim and page.
|
|
697
|
+
|
|
698
|
+
`MemoryTerminologyValueSetCache.prime(...)` accepts immutable startup
|
|
699
|
+
snapshots. This permits an application to ship or load the English IPS page
|
|
700
|
+
from private storage and render it without a live terminology call. A local
|
|
701
|
+
language page is preferred when cached; if refresh fails, an English cached
|
|
702
|
+
page is returned with `cacheStatus: 'english-fallback-cache'`. Complete
|
|
703
|
+
catalogs and the service bearer credential remain server-side. A BFF may
|
|
704
|
+
replace the memory implementation with a GCS, Firestore or PostgreSQL adapter
|
|
705
|
+
implementing `TerminologyValueSetCache`; large complete catalogs should not be
|
|
706
|
+
stored in one Firestore document.
|
|
707
|
+
|
|
666
708
|
## API Index
|
|
667
709
|
|
|
668
710
|
## Full Public Surface
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from 'gdc-sdk-core-ts';
|
|
2
|
+
export { AllergyIntoleranceClaim } from 'gdc-common-utils-ts/models/interoperable-claims/allergy-intolerance-claims';
|
|
2
3
|
export * from './runtime-contracts.js';
|
|
3
4
|
export * from './identity-bootstrap.js';
|
|
4
5
|
export * from './async-polling.js';
|
|
@@ -51,4 +52,5 @@ export * from './orchestration/professional-sdk.js';
|
|
|
51
52
|
export * from './orchestration/digital-twin-sdk.js';
|
|
52
53
|
export * from './legacy-compat.js';
|
|
53
54
|
export * from './local-terminology-bff.js';
|
|
55
|
+
export * from './terminology-service-client.js';
|
|
54
56
|
export * from './pdqm-patient-match.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
2
|
export * from 'gdc-sdk-core-ts';
|
|
3
|
+
// Explicitly use the direct common-utils release for newly promoted canonical claims.
|
|
4
|
+
export { AllergyIntoleranceClaim } from 'gdc-common-utils-ts/models/interoperable-claims/allergy-intolerance-claims';
|
|
3
5
|
export * from './runtime-contracts.js';
|
|
4
6
|
export * from './identity-bootstrap.js';
|
|
5
7
|
export * from './async-polling.js';
|
|
@@ -52,4 +54,5 @@ export * from './orchestration/professional-sdk.js';
|
|
|
52
54
|
export * from './orchestration/digital-twin-sdk.js';
|
|
53
55
|
export * from './legacy-compat.js';
|
|
54
56
|
export * from './local-terminology-bff.js';
|
|
57
|
+
export * from './terminology-service-client.js';
|
|
55
58
|
export * from './pdqm-patient-match.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { VerifiableCredentialV2 } from 'gdc-common-utils-ts/models/verifiable-credential';
|
|
2
|
+
import type { IWallet, WalletExecutionContext, WalletKeySelection } from 'gdc-sdk-core-ts';
|
|
3
|
+
/** Input required to add one human-authorizer proof to the out-of-band VC. */
|
|
4
|
+
export type SignOrganizationRegistrationAuthorizationInput = Readonly<{
|
|
5
|
+
credential: VerifiableCredentialV2;
|
|
6
|
+
wallet: IWallet;
|
|
7
|
+
context: WalletExecutionContext;
|
|
8
|
+
key: WalletKeySelection;
|
|
9
|
+
verificationMethod: string;
|
|
10
|
+
createdAt?: string;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Adds one detached JWS proof made by the already-unlocked professional
|
|
14
|
+
* profile. The browser never receives the private key or clear wallet seed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function signOrganizationRegistrationAuthorizationCredential(input: SignOrganizationRegistrationAuthorizationInput): Promise<VerifiableCredentialV2>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { canonicalizeOrganizationRegistrationAuthorizationCredential, } from 'gdc-common-utils-ts/utils/organization-registration-authorization';
|
|
2
|
+
/**
|
|
3
|
+
* Adds one detached JWS proof made by the already-unlocked professional
|
|
4
|
+
* profile. The browser never receives the private key or clear wallet seed.
|
|
5
|
+
*/
|
|
6
|
+
export async function signOrganizationRegistrationAuthorizationCredential(input) {
|
|
7
|
+
if (!input.wallet.signDetachedJws) {
|
|
8
|
+
throw new Error('Wallet does not support detached organization authorization proofs.');
|
|
9
|
+
}
|
|
10
|
+
const verificationMethod = String(input.verificationMethod || '').trim();
|
|
11
|
+
if (!verificationMethod)
|
|
12
|
+
throw new Error('Organization authorization proof requires verificationMethod.');
|
|
13
|
+
const payload = canonicalizeOrganizationRegistrationAuthorizationCredential(input.credential);
|
|
14
|
+
const jws = await input.wallet.signDetachedJws(input.context, {
|
|
15
|
+
payload,
|
|
16
|
+
header: { typ: 'application/vc+ld+json' },
|
|
17
|
+
key: input.key,
|
|
18
|
+
});
|
|
19
|
+
const existingProofs = Array.isArray(input.credential.proof)
|
|
20
|
+
? input.credential.proof
|
|
21
|
+
: input.credential.proof
|
|
22
|
+
? [input.credential.proof]
|
|
23
|
+
: [];
|
|
24
|
+
return {
|
|
25
|
+
...input.credential,
|
|
26
|
+
proof: [...existingProofs, {
|
|
27
|
+
type: 'JsonWebSignature2020',
|
|
28
|
+
created: input.createdAt || new Date().toISOString(),
|
|
29
|
+
proofPurpose: 'contractAgreement',
|
|
30
|
+
verificationMethod,
|
|
31
|
+
jws,
|
|
32
|
+
}],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -286,6 +286,8 @@ export type ServerAuthorizedSubjectSelectionInput = Readonly<{
|
|
|
286
286
|
/** Server-owned relationship projection for one directory subject. */
|
|
287
287
|
export type ServerAuthorizedSubjectGrant = Readonly<{
|
|
288
288
|
subjectDid: string;
|
|
289
|
+
/** Canonical private subject used for SMART when the directory exposes a public card alias. */
|
|
290
|
+
transportSubjectDid?: string;
|
|
289
291
|
actorMode: ServerActorMode;
|
|
290
292
|
attester?: ServerProfileAttester;
|
|
291
293
|
}>;
|
|
@@ -3,6 +3,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
3
3
|
import { ActorKinds } from 'gdc-common-utils-ts/constants/actor-session';
|
|
4
4
|
import { UrnPrefixes } from 'gdc-common-utils-ts/constants/urn';
|
|
5
5
|
import { normalizeUuid } from 'gdc-common-utils-ts/utils/normalize-uuid';
|
|
6
|
+
import { parseIndividualMemberDidWeb } from 'gdc-common-utils-ts/utils/did';
|
|
6
7
|
import { selectRelatedPersonListRecord, } from 'gdc-common-utils-ts/utils/related-person-list';
|
|
7
8
|
import { CompositionAttesterModes, } from 'gdc-common-utils-ts/models/interoperable-claims/composition-claims';
|
|
8
9
|
import { FhirIpsCreatorKinds, normalizeClinicalCreatorBinding, } from 'gdc-common-utils-ts/utils/fhir-ips-creator-identity';
|
|
@@ -619,8 +620,25 @@ export class ServerProfileSessionManager {
|
|
|
619
620
|
const walletState = await this.resolveWalletState(input.ownerId, input.sessionId);
|
|
620
621
|
const client = this.createRegisteredProfileClient(walletState.profile, walletState.wallet, walletState.context, idToken);
|
|
621
622
|
const subjects = await client.listAuthorizedIndividualSubjects(walletState.profile.routeContext, { verifiedContact: input.verifiedContact });
|
|
622
|
-
const
|
|
623
|
-
const
|
|
623
|
+
const directoryGrants = subjects.map(toServerAuthorizedSubjectGrant);
|
|
624
|
+
const canonicalControllerSubject = canonicalIndividualControllerSubject(actorSession.profile);
|
|
625
|
+
const enrolledSubjects = new Set(actorSession.profile.allowedSubjectDids);
|
|
626
|
+
const ownPublicGrant = canonicalControllerSubject && enrolledSubjects.has(canonicalControllerSubject)
|
|
627
|
+
? directoryGrants.find((grant) => grant.subjectDid !== canonicalControllerSubject
|
|
628
|
+
&& enrolledSubjects.has(grant.subjectDid)
|
|
629
|
+
&& (grant.actorMode === 'self' || grant.actorMode === 'controller'))
|
|
630
|
+
: undefined;
|
|
631
|
+
const authorizedSubjects = ownPublicGrant && canonicalControllerSubject
|
|
632
|
+
? [
|
|
633
|
+
...directoryGrants.map((grant) => grant === ownPublicGrant
|
|
634
|
+
? { ...grant, transportSubjectDid: canonicalControllerSubject }
|
|
635
|
+
: grant),
|
|
636
|
+
...(directoryGrants.some((grant) => grant.subjectDid === canonicalControllerSubject)
|
|
637
|
+
? []
|
|
638
|
+
: [{ ...ownPublicGrant, subjectDid: canonicalControllerSubject }]),
|
|
639
|
+
]
|
|
640
|
+
: directoryGrants;
|
|
641
|
+
const authorizedSubjectDids = unique(authorizedSubjects.map((subject) => subject.subjectDid));
|
|
624
642
|
const updatedProfile = {
|
|
625
643
|
...actorSession.profile,
|
|
626
644
|
allowedSubjectDids: authorizedSubjectDids,
|
|
@@ -651,18 +669,19 @@ export class ServerProfileSessionManager {
|
|
|
651
669
|
const stored = await this.options.store.getSession(input.sessionId);
|
|
652
670
|
if (!stored || stored.ownerId !== input.ownerId)
|
|
653
671
|
throw new Error('Profile session not found.');
|
|
654
|
-
const
|
|
655
|
-
if (!
|
|
672
|
+
const requestedSubjectDid = String(input.subjectDid || '').trim();
|
|
673
|
+
if (!requestedSubjectDid || !(stored.authorizedSubjectDids || []).includes(requestedSubjectDid)) {
|
|
656
674
|
throw new Error('Subject is not authorized for this personal actor profile.');
|
|
657
675
|
}
|
|
658
|
-
const selectedGrant = stored.authorizedSubjects?.find((grant) => grant.subjectDid ===
|
|
676
|
+
const selectedGrant = stored.authorizedSubjects?.find((grant) => grant.subjectDid === requestedSubjectDid);
|
|
659
677
|
if (!selectedGrant) {
|
|
660
678
|
throw new Error('Subject relationship metadata is missing from this personal actor session.');
|
|
661
679
|
}
|
|
662
680
|
const idToken = String(input.idToken || '').trim();
|
|
663
681
|
if (!idToken)
|
|
664
682
|
throw new Error('Subject selection requires idToken.');
|
|
665
|
-
const
|
|
683
|
+
const subjectDid = String(selectedGrant.transportSubjectDid || requestedSubjectDid).trim();
|
|
684
|
+
const scopes = unique(input.scopes).map((scope) => rewriteSmartScopeSubject(scope, requestedSubjectDid, subjectDid));
|
|
666
685
|
if (!scopes.length)
|
|
667
686
|
throw new Error('Subject selection requires scopes.');
|
|
668
687
|
const walletState = await this.resolveWalletState(input.ownerId, input.sessionId);
|
|
@@ -1151,6 +1170,30 @@ export class ServerProfileSessionManager {
|
|
|
1151
1170
|
}
|
|
1152
1171
|
now() { return this.options.now?.() || new Date(); }
|
|
1153
1172
|
}
|
|
1173
|
+
/** Resolves the private individual DID rooted in one registered controller actor. */
|
|
1174
|
+
function canonicalIndividualControllerSubject(profile) {
|
|
1175
|
+
if (profile.actorKind !== ActorKinds.IndividualController)
|
|
1176
|
+
return undefined;
|
|
1177
|
+
try {
|
|
1178
|
+
return parseIndividualMemberDidWeb(profile.actorDid).individualDidWeb;
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
return undefined;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
/** Rebinds a selected public card alias to its canonical SMART subject. */
|
|
1185
|
+
function rewriteSmartScopeSubject(scope, requestedSubjectDid, transportSubjectDid) {
|
|
1186
|
+
const value = String(scope || '').trim();
|
|
1187
|
+
if (!value || requestedSubjectDid === transportSubjectDid)
|
|
1188
|
+
return value;
|
|
1189
|
+
const [permission, query = ''] = value.split('?', 2);
|
|
1190
|
+
const parameters = new URLSearchParams(query);
|
|
1191
|
+
if (parameters.get('subject') !== requestedSubjectDid) {
|
|
1192
|
+
throw new Error('Every SMART scope must be bound to the selected public subject alias.');
|
|
1193
|
+
}
|
|
1194
|
+
parameters.set('subject', transportSubjectDid);
|
|
1195
|
+
return `${permission}?${parameters.toString()}`;
|
|
1196
|
+
}
|
|
1154
1197
|
/**
|
|
1155
1198
|
* Binds every direct resource action and async poll to the actor and DCR client
|
|
1156
1199
|
* stored in the server profile. The caller may author the business body and
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Input resolved by an authenticated application BFF, never directly by a browser. */
|
|
2
|
+
export type TerminologyValueSetInput = Readonly<{
|
|
3
|
+
/** Optional per-request override. Dedicated BFFs normally configure this once on the client. */
|
|
4
|
+
sector?: string;
|
|
5
|
+
claim: string;
|
|
6
|
+
fhirVersion?: 'R4';
|
|
7
|
+
language?: string;
|
|
8
|
+
jurisdiction?: string;
|
|
9
|
+
/** Exact code-system version URI. Omission asks the service for its latest/default release. */
|
|
10
|
+
version?: string;
|
|
11
|
+
offset?: number;
|
|
12
|
+
count?: number;
|
|
13
|
+
}>;
|
|
14
|
+
export type TerminologyValueSetCodeListInput = Omit<TerminologyValueSetInput, 'offset' | 'count'>;
|
|
15
|
+
export type TerminologyValueSetOption = Readonly<{
|
|
16
|
+
system: string;
|
|
17
|
+
code: string;
|
|
18
|
+
display: string;
|
|
19
|
+
localizedDisplay: string;
|
|
20
|
+
resolvedLanguage: string;
|
|
21
|
+
fallbackUsed: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
export type TerminologyValueSetDocument = Readonly<{
|
|
24
|
+
jsonapi: Readonly<{
|
|
25
|
+
version: '1.1';
|
|
26
|
+
}>;
|
|
27
|
+
data: readonly Readonly<{
|
|
28
|
+
type: 'terminology-option';
|
|
29
|
+
id: string;
|
|
30
|
+
attributes: TerminologyValueSetOption;
|
|
31
|
+
}>[];
|
|
32
|
+
meta: Readonly<{
|
|
33
|
+
count: number;
|
|
34
|
+
total: number;
|
|
35
|
+
offset: number;
|
|
36
|
+
language: string;
|
|
37
|
+
resourceType: string;
|
|
38
|
+
field: string;
|
|
39
|
+
claim: string;
|
|
40
|
+
valueSet: Readonly<{
|
|
41
|
+
id: string;
|
|
42
|
+
url: string;
|
|
43
|
+
version: string;
|
|
44
|
+
system: string;
|
|
45
|
+
}>;
|
|
46
|
+
terminologyVersions?: Readonly<Record<string, string>>;
|
|
47
|
+
}>;
|
|
48
|
+
}>;
|
|
49
|
+
export type TerminologyCacheEntry = Readonly<{
|
|
50
|
+
document: TerminologyValueSetDocument;
|
|
51
|
+
storedAt: number;
|
|
52
|
+
}>;
|
|
53
|
+
export interface TerminologyValueSetCache {
|
|
54
|
+
get(key: string): TerminologyCacheEntry | undefined | Promise<TerminologyCacheEntry | undefined>;
|
|
55
|
+
set(key: string, entry: TerminologyCacheEntry): void | Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/** Stable key usable by in-memory, GCS, Firestore or PostgreSQL cache adapters. */
|
|
58
|
+
export declare function terminologyValueSetCacheKey(input: TerminologyValueSetInput): string;
|
|
59
|
+
/** Process-local cache suitable for warm BFF instances and immutable startup snapshots. */
|
|
60
|
+
export declare class MemoryTerminologyValueSetCache implements TerminologyValueSetCache {
|
|
61
|
+
#private;
|
|
62
|
+
get(key: string): TerminologyCacheEntry | undefined;
|
|
63
|
+
set(key: string, entry: TerminologyCacheEntry): void;
|
|
64
|
+
prime(input: TerminologyValueSetInput, document: TerminologyValueSetDocument, storedAt?: number): void;
|
|
65
|
+
}
|
|
66
|
+
export type TerminologyValueSetResult = Readonly<{
|
|
67
|
+
document: TerminologyValueSetDocument;
|
|
68
|
+
cacheStatus: 'remote' | 'fresh-cache' | 'stale-cache' | 'english-fallback-cache';
|
|
69
|
+
}>;
|
|
70
|
+
/** Frontend-ready code list. Coding system and code remain separate FHIR Coding values. */
|
|
71
|
+
export type TerminologyValueSetCodeList = Readonly<{
|
|
72
|
+
codingSystem: string;
|
|
73
|
+
codeToDisplay: Readonly<Record<string, string>>;
|
|
74
|
+
requestedLanguage: string;
|
|
75
|
+
resolvedLanguage: string;
|
|
76
|
+
fallbackUsed: boolean;
|
|
77
|
+
valueSet: TerminologyValueSetDocument['meta']['valueSet'];
|
|
78
|
+
terminologyVersions?: Readonly<Record<string, string>>;
|
|
79
|
+
}>;
|
|
80
|
+
/** Server-only client for a product-neutral terminology service. */
|
|
81
|
+
export declare class TerminologyServiceClient {
|
|
82
|
+
#private;
|
|
83
|
+
constructor(options: Readonly<{
|
|
84
|
+
baseUrl: string;
|
|
85
|
+
/** Default sector for a dedicated human-health or animal-health BFF. */
|
|
86
|
+
sector?: string;
|
|
87
|
+
serviceToken?: string;
|
|
88
|
+
fetchImplementation?: typeof fetch;
|
|
89
|
+
cache?: TerminologyValueSetCache;
|
|
90
|
+
cacheTtlMs?: number;
|
|
91
|
+
staleIfError?: boolean;
|
|
92
|
+
now?: () => number;
|
|
93
|
+
}>);
|
|
94
|
+
/** Resolves the ValueSet bound by the service to one canonical flat claim. */
|
|
95
|
+
expandValueSetForClaim(input: TerminologyValueSetInput): Promise<TerminologyValueSetResult>;
|
|
96
|
+
/** Resolves every page and returns the simple code-to-display object expected by forms. */
|
|
97
|
+
getValueSetCodeListForClaim(input: TerminologyValueSetCodeListInput): Promise<TerminologyValueSetCodeList>;
|
|
98
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
7
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
|
+
};
|
|
12
|
+
var _MemoryTerminologyValueSetCache_entries, _TerminologyServiceClient_instances, _TerminologyServiceClient_baseUrl, _TerminologyServiceClient_serviceToken, _TerminologyServiceClient_sector, _TerminologyServiceClient_fetch, _TerminologyServiceClient_cache, _TerminologyServiceClient_cacheTtlMs, _TerminologyServiceClient_staleIfError, _TerminologyServiceClient_now, _TerminologyServiceClient_request, _TerminologyServiceClient_validateInput, _TerminologyServiceClient_isDocument;
|
|
13
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
14
|
+
function normalizedInput(input) {
|
|
15
|
+
return {
|
|
16
|
+
claim: input.claim.trim(),
|
|
17
|
+
fhirVersion: input.fhirVersion ?? 'R4',
|
|
18
|
+
language: (input.language?.trim() || 'en').replace(/_/g, '-').toLowerCase(),
|
|
19
|
+
...(input.jurisdiction?.trim() ? { jurisdiction: input.jurisdiction.trim().toUpperCase() } : {}),
|
|
20
|
+
...(input.version?.trim() ? { version: input.version.trim() } : {}),
|
|
21
|
+
offset: input.offset ?? 0,
|
|
22
|
+
count: input.count ?? 50,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Stable key usable by in-memory, GCS, Firestore or PostgreSQL cache adapters. */
|
|
26
|
+
export function terminologyValueSetCacheKey(input) {
|
|
27
|
+
const normalizedTerminologyContext = normalizedInput(input);
|
|
28
|
+
return JSON.stringify([
|
|
29
|
+
normalizedTerminologyContext.fhirVersion,
|
|
30
|
+
input.sector?.trim().toLowerCase() ?? '',
|
|
31
|
+
normalizedTerminologyContext.claim,
|
|
32
|
+
normalizedTerminologyContext.language,
|
|
33
|
+
normalizedTerminologyContext.jurisdiction ?? '',
|
|
34
|
+
normalizedTerminologyContext.version ?? 'latest',
|
|
35
|
+
normalizedTerminologyContext.offset,
|
|
36
|
+
normalizedTerminologyContext.count,
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
function resourceTypeFromClaim(claim) {
|
|
40
|
+
const canonicalClaim = claim.trim();
|
|
41
|
+
const separatorIndex = canonicalClaim.indexOf('.');
|
|
42
|
+
const resourceType = canonicalClaim.slice(0, separatorIndex);
|
|
43
|
+
if (separatorIndex < 1 || !/^[A-Z][A-Za-z0-9]*$/.test(resourceType)) {
|
|
44
|
+
throw new Error('claim must start with its FHIR resource type');
|
|
45
|
+
}
|
|
46
|
+
return resourceType;
|
|
47
|
+
}
|
|
48
|
+
/** Process-local cache suitable for warm BFF instances and immutable startup snapshots. */
|
|
49
|
+
export class MemoryTerminologyValueSetCache {
|
|
50
|
+
constructor() {
|
|
51
|
+
_MemoryTerminologyValueSetCache_entries.set(this, new Map());
|
|
52
|
+
}
|
|
53
|
+
get(key) {
|
|
54
|
+
return __classPrivateFieldGet(this, _MemoryTerminologyValueSetCache_entries, "f").get(key);
|
|
55
|
+
}
|
|
56
|
+
set(key, entry) {
|
|
57
|
+
__classPrivateFieldGet(this, _MemoryTerminologyValueSetCache_entries, "f").set(key, entry);
|
|
58
|
+
}
|
|
59
|
+
prime(input, document, storedAt = Date.now()) {
|
|
60
|
+
this.set(terminologyValueSetCacheKey(input), { document, storedAt });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
_MemoryTerminologyValueSetCache_entries = new WeakMap();
|
|
64
|
+
/** Server-only client for a product-neutral terminology service. */
|
|
65
|
+
export class TerminologyServiceClient {
|
|
66
|
+
constructor(options) {
|
|
67
|
+
_TerminologyServiceClient_instances.add(this);
|
|
68
|
+
_TerminologyServiceClient_baseUrl.set(this, void 0);
|
|
69
|
+
_TerminologyServiceClient_serviceToken.set(this, void 0);
|
|
70
|
+
_TerminologyServiceClient_sector.set(this, void 0);
|
|
71
|
+
_TerminologyServiceClient_fetch.set(this, void 0);
|
|
72
|
+
_TerminologyServiceClient_cache.set(this, void 0);
|
|
73
|
+
_TerminologyServiceClient_cacheTtlMs.set(this, void 0);
|
|
74
|
+
_TerminologyServiceClient_staleIfError.set(this, void 0);
|
|
75
|
+
_TerminologyServiceClient_now.set(this, void 0);
|
|
76
|
+
__classPrivateFieldSet(this, _TerminologyServiceClient_baseUrl, options.baseUrl.replace(/\/$/, ''), "f");
|
|
77
|
+
__classPrivateFieldSet(this, _TerminologyServiceClient_serviceToken, options.serviceToken?.trim() || undefined, "f");
|
|
78
|
+
__classPrivateFieldSet(this, _TerminologyServiceClient_sector, options.sector?.trim() || undefined, "f");
|
|
79
|
+
__classPrivateFieldSet(this, _TerminologyServiceClient_fetch, options.fetchImplementation ?? fetch, "f");
|
|
80
|
+
__classPrivateFieldSet(this, _TerminologyServiceClient_cache, options.cache ?? new MemoryTerminologyValueSetCache(), "f");
|
|
81
|
+
__classPrivateFieldSet(this, _TerminologyServiceClient_cacheTtlMs, options.cacheTtlMs ?? DEFAULT_TTL_MS, "f");
|
|
82
|
+
__classPrivateFieldSet(this, _TerminologyServiceClient_staleIfError, options.staleIfError ?? true, "f");
|
|
83
|
+
__classPrivateFieldSet(this, _TerminologyServiceClient_now, options.now ?? Date.now, "f");
|
|
84
|
+
}
|
|
85
|
+
/** Resolves the ValueSet bound by the service to one canonical flat claim. */
|
|
86
|
+
async expandValueSetForClaim(input) {
|
|
87
|
+
__classPrivateFieldGet(this, _TerminologyServiceClient_instances, "m", _TerminologyServiceClient_validateInput).call(this, input);
|
|
88
|
+
const resolvedTerminologyContext = {
|
|
89
|
+
...input,
|
|
90
|
+
sector: input.sector?.trim() || __classPrivateFieldGet(this, _TerminologyServiceClient_sector, "f"),
|
|
91
|
+
};
|
|
92
|
+
const key = terminologyValueSetCacheKey(resolvedTerminologyContext);
|
|
93
|
+
const cached = await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").get(key);
|
|
94
|
+
if (cached && __classPrivateFieldGet(this, _TerminologyServiceClient_now, "f").call(this) - cached.storedAt <= __classPrivateFieldGet(this, _TerminologyServiceClient_cacheTtlMs, "f")) {
|
|
95
|
+
return { document: cached.document, cacheStatus: 'fresh-cache' };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const document = await __classPrivateFieldGet(this, _TerminologyServiceClient_instances, "m", _TerminologyServiceClient_request).call(this, resolvedTerminologyContext);
|
|
99
|
+
await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").set(key, { document, storedAt: __classPrivateFieldGet(this, _TerminologyServiceClient_now, "f").call(this) });
|
|
100
|
+
return { document, cacheStatus: 'remote' };
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (cached && __classPrivateFieldGet(this, _TerminologyServiceClient_staleIfError, "f")) {
|
|
104
|
+
return { document: cached.document, cacheStatus: 'stale-cache' };
|
|
105
|
+
}
|
|
106
|
+
const requestedLanguage = input.language?.trim().toLowerCase();
|
|
107
|
+
if (__classPrivateFieldGet(this, _TerminologyServiceClient_staleIfError, "f") && requestedLanguage && requestedLanguage !== 'en') {
|
|
108
|
+
const english = await __classPrivateFieldGet(this, _TerminologyServiceClient_cache, "f").get(terminologyValueSetCacheKey({
|
|
109
|
+
...resolvedTerminologyContext,
|
|
110
|
+
language: 'en',
|
|
111
|
+
}));
|
|
112
|
+
if (english) {
|
|
113
|
+
return { document: english.document, cacheStatus: 'english-fallback-cache' };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new Error('Terminology service request failed');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Resolves every page and returns the simple code-to-display object expected by forms. */
|
|
120
|
+
async getValueSetCodeListForClaim(input) {
|
|
121
|
+
const pageSize = 100;
|
|
122
|
+
let nextOffset = 0;
|
|
123
|
+
let totalOptionCount = 0;
|
|
124
|
+
let codingSystem = '';
|
|
125
|
+
let resolvedLanguage = '';
|
|
126
|
+
let fallbackUsed = false;
|
|
127
|
+
let resolvedValueSet;
|
|
128
|
+
let terminologyVersions;
|
|
129
|
+
const codeToDisplay = {};
|
|
130
|
+
do {
|
|
131
|
+
const valueSetPage = await this.expandValueSetForClaim({
|
|
132
|
+
...input,
|
|
133
|
+
offset: nextOffset,
|
|
134
|
+
count: pageSize,
|
|
135
|
+
});
|
|
136
|
+
const { document } = valueSetPage;
|
|
137
|
+
totalOptionCount = document.meta.total;
|
|
138
|
+
resolvedLanguage || (resolvedLanguage = document.meta.language);
|
|
139
|
+
resolvedValueSet ?? (resolvedValueSet = document.meta.valueSet);
|
|
140
|
+
terminologyVersions ?? (terminologyVersions = document.meta.terminologyVersions);
|
|
141
|
+
if (resolvedValueSet.system !== document.meta.valueSet.system) {
|
|
142
|
+
throw new Error('Terminology service returned inconsistent ValueSet pages');
|
|
143
|
+
}
|
|
144
|
+
for (const terminologyOptionResource of document.data) {
|
|
145
|
+
const terminologyOption = terminologyOptionResource.attributes;
|
|
146
|
+
if (codingSystem && codingSystem !== terminologyOption.system) {
|
|
147
|
+
throw new Error('Terminology ValueSet contains more than one coding system');
|
|
148
|
+
}
|
|
149
|
+
codingSystem || (codingSystem = terminologyOption.system);
|
|
150
|
+
codeToDisplay[terminologyOption.code] = terminologyOption.localizedDisplay;
|
|
151
|
+
fallbackUsed || (fallbackUsed = terminologyOption.fallbackUsed);
|
|
152
|
+
}
|
|
153
|
+
if (document.data.length === 0 && nextOffset < totalOptionCount) {
|
|
154
|
+
throw new Error('Terminology service returned an incomplete ValueSet page');
|
|
155
|
+
}
|
|
156
|
+
nextOffset += document.data.length;
|
|
157
|
+
} while (nextOffset < totalOptionCount);
|
|
158
|
+
if (!resolvedValueSet)
|
|
159
|
+
throw new Error('Terminology service returned no ValueSet metadata');
|
|
160
|
+
return {
|
|
161
|
+
codingSystem: codingSystem || resolvedValueSet.system,
|
|
162
|
+
codeToDisplay,
|
|
163
|
+
requestedLanguage: (input.language?.trim() || 'en').replace(/_/g, '-').toLowerCase(),
|
|
164
|
+
resolvedLanguage: resolvedLanguage || 'en',
|
|
165
|
+
fallbackUsed,
|
|
166
|
+
valueSet: resolvedValueSet,
|
|
167
|
+
...(terminologyVersions ? { terminologyVersions } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
_TerminologyServiceClient_baseUrl = new WeakMap(), _TerminologyServiceClient_serviceToken = new WeakMap(), _TerminologyServiceClient_sector = new WeakMap(), _TerminologyServiceClient_fetch = new WeakMap(), _TerminologyServiceClient_cache = new WeakMap(), _TerminologyServiceClient_cacheTtlMs = new WeakMap(), _TerminologyServiceClient_staleIfError = new WeakMap(), _TerminologyServiceClient_now = new WeakMap(), _TerminologyServiceClient_instances = new WeakSet(), _TerminologyServiceClient_request = async function _TerminologyServiceClient_request(input) {
|
|
172
|
+
const url = new URL(`${__classPrivateFieldGet(this, _TerminologyServiceClient_baseUrl, "f")}/v1/terminology/value-set-options`);
|
|
173
|
+
url.searchParams.set('sector', input.sector?.trim() || __classPrivateFieldGet(this, _TerminologyServiceClient_sector, "f") || '');
|
|
174
|
+
url.searchParams.set('claim', input.claim.trim());
|
|
175
|
+
url.searchParams.set('offset', String(input.offset ?? 0));
|
|
176
|
+
url.searchParams.set('count', String(input.count ?? 50));
|
|
177
|
+
if (input.fhirVersion)
|
|
178
|
+
url.searchParams.set('fhirVersion', input.fhirVersion);
|
|
179
|
+
if (input.language?.trim())
|
|
180
|
+
url.searchParams.set('language', input.language.trim());
|
|
181
|
+
if (input.jurisdiction?.trim())
|
|
182
|
+
url.searchParams.set('jurisdiction', input.jurisdiction.trim());
|
|
183
|
+
if (input.version?.trim())
|
|
184
|
+
url.searchParams.set('version', input.version.trim());
|
|
185
|
+
const response = await __classPrivateFieldGet(this, _TerminologyServiceClient_fetch, "f").call(this, url, {
|
|
186
|
+
headers: {
|
|
187
|
+
accept: 'application/vnd.api+json',
|
|
188
|
+
...(__classPrivateFieldGet(this, _TerminologyServiceClient_serviceToken, "f") ? { authorization: `Bearer ${__classPrivateFieldGet(this, _TerminologyServiceClient_serviceToken, "f")}` } : {}),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
if (!response.ok)
|
|
192
|
+
throw new Error(`Terminology service returned HTTP ${response.status}`);
|
|
193
|
+
const document = await response.json();
|
|
194
|
+
if (!__classPrivateFieldGet(this, _TerminologyServiceClient_instances, "m", _TerminologyServiceClient_isDocument).call(this, document, input))
|
|
195
|
+
throw new Error('Terminology service returned an invalid document');
|
|
196
|
+
return document;
|
|
197
|
+
}, _TerminologyServiceClient_validateInput = function _TerminologyServiceClient_validateInput(input) {
|
|
198
|
+
if (!(input.sector?.trim() || __classPrivateFieldGet(this, _TerminologyServiceClient_sector, "f")))
|
|
199
|
+
throw new Error('sector is required');
|
|
200
|
+
if (!input.claim.trim())
|
|
201
|
+
throw new Error('claim is required');
|
|
202
|
+
resourceTypeFromClaim(input.claim);
|
|
203
|
+
if (input.offset !== undefined && (!Number.isInteger(input.offset) || input.offset < 0)) {
|
|
204
|
+
throw new Error('offset must be a non-negative integer');
|
|
205
|
+
}
|
|
206
|
+
if (input.count !== undefined && (!Number.isInteger(input.count) || input.count < 1 || input.count > 100)) {
|
|
207
|
+
throw new Error('count must be an integer from 1 to 100');
|
|
208
|
+
}
|
|
209
|
+
}, _TerminologyServiceClient_isDocument = function _TerminologyServiceClient_isDocument(value, input) {
|
|
210
|
+
if (!value || typeof value !== 'object')
|
|
211
|
+
return false;
|
|
212
|
+
const document = value;
|
|
213
|
+
if (document.jsonapi?.version !== '1.1' || !Array.isArray(document.data) || !document.meta)
|
|
214
|
+
return false;
|
|
215
|
+
if (document.meta.resourceType !== resourceTypeFromClaim(input.claim) || document.meta.claim !== input.claim.trim())
|
|
216
|
+
return false;
|
|
217
|
+
return document.data.every((resource) => (resource?.type === 'terminology-option'
|
|
218
|
+
&& typeof resource.id === 'string'
|
|
219
|
+
&& typeof resource.attributes?.system === 'string'
|
|
220
|
+
&& typeof resource.attributes?.code === 'string'
|
|
221
|
+
&& typeof resource.attributes?.display === 'string'
|
|
222
|
+
&& typeof resource.attributes?.localizedDisplay === 'string'
|
|
223
|
+
&& typeof resource.attributes?.resolvedLanguage === 'string'
|
|
224
|
+
&& typeof resource.attributes?.fallbackUsed === 'boolean'));
|
|
225
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdc-sdk-node-ts",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.21",
|
|
4
4
|
"description": "Next-generation Node runtime package for the GDC SDK family",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Antifraud Services Inc.",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test:e2e:live-gw:clean": "bash ./scripts/run-live-gw-clean.sh"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"gdc-common-utils-ts": "2.9.
|
|
41
|
+
"gdc-common-utils-ts": "2.9.20",
|
|
42
42
|
"gdc-sdk-core-ts": "2.9.10"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|