gdc-common-utils-ts 2.6.3 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/utils/authority-resolution.js +3 -1
- package/dist/utils/bundle-document-builder.d.ts +8 -0
- package/dist/utils/bundle-document-builder.js +11 -2
- package/dist/utils/did.js +14 -1
- package/dist/utils/license-list-search.js +14 -6
- package/dist/utils/organization-employee-lifecycle.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,6 +59,11 @@ It is intentionally not a full backend orchestration layer.
|
|
|
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
|
+
- Claims-first aggregates may intentionally mix related resource families. A
|
|
63
|
+
Digital Twin `ResearchSubject.meta.claims` contains both `ResearchSubject.*`
|
|
64
|
+
and `Composition.*`; strict FHIR R4/R5 translates the Composition into
|
|
65
|
+
`ResearchSubject.contained[]`. Producers never invent a
|
|
66
|
+
`ResearchSubject.composition` wire property.
|
|
62
67
|
- Native FHIR resources received from EHR systems do not need that extension:
|
|
63
68
|
normalize them with `normalizeClaimsFromFhirResource(...)` at the processing
|
|
64
69
|
boundary before indexed storage. Existing `resource.meta.claims` take
|
|
@@ -4,7 +4,9 @@ function normalizeString(value) {
|
|
|
4
4
|
return typeof value === 'string' ? value.trim() : '';
|
|
5
5
|
}
|
|
6
6
|
function encodeDidWebAuthority(authority) {
|
|
7
|
-
|
|
7
|
+
// DNS hostnames are case-insensitive, while the canonical did:web spelling
|
|
8
|
+
// keeps URI percent escapes uppercase for exact DID-document comparisons.
|
|
9
|
+
return authority.toLowerCase().replace(/:/g, '%3A');
|
|
8
10
|
}
|
|
9
11
|
function normalizeAuthorityHost(value) {
|
|
10
12
|
const raw = normalizeString(value).replace(/\/+$/, '');
|
|
@@ -51,6 +51,14 @@ export declare function getBundleDocumentPatientFullName(bundle: Record<string,
|
|
|
51
51
|
* equal by accident.
|
|
52
52
|
*/
|
|
53
53
|
export declare function normalizeFullNameForComparison(value: unknown): string;
|
|
54
|
+
/**
|
|
55
|
+
* Detects the native FHIR resource represented by one version-neutral claims
|
|
56
|
+
* map. A Composition-only map is a complete resource input: it is not merely
|
|
57
|
+
* metadata for another resource and must remain translatable for strict FHIR
|
|
58
|
+
* `contained[]` output. When another resource prefix is present,
|
|
59
|
+
* `Composition.*` remains section-membership metadata and cannot replace that
|
|
60
|
+
* resource's type.
|
|
61
|
+
*/
|
|
54
62
|
export declare function detectClaimsResourceType(claims: BundleDocumentClaims): string | undefined;
|
|
55
63
|
export declare function convertClaimsToFhirResource(claims: BundleDocumentClaims, version?: 'r4'): FhirResource;
|
|
56
64
|
/**
|
|
@@ -328,6 +328,14 @@ function mergeContainedResourceReferenceList(claims, references) {
|
|
|
328
328
|
[claimKey]: next.join(','),
|
|
329
329
|
};
|
|
330
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Detects the native FHIR resource represented by one version-neutral claims
|
|
333
|
+
* map. A Composition-only map is a complete resource input: it is not merely
|
|
334
|
+
* metadata for another resource and must remain translatable for strict FHIR
|
|
335
|
+
* `contained[]` output. When another resource prefix is present,
|
|
336
|
+
* `Composition.*` remains section-membership metadata and cannot replace that
|
|
337
|
+
* resource's type.
|
|
338
|
+
*/
|
|
331
339
|
export function detectClaimsResourceType(claims) {
|
|
332
340
|
const keys = Object.keys(claims || {});
|
|
333
341
|
if (keys.some((key) => key.startsWith('MedicationStatement.')))
|
|
@@ -338,9 +346,10 @@ export function detectClaimsResourceType(claims) {
|
|
|
338
346
|
return ResourceTypesFhirR4.Condition;
|
|
339
347
|
if (keys.some((key) => key.startsWith('DocumentReference.')))
|
|
340
348
|
return ResourceTypesFhirR4.DocumentReference;
|
|
341
|
-
const
|
|
349
|
+
const qualifiedKeys = keys
|
|
342
350
|
.map((key) => getSimpleClaimAttributeName(key))
|
|
343
|
-
.
|
|
351
|
+
.filter((key) => key.includes('.') && !key.startsWith('@'));
|
|
352
|
+
const firstContextualized = qualifiedKeys.find((key) => !key.startsWith(`${ResourceTypesFhirR4.Composition}.`)) || qualifiedKeys.find((key) => key.startsWith(`${ResourceTypesFhirR4.Composition}.`));
|
|
344
353
|
if (firstContextualized) {
|
|
345
354
|
return firstContextualized.split('.')[0];
|
|
346
355
|
}
|
package/dist/utils/did.js
CHANGED
|
@@ -224,12 +224,25 @@ export function getBaseUrlFromDidWeb(did) {
|
|
|
224
224
|
const domainPart = didParts[0];
|
|
225
225
|
const pathParts = didParts.slice(1);
|
|
226
226
|
const decodedDomain = decodeURIComponent(domainPart);
|
|
227
|
-
const protocol = decodedDomain
|
|
227
|
+
const protocol = isLoopbackDidWebAuthority(decodedDomain) ? 'http' : 'https';
|
|
228
228
|
const path = pathParts.join('/').replace(/cds-(es|us|gb)/, (match, p1) => `cds-${p1.toUpperCase()}`);
|
|
229
229
|
// Ensure a trailing slash for the base URL, without double slashes when no path is present.
|
|
230
230
|
const normalizedPath = path ? `${path}/` : '';
|
|
231
231
|
return `${protocol}://${decodedDomain}/${normalizedPath}`;
|
|
232
232
|
}
|
|
233
|
+
/** Keeps local did:web discovery usable without weakening non-loopback HTTPS. */
|
|
234
|
+
function isLoopbackDidWebAuthority(authority) {
|
|
235
|
+
try {
|
|
236
|
+
const hostname = new URL(`http://${authority}`).hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
237
|
+
return hostname === 'localhost'
|
|
238
|
+
|| hostname.endsWith('.localhost')
|
|
239
|
+
|| hostname === '::1'
|
|
240
|
+
|| /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
233
246
|
/**
|
|
234
247
|
* Builds the canonical hosted organization/tenant DID in the data space.
|
|
235
248
|
*
|
|
@@ -179,15 +179,23 @@ export function readLicenseListRecords(body) {
|
|
|
179
179
|
return data
|
|
180
180
|
.filter((entry) => !!entry && typeof entry === 'object')
|
|
181
181
|
.map((entry) => {
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
// Canonical search responses place one license row in `entry.resource`;
|
|
183
|
+
// deprecated aggregate responses placed rows in `resource.data[]`.
|
|
184
|
+
const primaryResource = entry.resource && typeof entry.resource === 'object' && !Array.isArray(entry.resource)
|
|
185
|
+
? entry.resource
|
|
186
|
+
: undefined;
|
|
187
|
+
const row = primaryResource && !Array.isArray(primaryResource.data) && isLicenseListEntry(primaryResource)
|
|
188
|
+
? primaryResource
|
|
189
|
+
: entry;
|
|
190
|
+
const claims = extractClaims(row);
|
|
191
|
+
const meta = row.meta && typeof row.meta === 'object' ? row.meta : {};
|
|
184
192
|
return {
|
|
185
193
|
id: normalizeText(claims[ClaimsIndividualProductSchemaorg.serialNumber]
|
|
186
194
|
|| claims[ClaimsOfferSchemaorg.serialNumber]
|
|
187
|
-
||
|
|
188
|
-
status: normalizeText(meta.status ||
|
|
189
|
-
subjectId: normalizeText(meta.subjectId ||
|
|
190
|
-
ownerOrganizationId: normalizeText(meta.ownerOrganizationId ||
|
|
195
|
+
|| row.id),
|
|
196
|
+
status: normalizeText(meta.status || row.status),
|
|
197
|
+
subjectId: normalizeText(meta.subjectId || row.subjectId),
|
|
198
|
+
ownerOrganizationId: normalizeText(meta.ownerOrganizationId || row.ownerOrganizationId || claims['License.ownerOrganizationId']),
|
|
191
199
|
email: normalizeText(claims[ClaimsPersonSchemaorg.email]),
|
|
192
200
|
telephone: normalizeText(claims[ClaimsPersonSchemaorg.telephone]),
|
|
193
201
|
role: normalizeText(claims[ClaimsPersonSchemaorg.hasOccupationalRoleValue]),
|
|
@@ -39,6 +39,7 @@ export function extractBundleSearchResources(value) {
|
|
|
39
39
|
const entries = candidate.data.map(record).filter(isRecord);
|
|
40
40
|
if (entries.length === 0)
|
|
41
41
|
return [];
|
|
42
|
+
let recognizedSearchEnvelope = false;
|
|
42
43
|
const resources = entries.flatMap((entry) => {
|
|
43
44
|
const resource = record(entry.resource);
|
|
44
45
|
if (!resource)
|
|
@@ -46,10 +47,14 @@ export function extractBundleSearchResources(value) {
|
|
|
46
47
|
const legacyRows = resource.total !== undefined && Array.isArray(resource.data)
|
|
47
48
|
? resource.data.map(record).filter(isRecord)
|
|
48
49
|
: undefined;
|
|
50
|
+
if (legacyRows)
|
|
51
|
+
recognizedSearchEnvelope = true;
|
|
49
52
|
return legacyRows ?? [resource];
|
|
50
53
|
});
|
|
51
54
|
if (resources.length > 0)
|
|
52
55
|
return resources;
|
|
56
|
+
if (recognizedSearchEnvelope)
|
|
57
|
+
return [];
|
|
53
58
|
// Compatibility for older SDK fixtures that passed already-extracted rows.
|
|
54
59
|
return entries;
|
|
55
60
|
}
|