babelfhir-ts 1.0.38 → 1.0.40
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 +11 -11
- package/out/src/generator/classGenerator.js +47 -12
- package/out/src/generator/clientGenerator.js +95 -31
- package/out/src/generator/index.js +4 -0
- package/out/src/generator/interfaceGenerator.js +43 -28
- package/out/src/generator/randomSupportGenerator.js +65 -5
- package/out/src/generator/sdParser.js +29 -16
- package/out/src/generator/validatorGenerator.js +8 -8
- package/out/src/main.js +40 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,11 +48,11 @@ Every pull request runs two independent CI pipelines that validate generated cod
|
|
|
48
48
|
|
|
49
49
|
The first pipeline validates generated resources using the [Firely .NET SDK validator](https://docs.fire.ly/projects/Firely-NET-SDK/) (v3.0.1). Results are published as live badges:
|
|
50
50
|
|
|
51
|
-

|
|
52
|
+

|
|
53
|
+

|
|
54
|
+

|
|
55
|
+

|
|
56
56
|
|
|
57
57
|
> 11 profiles are excluded from this pipeline due to schema loading issues in the Firely SDK. These profiles validate successfully with the HL7 Java Validator below. Details in [docs/FIRELY-VALIDATOR-BUGS.md](./docs/FIRELY-VALIDATOR-BUGS.md).
|
|
58
58
|
|
|
@@ -60,15 +60,15 @@ The first pipeline validates generated resources using the [Firely .NET SDK vali
|
|
|
60
60
|
|
|
61
61
|
The second pipeline validates using the [official HL7 FHIR Validator](https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator) (v6.3.11), the reference implementation for FHIR conformance checking:
|
|
62
62
|
|
|
63
|
-

|
|
64
|
+

|
|
65
|
+

|
|
66
|
+

|
|
67
|
+

|
|
68
68
|
|
|
69
69
|
> Terminology validation requires a tx server. The pipeline uses `--tx-server https://tx.fhir.org/r4` during generation to expand ValueSets and produce valid codes.
|
|
70
70
|
|
|
71
|
-
📊 **[Full Report
|
|
71
|
+
📊 **[Full Report](https://max-health-inc.github.io/BabelFHIR-TS/)**
|
|
72
72
|
<!-- HL7-PARITY-BADGES:END -->
|
|
73
73
|
<!-- PARITY-BADGES:END -->
|
|
74
74
|
|
|
@@ -238,7 +238,7 @@ function applyRequiredFieldHeuristic(rf, ctx) {
|
|
|
238
238
|
return { expr: `skeletonCodeableConcept('http://loinc.org', ['72166-2'])` };
|
|
239
239
|
}
|
|
240
240
|
if (baseResource === 'Medication') {
|
|
241
|
-
return { expr: makeCodeableConcept('http://www.nlm.nih.gov/research/umls/rxnorm', '1049502'
|
|
241
|
+
return { expr: makeCodeableConcept('http://www.nlm.nih.gov/research/umls/rxnorm', '1049502'), isObjectLiteral: true };
|
|
242
242
|
}
|
|
243
243
|
if (baseResource === 'Immunization') {
|
|
244
244
|
return { expr: makeCodeableConcept('http://hl7.org/fhir/sid/cvx', '207', 'COVID-19 mRNA vaccine', 'COVID-19 mRNA vaccine'), isObjectLiteral: true };
|
|
@@ -248,7 +248,7 @@ function applyRequiredFieldHeuristic(rf, ctx) {
|
|
|
248
248
|
return { expr: makeCodeableConcept('http://hl7.org/fhir/sid/cvx', '207', 'COVID-19 mRNA vaccine', 'COVID-19 mRNA vaccine'), isObjectLiteral: true };
|
|
249
249
|
}
|
|
250
250
|
if (rf.name === 'medicationCodeableConcept') {
|
|
251
|
-
return { expr: makeCodeableConcept('http://www.nlm.nih.gov/research/umls/rxnorm', '313782'
|
|
251
|
+
return { expr: makeCodeableConcept('http://www.nlm.nih.gov/research/umls/rxnorm', '313782'), isObjectLiteral: true };
|
|
252
252
|
}
|
|
253
253
|
if (rf.name === 'valueCodeableConcept') {
|
|
254
254
|
// Skip heuristic if there are nestedCodingSlices - let the dedicated handler take care of it
|
|
@@ -760,12 +760,15 @@ function generateSliceElement(slice, elementType) {
|
|
|
760
760
|
const profileUrl = profileUrls[0]; // Primary profile URL
|
|
761
761
|
log.debug(`Resource child - sliceName: ${sliceName}, profileUrls: ${JSON.stringify(profileUrls)}, profileUrl: ${profileUrl}`);
|
|
762
762
|
const metaProfile = profileUrl ? `, meta: { profile: ['${profileUrl}'] }` : '';
|
|
763
|
-
// Extract resource type
|
|
764
|
-
//
|
|
763
|
+
// Extract resource type: prefer baseTypeCode (authoritative from StructureDefinition type[].code),
|
|
764
|
+
// then try parsing profile URL, then fall back to slice name heuristics
|
|
765
765
|
let resourceType = 'Composition'; // default
|
|
766
|
-
if (
|
|
766
|
+
if (child.baseTypeCode) {
|
|
767
|
+
resourceType = capitalise(child.baseTypeCode);
|
|
768
|
+
}
|
|
769
|
+
else if (profileUrl) {
|
|
767
770
|
const profileName = profileUrl.split('/').pop() || '';
|
|
768
|
-
// Extract base type from profile name (e.g., Composition-uv-ips
|
|
771
|
+
// Extract base type from profile name (e.g., Composition-uv-ips -> Composition)
|
|
769
772
|
if (profileName.includes('-')) {
|
|
770
773
|
resourceType = capitalise(profileName.split('-')[0]);
|
|
771
774
|
}
|
|
@@ -798,6 +801,12 @@ function generateSliceElement(slice, elementType) {
|
|
|
798
801
|
else if (resourceType === 'Observation') {
|
|
799
802
|
resourceExpr = `{ resourceType: 'Observation', id: randomId()${metaProfile}, status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, subject: { reference: 'Patient/' + randomId() } }`;
|
|
800
803
|
}
|
|
804
|
+
else if (resourceType === 'Claim') {
|
|
805
|
+
resourceExpr = `{ resourceType: 'Claim', id: randomId()${metaProfile}, status: 'active', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/claim-type', code: 'professional' }] }, use: 'preauthorization', patient: { reference: 'Patient/' + randomId() }, created: new Date().toISOString().split('T')[0], provider: { reference: 'Practitioner/' + randomId() }, priority: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/processpriority', code: 'normal' }] }, insurance: [{ sequence: 1, focal: true, coverage: { reference: 'Coverage/' + randomId() } }] }`;
|
|
806
|
+
}
|
|
807
|
+
else if (resourceType === 'ClaimResponse') {
|
|
808
|
+
resourceExpr = `{ resourceType: 'ClaimResponse', id: randomId()${metaProfile}, status: 'active', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/claim-type', code: 'professional' }] }, use: 'preauthorization', patient: { reference: 'Patient/' + randomId() }, created: new Date().toISOString().split('T')[0], insurer: { reference: 'Organization/' + randomId() }, outcome: 'complete' }`;
|
|
809
|
+
}
|
|
801
810
|
else if (resourceType === 'Composition') {
|
|
802
811
|
// Composition requires special handling for different profiles
|
|
803
812
|
// ISiKBerichtSubSysteme requires: text.status='extensions', LOINC code 55112-7, type.text required
|
|
@@ -924,6 +933,14 @@ function generateSliceElement(slice, elementType) {
|
|
|
924
933
|
// Device slice
|
|
925
934
|
return `{ fullUrl: 'urn:uuid:' + crypto.randomUUID(), resource: { resourceType: 'Device', id: randomId()${metaProfileExpr}, type: { coding: [{ system: 'http://snomed.info/sct', code: '49062001', display: 'Device' }] } } }`;
|
|
926
935
|
}
|
|
936
|
+
if (sliceName.includes('claimresponse')) {
|
|
937
|
+
// ClaimResponse slice
|
|
938
|
+
return `{ fullUrl: 'urn:uuid:' + crypto.randomUUID(), resource: { resourceType: 'ClaimResponse', id: randomId()${metaProfileExpr}, status: 'active', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/claim-type', code: 'professional' }] }, use: 'preauthorization', patient: { reference: 'Patient/' + randomId() }, created: new Date().toISOString().split('T')[0], insurer: { reference: 'Organization/' + randomId() }, outcome: 'complete' } }`;
|
|
939
|
+
}
|
|
940
|
+
if (sliceName.includes('claim')) {
|
|
941
|
+
// Claim slice (must come after claimresponse check)
|
|
942
|
+
return `{ fullUrl: 'urn:uuid:' + crypto.randomUUID(), resource: { resourceType: 'Claim', id: randomId()${metaProfileExpr}, status: 'active', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/claim-type', code: 'professional' }] }, use: 'preauthorization', patient: { reference: 'Patient/' + randomId() }, created: new Date().toISOString().split('T')[0], provider: { reference: 'Practitioner/' + randomId() }, priority: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/processpriority', code: 'normal' }] }, insurance: [{ sequence: 1, focal: true, coverage: { reference: 'Coverage/' + randomId() } }] } }`;
|
|
943
|
+
}
|
|
927
944
|
// Handle non-Bundle element types - don't return fullUrl structure for simple types
|
|
928
945
|
if (elementType === 'CodeableConcept') {
|
|
929
946
|
// For CodeableConcept slices (like category:us-core), return a valid CodeableConcept
|
|
@@ -1327,6 +1344,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
1327
1344
|
// Try to get system from slice's patternConstraint or profileUrls
|
|
1328
1345
|
let system = 'http://terminology.hl7.org/CodeSystem/v3-NullFlavor';
|
|
1329
1346
|
let code = `'code-' + randomId().slice(0, 6)`;
|
|
1347
|
+
let codeResolved = false;
|
|
1330
1348
|
let version;
|
|
1331
1349
|
// Check if slice has a pattern constraint with system
|
|
1332
1350
|
if (slice.patternConstraint && typeof slice.patternConstraint === 'object') {
|
|
@@ -1336,6 +1354,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
1336
1354
|
}
|
|
1337
1355
|
if (pattern.code && typeof pattern.code === 'string') {
|
|
1338
1356
|
code = `'${pattern.code}'`;
|
|
1357
|
+
codeResolved = true;
|
|
1339
1358
|
}
|
|
1340
1359
|
if (pattern.version && typeof pattern.version === 'string') {
|
|
1341
1360
|
version = pattern.version;
|
|
@@ -1367,6 +1386,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
1367
1386
|
system = slice.binding.sampleCode.system;
|
|
1368
1387
|
}
|
|
1369
1388
|
code = JSON.stringify(slice.binding.sampleCode.code);
|
|
1389
|
+
codeResolved = true;
|
|
1370
1390
|
}
|
|
1371
1391
|
else if (slice.binding?.codes && slice.binding.codes.length > 0) {
|
|
1372
1392
|
// Fallback to first code from the binding's codes array
|
|
@@ -1375,6 +1395,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
1375
1395
|
system = firstCode.system;
|
|
1376
1396
|
}
|
|
1377
1397
|
code = JSON.stringify(firstCode.code);
|
|
1398
|
+
codeResolved = true;
|
|
1378
1399
|
}
|
|
1379
1400
|
// Check if parent field has a binding that matches this slice's system
|
|
1380
1401
|
// E.g., ISiKSchwangerschaftErwarteterEntbindungstermin binds code to SchwangerschaftEtMethodeVS
|
|
@@ -1383,6 +1404,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
1383
1404
|
// If parent has a binding and this slice uses LOINC, prefer parent's code
|
|
1384
1405
|
if (parentBindingCode.system === 'http://loinc.org' || !parentBindingCode.system) {
|
|
1385
1406
|
code = JSON.stringify(parentBindingCode.code);
|
|
1407
|
+
codeResolved = true;
|
|
1386
1408
|
}
|
|
1387
1409
|
}
|
|
1388
1410
|
// Check requiredChildren from type profile for required fields like 'version' and 'system'
|
|
@@ -1414,13 +1436,23 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
1414
1436
|
}
|
|
1415
1437
|
}
|
|
1416
1438
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1439
|
+
// If no static code was resolved, try runtime resolution via _codeResolver
|
|
1440
|
+
const bindingUri = slice.binding?.uri || rf.bindingUri;
|
|
1441
|
+
if (!codeResolved && bindingUri) {
|
|
1442
|
+
// Emit a spread expression that resolves at runtime from ValueSetRegistry
|
|
1443
|
+
const versionArg = version ? `, ${JSON.stringify(version)}` : '';
|
|
1444
|
+
codingElements.push(`{ ...resolveSliceCode(${className}._codeResolver, ${JSON.stringify(bindingUri)}, ${JSON.stringify(system)}${versionArg}) }`);
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
// Static code available or no binding URI - use traditional inline approach
|
|
1448
|
+
codingProps.push(`system: ${JSON.stringify(system)}`);
|
|
1449
|
+
if (version) {
|
|
1450
|
+
codingProps.push(`version: ${JSON.stringify(version)}`);
|
|
1451
|
+
}
|
|
1452
|
+
codingProps.push(`code: ${code}`);
|
|
1453
|
+
codingProps.push(`display: 'disp-' + randomId().slice(0, 6)`);
|
|
1454
|
+
codingElements.push(`{ ${codingProps.join(', ')} }`);
|
|
1420
1455
|
}
|
|
1421
|
-
codingProps.push(`code: ${code}`);
|
|
1422
|
-
codingProps.push(`display: 'disp-' + randomId().slice(0, 6)`);
|
|
1423
|
-
codingElements.push(`{ ${codingProps.join(', ')} }`);
|
|
1424
1456
|
}
|
|
1425
1457
|
const expr = `{ coding: [${codingElements.join(', ')}], text: 'text-' + randomId().slice(0, 6) }`;
|
|
1426
1458
|
const initExpr = buildValueExpression(rf, expr, { isObjectLiteral: true });
|
|
@@ -1509,6 +1541,9 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
1509
1541
|
if (requiredInitBlock.includes('randomNPI(')) {
|
|
1510
1542
|
helperImports.add('randomNPI');
|
|
1511
1543
|
}
|
|
1544
|
+
if (requiredInitBlock.includes('resolveSliceCode(')) {
|
|
1545
|
+
helperImports.add('resolveSliceCode');
|
|
1546
|
+
}
|
|
1512
1547
|
for (const helper of usedSkeletons) {
|
|
1513
1548
|
helperImports.add(helper);
|
|
1514
1549
|
}
|
|
@@ -32,6 +32,8 @@ export function generateClient(options) {
|
|
|
32
32
|
generateSmartClient(clientDir, resourceTypes);
|
|
33
33
|
generateIndex(clientDir, resourceTypes);
|
|
34
34
|
generateReadme(clientDir);
|
|
35
|
+
// Install base client type declarations so tsc can resolve @babelfhir-ts/client-r4
|
|
36
|
+
installBaseClientTypes(options.outputDir);
|
|
35
37
|
log.success(`Generated FHIR client with ${resourceTypes.length} resource types + SMART auth`);
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
@@ -525,64 +527,50 @@ export function parseBundle(bundle: Bundle<FhirResource>): BundleParser {
|
|
|
525
527
|
* Generate fhir-client.ts
|
|
526
528
|
*/
|
|
527
529
|
function generateFhirClient(clientDir, resourceTypes) {
|
|
528
|
-
const readerImports = resourceTypes
|
|
529
|
-
.map((rt) => ` type ${rt.profileName}Reader,`)
|
|
530
|
-
.join("\n");
|
|
531
|
-
const writerImports = resourceTypes
|
|
532
|
-
.map((rt) => ` type ${rt.profileName}Writer,`)
|
|
533
|
-
.join("\n");
|
|
534
530
|
const readerMethods = resourceTypes
|
|
535
531
|
.map((rt) => {
|
|
536
532
|
const methodName = rt.profileName.charAt(0).toLowerCase() + rt.profileName.slice(1);
|
|
537
|
-
return ` ${methodName}()
|
|
538
|
-
return
|
|
533
|
+
return ` ${methodName}() {
|
|
534
|
+
return this.forType<GeneratedTypes.${rt.profileName}>("${rt.baseResourceType}");
|
|
539
535
|
}`;
|
|
540
536
|
})
|
|
541
537
|
.join("\n\n");
|
|
542
538
|
const writerMethods = resourceTypes
|
|
543
539
|
.map((rt) => {
|
|
544
540
|
const methodName = rt.profileName.charAt(0).toLowerCase() + rt.profileName.slice(1);
|
|
545
|
-
return ` ${methodName}()
|
|
546
|
-
return
|
|
541
|
+
return ` ${methodName}() {
|
|
542
|
+
return this.forType<GeneratedTypes.${rt.profileName}>("${rt.baseResourceType}");
|
|
547
543
|
}`;
|
|
548
544
|
})
|
|
549
545
|
.join("\n\n");
|
|
550
546
|
const content = `import {
|
|
551
|
-
|
|
552
|
-
|
|
547
|
+
FhirReadClient as BaseFhirReadClient,
|
|
548
|
+
FhirWriteClient as BaseFhirWriteClient,
|
|
553
549
|
type FetchFn,
|
|
554
|
-
} from "
|
|
555
|
-
import
|
|
556
|
-
${writerImports}
|
|
557
|
-
FhirResourceWriterImpl,
|
|
558
|
-
} from "./resource-writer.js";
|
|
550
|
+
} from "@babelfhir-ts/client-r4";
|
|
551
|
+
import type * as GeneratedTypes from "../index.js";
|
|
559
552
|
|
|
560
553
|
/**
|
|
561
|
-
* FHIR Client
|
|
554
|
+
* Profile-specific FHIR Read Client.
|
|
555
|
+
* Extends the base R4 read client with typed profile accessors.
|
|
556
|
+
* Inherits all base R4 resource methods from @babelfhir-ts/client-r4.
|
|
562
557
|
*/
|
|
563
|
-
export class FhirReadClient {
|
|
564
|
-
constructor(
|
|
565
|
-
private readonly baseUrl: string,
|
|
566
|
-
private readonly fetchFn?: FetchFn,
|
|
567
|
-
) {}
|
|
568
|
-
|
|
558
|
+
export class FhirReadClient extends BaseFhirReadClient {
|
|
569
559
|
${readerMethods}
|
|
570
560
|
}
|
|
571
561
|
|
|
572
562
|
/**
|
|
573
|
-
* FHIR Client
|
|
563
|
+
* Profile-specific FHIR Write Client.
|
|
564
|
+
* Extends the base R4 write client with typed profile accessors.
|
|
565
|
+
* Inherits all base R4 resource methods from @babelfhir-ts/client-r4.
|
|
574
566
|
*/
|
|
575
|
-
export class FhirWriteClient {
|
|
576
|
-
constructor(
|
|
577
|
-
private readonly baseUrl: string,
|
|
578
|
-
private readonly fetchFn?: FetchFn,
|
|
579
|
-
) {}
|
|
580
|
-
|
|
567
|
+
export class FhirWriteClient extends BaseFhirWriteClient {
|
|
581
568
|
${writerMethods}
|
|
582
569
|
}
|
|
583
570
|
|
|
584
571
|
/**
|
|
585
572
|
* Main FHIR Client — works with plain fetch or an authenticated fetch wrapper.
|
|
573
|
+
* Provides both base R4 accessors (inherited) and profile-specific accessors.
|
|
586
574
|
*
|
|
587
575
|
* @example
|
|
588
576
|
* // Unauthenticated
|
|
@@ -590,6 +578,12 @@ ${writerMethods}
|
|
|
590
578
|
*
|
|
591
579
|
* // With custom fetch (e.g. from SmartFhirClient)
|
|
592
580
|
* const client = new FhirClient("https://fhir.example.com", authenticatedFetch);
|
|
581
|
+
*
|
|
582
|
+
* // Base R4 methods (inherited from @babelfhir-ts/client-r4)
|
|
583
|
+
* const pt = await client.read().patient().read("123");
|
|
584
|
+
*
|
|
585
|
+
* // Profile-specific methods (generated)
|
|
586
|
+
* const claim = await client.read().pASClaim().search({ status: "active" });
|
|
593
587
|
*/
|
|
594
588
|
export class FhirClient {
|
|
595
589
|
private readonly readClient: FhirReadClient;
|
|
@@ -1437,3 +1431,73 @@ const client = new FhirClient('https://fhir.example.com/fhir', loggingFetch);
|
|
|
1437
1431
|
`;
|
|
1438
1432
|
fs.writeFileSync(path.join(clientDir, "README.md"), content);
|
|
1439
1433
|
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Install @babelfhir-ts/client-r4 type declarations into the generated output's
|
|
1436
|
+
* node_modules so tsc can resolve the import during compilation.
|
|
1437
|
+
* At runtime, the consumer installs the actual package via npm.
|
|
1438
|
+
*/
|
|
1439
|
+
function installBaseClientTypes(outputDir) {
|
|
1440
|
+
const pkgDir = path.join(outputDir, "node_modules", "@babelfhir-ts", "client-r4");
|
|
1441
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
1442
|
+
// Minimal package.json
|
|
1443
|
+
fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify({
|
|
1444
|
+
name: "@babelfhir-ts/client-r4",
|
|
1445
|
+
version: "0.0.0-types",
|
|
1446
|
+
types: "index.d.ts"
|
|
1447
|
+
}, null, 2));
|
|
1448
|
+
// Type declarations matching the base package's public API
|
|
1449
|
+
const dts = `/// <reference types="@types/fhir" />
|
|
1450
|
+
|
|
1451
|
+
export type FetchFn = typeof globalThis.fetch;
|
|
1452
|
+
|
|
1453
|
+
export type WithId<T> = T & { id: string };
|
|
1454
|
+
|
|
1455
|
+
export type SearchParams = Record<string, boolean | number | string | string[] | undefined>;
|
|
1456
|
+
|
|
1457
|
+
export interface Bundle<T = fhir4.Resource> {
|
|
1458
|
+
resourceType: "Bundle";
|
|
1459
|
+
type: "collection" | "searchset" | "transaction-response" | "transaction" | "batch" | "batch-response" | "document" | "message" | "history";
|
|
1460
|
+
total?: number;
|
|
1461
|
+
link?: { relation: string; url: string }[];
|
|
1462
|
+
entry?: { resource?: T; fullUrl?: string; search?: { mode?: string; score?: number }; request?: { method: string; url: string }; response?: { status: string } }[];
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
export declare class FhirResourceReader<T extends fhir4.Resource> {
|
|
1466
|
+
readonly baseUrl: string;
|
|
1467
|
+
readonly resourceType: string;
|
|
1468
|
+
constructor(baseUrl: string, resourceType: string, fetchFn?: FetchFn);
|
|
1469
|
+
read(id: string): Promise<WithId<T>>;
|
|
1470
|
+
search(params?: SearchParams): Promise<Bundle<WithId<T>>>;
|
|
1471
|
+
searchOne(params?: SearchParams): Promise<WithId<T> | undefined>;
|
|
1472
|
+
searchAll(params?: SearchParams): Promise<WithId<T>[]>;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
export declare class FhirResourceWriter<T extends fhir4.Resource> {
|
|
1476
|
+
readonly baseUrl: string;
|
|
1477
|
+
readonly resourceType: string;
|
|
1478
|
+
constructor(baseUrl: string, resourceType: string, fetchFn?: FetchFn);
|
|
1479
|
+
create(resource: T): Promise<WithId<T>>;
|
|
1480
|
+
update(resource: WithId<T>): Promise<WithId<T>>;
|
|
1481
|
+
delete(id: string): Promise<void>;
|
|
1482
|
+
createOrUpdate(resource: WithId<T>): Promise<WithId<T>>;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
export declare class FhirReadClient {
|
|
1486
|
+
constructor(baseUrl: string, fetchFn?: FetchFn);
|
|
1487
|
+
protected forType<T extends fhir4.Resource>(resourceType: string): FhirResourceReader<T>;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
export declare class FhirWriteClient {
|
|
1491
|
+
constructor(baseUrl: string, fetchFn?: FetchFn);
|
|
1492
|
+
protected forType<T extends fhir4.Resource>(resourceType: string): FhirResourceWriter<T>;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
export declare class FhirClient {
|
|
1496
|
+
readonly baseUrl: string;
|
|
1497
|
+
constructor(baseUrl: string, fetchFn?: FetchFn);
|
|
1498
|
+
read(): FhirReadClient;
|
|
1499
|
+
write(): FhirWriteClient;
|
|
1500
|
+
}
|
|
1501
|
+
`;
|
|
1502
|
+
fs.writeFileSync(path.join(pkgDir, "index.d.ts"), dts);
|
|
1503
|
+
}
|
|
@@ -1295,6 +1295,10 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
1295
1295
|
'@types/fhir': '^0.0.41'
|
|
1296
1296
|
}
|
|
1297
1297
|
};
|
|
1298
|
+
// When client is generated, add @babelfhir-ts/client-r4 as dependency (generated client extends base)
|
|
1299
|
+
if (!flags?.noClient) {
|
|
1300
|
+
generatedPackageJson.dependencies['@babelfhir-ts/client-r4'] = '^0.2.0';
|
|
1301
|
+
}
|
|
1298
1302
|
const generatedPackageJsonPath = path.join(outputDir, 'package.json');
|
|
1299
1303
|
fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
|
|
1300
1304
|
logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
|
|
@@ -448,19 +448,10 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
448
448
|
if (system) {
|
|
449
449
|
codingLines.push(` system: "${system}";`);
|
|
450
450
|
}
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
else if (valueSetTypeName && valueSetSystem && system === valueSetSystem) {
|
|
457
|
-
// Valueset with uniform system matching the pattern - use valueset type
|
|
458
|
-
codingLines.push(` code: ${valueSetTypeName};`);
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
// No valueset or system mismatch - use literal code
|
|
462
|
-
codingLines.push(` code: "${code}";`);
|
|
463
|
-
}
|
|
451
|
+
// For FIXED pattern constraints, always use the literal code type.
|
|
452
|
+
// The pattern value may not be a member of the bound ValueSet
|
|
453
|
+
// (e.g., panel code "85354-9" vs individual vital signs VS).
|
|
454
|
+
codingLines.push(` code: "${code}";`);
|
|
464
455
|
});
|
|
465
456
|
// Only generate the coding interface if it has content
|
|
466
457
|
if (codingLines.length > 0) {
|
|
@@ -492,13 +483,9 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
492
483
|
if (patternSystem) {
|
|
493
484
|
codingLines.push(` system: "${patternSystem}";`);
|
|
494
485
|
}
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
codingLines.push(` code: "${pattern.code}";`);
|
|
501
|
-
}
|
|
486
|
+
// For FIXED pattern constraints, always use the literal code type.
|
|
487
|
+
// The pattern value may not be a member of the bound ValueSet.
|
|
488
|
+
codingLines.push(` code: "${pattern.code}";`);
|
|
502
489
|
importManager.addFhirType('Coding');
|
|
503
490
|
const codingInterface = `export interface ${codingInterfaceName} extends Coding {\n${codingLines.join('\n')}\n}`;
|
|
504
491
|
// Check if this interface already exists
|
|
@@ -1061,6 +1048,14 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1061
1048
|
return;
|
|
1062
1049
|
const existingLine = emittedLinesByField.get(fieldName);
|
|
1063
1050
|
if (existingLine) {
|
|
1051
|
+
// Keep existing line if it's more specific (has type intersection or binding constraint)
|
|
1052
|
+
// Only replace if the new line is more specific than the existing one
|
|
1053
|
+
const existingIsConstrained = existingLine.includes(' & ') || existingLine.includes('Array<{');
|
|
1054
|
+
const newIsConstrained = line.includes(' & ') || line.includes('Array<{');
|
|
1055
|
+
if (existingIsConstrained && !newIsConstrained) {
|
|
1056
|
+
// Existing is more specific - skip the less-specific replacement
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1064
1059
|
const idx = interfaceLines.indexOf(existingLine);
|
|
1065
1060
|
if (idx >= 0)
|
|
1066
1061
|
interfaceLines.splice(idx, 1);
|
|
@@ -1658,31 +1653,51 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1658
1653
|
addTypeImport(sanitizedParent);
|
|
1659
1654
|
}
|
|
1660
1655
|
if (interfaceLines.length > 0) {
|
|
1656
|
+
// Deduplicate fields by name: keep the first (more specific) version of each field.
|
|
1657
|
+
// This handles cases where the same field appears twice — once with a binding
|
|
1658
|
+
// constraint and once plain (e.g., type?: CodeableConcept & {...} vs type?: CodeableConcept).
|
|
1659
|
+
const seenFieldNames = new Set();
|
|
1660
|
+
const deduped = [];
|
|
1661
|
+
for (const line of interfaceLines) {
|
|
1662
|
+
// Extract field name from line (e.g., "type?: CodeableConcept;" -> "type")
|
|
1663
|
+
// Also handle JSDoc comments (/** ... */) which don't start with a field name
|
|
1664
|
+
const fieldMatch = line.match(/^(\w+)[?:]/);
|
|
1665
|
+
if (!fieldMatch) {
|
|
1666
|
+
deduped.push(line); // JSDoc or other non-field lines
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
const name = fieldMatch[1];
|
|
1670
|
+
if (seenFieldNames.has(name)) {
|
|
1671
|
+
continue; // Skip duplicate — keep the first (more specific) version
|
|
1672
|
+
}
|
|
1673
|
+
seenFieldNames.add(name);
|
|
1674
|
+
deduped.push(line);
|
|
1675
|
+
}
|
|
1661
1676
|
localInterfaceNames.add(parentInterfaceName);
|
|
1662
1677
|
if (parentInterfaceName === interfaceName) {
|
|
1663
1678
|
const sanitizedBaseResource = baseResource ? sanitizeIdentifier(baseResource) : undefined;
|
|
1664
1679
|
const isBaseAbstract = sanitizedBaseResource === 'Base';
|
|
1665
1680
|
const extendsClause = (sanitizedBaseResource && !isBaseAbstract && !isPrimitiveType(sanitizedBaseResource)) ? ` extends ${sanitizedBaseResource}` : '';
|
|
1666
|
-
debug('emit root interface', parentInterfaceName, 'extends', sanitizedBaseResource, 'fields',
|
|
1667
|
-
interfaces.push(`export interface ${parentInterfaceName}${extendsClause} {\n${
|
|
1681
|
+
debug('emit root interface', parentInterfaceName, 'extends', sanitizedBaseResource, 'fields', deduped.length);
|
|
1682
|
+
interfaces.push(`export interface ${parentInterfaceName}${extendsClause} {\n${deduped.map(l => ` ${l}`).join('\n')}\n}`);
|
|
1668
1683
|
}
|
|
1669
1684
|
else {
|
|
1670
|
-
debug('emit nested interface', parentInterfaceName, 'extends', parentType, 'fields',
|
|
1685
|
+
debug('emit nested interface', parentInterfaceName, 'extends', parentType, 'fields', deduped.length);
|
|
1671
1686
|
logger.debug('[DEBUG emit ALL nested interfaces]', {
|
|
1672
1687
|
parentInterfaceName,
|
|
1673
1688
|
parentType,
|
|
1674
1689
|
parentFieldType,
|
|
1675
1690
|
willExtend: parentType && !isPrimitiveType(parentType),
|
|
1676
|
-
linesCount:
|
|
1691
|
+
linesCount: deduped.length
|
|
1677
1692
|
});
|
|
1678
1693
|
if (parentInterfaceName === 'USCoreQuestionnaireResponseProfileItem') {
|
|
1679
1694
|
logger.debug('[DEBUG emit Item interface]', {
|
|
1680
|
-
linesCount:
|
|
1681
|
-
lines:
|
|
1682
|
-
answerLines:
|
|
1695
|
+
linesCount: deduped.length,
|
|
1696
|
+
lines: deduped,
|
|
1697
|
+
answerLines: deduped.filter(l => l.includes('answer'))
|
|
1683
1698
|
});
|
|
1684
1699
|
}
|
|
1685
|
-
interfaces.push(`export interface ${parentInterfaceName}${parentType && !isPrimitiveType(parentType) ? ` extends ${parentType}` : ''} {\n${
|
|
1700
|
+
interfaces.push(`export interface ${parentInterfaceName}${parentType && !isPrimitiveType(parentType) ? ` extends ${parentType}` : ''} {\n${deduped.map(l => ` ${l}`).join('\n')}\n}`);
|
|
1686
1701
|
}
|
|
1687
1702
|
}
|
|
1688
1703
|
}
|
|
@@ -75,7 +75,38 @@ export function skeletonContactPoint(): ContactPoint { return { system: 'phone',
|
|
|
75
75
|
export function skeletonQuantity(): Quantity { return { value: randomInt(1,100), unit: randomString('u'), system: 'http://unitsofmeasure.org' }; }
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
78
|
+
* Resolve a coding for a nestedCodingSlice from a ValueSet binding at runtime.
|
|
79
|
+
* When a slice has a binding URI but no statically-known codes, this function
|
|
80
|
+
* attempts to resolve a valid code from the ValueSetRegistry via the codeResolver.
|
|
81
|
+
* Falls back to fake placeholder codes if resolution fails.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveSliceCode(
|
|
84
|
+
codeResolver: ((url: string) => { code: string; system: string; display?: string } | undefined) | undefined,
|
|
85
|
+
valueSetUrl: string | undefined,
|
|
86
|
+
fallbackSystem: string,
|
|
87
|
+
fallbackVersion?: string
|
|
88
|
+
): { system: string; code: string; display: string; version?: string } {
|
|
89
|
+
if (valueSetUrl && codeResolver) {
|
|
90
|
+
const resolved = codeResolver(valueSetUrl);
|
|
91
|
+
if (resolved) {
|
|
92
|
+
const result: { system: string; code: string; display: string; version?: string } = {
|
|
93
|
+
system: resolved.system || fallbackSystem,
|
|
94
|
+
code: resolved.code,
|
|
95
|
+
display: resolved.display || resolved.code,
|
|
96
|
+
};
|
|
97
|
+
if (fallbackVersion) result.version = fallbackVersion;
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const result: { system: string; code: string; display: string; version?: string } = {
|
|
102
|
+
system: fallbackSystem,
|
|
103
|
+
code: 'code-' + randomId().slice(0, 6),
|
|
104
|
+
display: 'disp-' + randomId().slice(0, 6),
|
|
105
|
+
};
|
|
106
|
+
if (fallbackVersion) result.version = fallbackVersion;
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
79
110
|
/** Resolved code from a ValueSet binding */
|
|
80
111
|
export type ResolvedCode = { code: string; system: string; display?: string };
|
|
81
112
|
|
|
@@ -520,12 +551,16 @@ export function enrichResource(
|
|
|
520
551
|
}
|
|
521
552
|
// Use proper UUIDs for fullUrl values
|
|
522
553
|
if (!('entry' in base)) base.entry = [{ fullUrl: 'urn:uuid:' + randomUUID(), resource: { resourceType: 'Composition', id: randomId(), status: 'final', type: { coding: [{ system: 'http://loinc.org', code: '60591-5' }] }, subject: { reference: 'Patient/' + randomId() }, date: new Date().toISOString(), author: [{ reference: 'Practitioner/' + randomId() }], title: 'Example Document', section: [{ title: 'Section', text: { status: 'generated', div: '<div xmlns="http://www.w3.org/1999/xhtml">Section content</div>' } }] } }];
|
|
523
|
-
// Fix existing entry fullUrls to use proper UUIDs
|
|
554
|
+
// Fix existing entry fullUrls to use proper UUIDs and empty resources
|
|
524
555
|
if (Array.isArray(base.entry)) {
|
|
525
|
-
for (const entry of base.entry as Array<{ fullUrl?: string }>) {
|
|
556
|
+
for (const entry of base.entry as Array<{ fullUrl?: string; resource?: Record<string, unknown> }>) {
|
|
526
557
|
if (entry.fullUrl && entry.fullUrl.startsWith('urn:uuid:id-')) {
|
|
527
558
|
entry.fullUrl = 'urn:uuid:' + randomUUID();
|
|
528
559
|
}
|
|
560
|
+
// Fix empty resource objects — a resource must have at least a resourceType
|
|
561
|
+
if (entry.resource && typeof entry.resource === 'object' && !('resourceType' in entry.resource)) {
|
|
562
|
+
entry.resource = { resourceType: 'Basic', id: randomId(), code: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/basic-resource-type', code: 'study' }] } };
|
|
563
|
+
}
|
|
529
564
|
}
|
|
530
565
|
}
|
|
531
566
|
}
|
|
@@ -785,8 +820,8 @@ export function enrichResource(
|
|
|
785
820
|
}
|
|
786
821
|
|
|
787
822
|
// Generic narrative for DomainResource derivatives
|
|
788
|
-
//
|
|
789
|
-
if (
|
|
823
|
+
// Bundle, Binary, and Parameters are pure Resource (not DomainResource) and do NOT support text/narrative
|
|
824
|
+
if (!/^(Bundle|Binary|Parameters)$/.test(baseResource)) {
|
|
790
825
|
if (!('text' in base)) {
|
|
791
826
|
// Check if there's a pattern constraint for text.status
|
|
792
827
|
const textStatusPattern = getPrimitivePattern('text.status');
|
|
@@ -924,6 +959,31 @@ export function enrichResource(
|
|
|
924
959
|
}
|
|
925
960
|
}
|
|
926
961
|
}
|
|
962
|
+
|
|
963
|
+
// Reorder fields so FHIR base Resource/DomainResource fields appear before domain-specific fields.
|
|
964
|
+
// FHIR element ordering: resourceType, id, meta, implicitRules, language, text, contained,
|
|
965
|
+
// extension, modifierExtension, then domain-specific fields.
|
|
966
|
+
// Firely (and HL7) validators enforce this ordering.
|
|
967
|
+
const fhirBaseFieldOrder = [
|
|
968
|
+
'resourceType', 'id', 'meta', 'implicitRules', 'language',
|
|
969
|
+
'text', 'contained', 'extension', 'modifierExtension'
|
|
970
|
+
];
|
|
971
|
+
const reordered: Record<string, unknown> = {};
|
|
972
|
+
for (const key of fhirBaseFieldOrder) {
|
|
973
|
+
if (key in base) {
|
|
974
|
+
reordered[key] = base[key];
|
|
975
|
+
delete base[key];
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
// Copy remaining domain-specific fields in their original order
|
|
979
|
+
for (const key of Object.keys(base)) {
|
|
980
|
+
reordered[key] = base[key];
|
|
981
|
+
}
|
|
982
|
+
// Overwrite base in-place
|
|
983
|
+
for (const key of Object.keys(base)) {
|
|
984
|
+
delete base[key];
|
|
985
|
+
}
|
|
986
|
+
Object.assign(base, reordered);
|
|
927
987
|
} catch { /* enrichment is best-effort */ }
|
|
928
988
|
}
|
|
929
989
|
`;
|
|
@@ -372,11 +372,16 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
372
372
|
}
|
|
373
373
|
// Build a map from snapshot for enriching differential elements with binding information
|
|
374
374
|
const snapshotBindings = new Map();
|
|
375
|
+
// Build a map from snapshot for authoritative min cardinality (resolves inherited min correctly)
|
|
376
|
+
const snapshotMin = new Map();
|
|
375
377
|
if (structureDefinition.differential && structureDefinition.snapshot) {
|
|
376
378
|
for (const snapElem of structureDefinition.snapshot.element) {
|
|
377
379
|
if (snapElem.binding) {
|
|
378
380
|
snapshotBindings.set(snapElem.path, snapElem.binding);
|
|
379
381
|
}
|
|
382
|
+
if (typeof snapElem.min === 'number') {
|
|
383
|
+
snapshotMin.set(snapElem.path, snapElem.min);
|
|
384
|
+
}
|
|
380
385
|
}
|
|
381
386
|
}
|
|
382
387
|
const newFields = elements.map((element) => {
|
|
@@ -438,22 +443,29 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
438
443
|
? element.min
|
|
439
444
|
: (typeof element.min === 'string' ? parseInt(element.min, 10) : undefined);
|
|
440
445
|
let isOptional = element.min === 0;
|
|
441
|
-
// If min is not specified in the differential,
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
if (structureDefinition.differential &&
|
|
445
|
-
//
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
446
|
+
// If min is not specified in the differential, use the snapshot's authoritative min first,
|
|
447
|
+
// then fall back to base fields. The snapshot represents the fully merged cardinality
|
|
448
|
+
// and is the single source of truth for the current profile's constraints.
|
|
449
|
+
if (structureDefinition.differential && parsedMin === undefined) {
|
|
450
|
+
// Prefer snapshot min (authoritative merged value for this profile)
|
|
451
|
+
const snapMin = snapshotMin.get(element.path);
|
|
452
|
+
if (snapMin !== undefined) {
|
|
453
|
+
parsedMin = snapMin;
|
|
454
|
+
isOptional = snapMin === 0;
|
|
455
|
+
}
|
|
456
|
+
else if (baseFields.length > 0) {
|
|
457
|
+
// Fall back to base field inheritance when no snapshot is available
|
|
458
|
+
const sliceName = element.sliceName;
|
|
459
|
+
let baseField = sliceName
|
|
460
|
+
? baseFields.find(f => f.name === element.path && f.sliceName === sliceName)
|
|
461
|
+
: baseFields.find(f => f.name === element.path);
|
|
462
|
+
if (!baseField) {
|
|
463
|
+
baseField = baseFields.find(f => f.name === element.path && !f.sliceName);
|
|
464
|
+
}
|
|
465
|
+
if (baseField && typeof baseField.min === 'number') {
|
|
466
|
+
parsedMin = baseField.min;
|
|
467
|
+
isOptional = baseField.isOptional;
|
|
468
|
+
}
|
|
457
469
|
}
|
|
458
470
|
}
|
|
459
471
|
// Default to 0 if still undefined (base resource default)
|
|
@@ -795,6 +807,7 @@ async function aggregateRequiredSlices(fields, baseFields = []) {
|
|
|
795
807
|
patternConstraint: childField.patternConstraint,
|
|
796
808
|
binding: childField.binding,
|
|
797
809
|
type: resolvedType, // Include type for placeholder generation
|
|
810
|
+
baseTypeCode: childField.baseTypeCode, // Raw FHIR type code for resource identification
|
|
798
811
|
isRequired,
|
|
799
812
|
isArray: resolvedIsArray, // Include array info for placeholder wrapping
|
|
800
813
|
profileUrls: childField.profileUrls, // Profile URLs for discriminator matching
|
|
@@ -244,7 +244,7 @@ valueSets // Optional ValueSet map for runtime validation
|
|
|
244
244
|
// Pattern constraint for ${humanRel}: at least one element must include coding with system="${system}" and code="${code}"
|
|
245
245
|
if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
|
|
246
246
|
const ${varName}Valid = resource.${rel}.some(item =>
|
|
247
|
-
item?.coding?.some(coding => coding?.system === "${system}" && coding?.code === "${code}")
|
|
247
|
+
item?.coding?.some(coding => (coding?.system as string) === "${system}" && (coding?.code as string) === "${code}")
|
|
248
248
|
);
|
|
249
249
|
if (!${varName}Valid) {
|
|
250
250
|
errors.push("${humanRel} must include at least one element with a coding with system '${system}' and code '${code}'");
|
|
@@ -256,7 +256,7 @@ valueSets // Optional ValueSet map for runtime validation
|
|
|
256
256
|
// Pattern constraint for ${humanRel}: at least one element must include coding with code="${code}"
|
|
257
257
|
if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
|
|
258
258
|
const ${varName}Valid = resource.${rel}.some(item =>
|
|
259
|
-
item?.coding?.some(coding => coding?.code === "${code}")
|
|
259
|
+
item?.coding?.some(coding => (coding?.code as string) === "${code}")
|
|
260
260
|
);
|
|
261
261
|
if (!${varName}Valid) {
|
|
262
262
|
errors.push("${humanRel} must include at least one element with a coding with code '${code}'");
|
|
@@ -272,7 +272,7 @@ valueSets // Optional ValueSet map for runtime validation
|
|
|
272
272
|
// Pattern constraint for ${humanRel}: must include coding with system="${system}" and code="${code}"
|
|
273
273
|
if (resource.${rel}) {
|
|
274
274
|
const ${varName}Valid = resource.${rel}.coding?.some(coding =>
|
|
275
|
-
coding?.system === "${system}" && coding?.code === "${code}"
|
|
275
|
+
(coding?.system as string) === "${system}" && (coding?.code as string) === "${code}"
|
|
276
276
|
);
|
|
277
277
|
if (!${varName}Valid) {
|
|
278
278
|
errors.push("${humanRel} must include a coding with system '${system}' and code '${code}'");
|
|
@@ -284,7 +284,7 @@ valueSets // Optional ValueSet map for runtime validation
|
|
|
284
284
|
// Pattern constraint for ${humanRel}: must include coding with code="${code}"
|
|
285
285
|
if (resource.${rel}) {
|
|
286
286
|
const ${varName}Valid = resource.${rel}.coding?.some(coding =>
|
|
287
|
-
coding?.code === "${code}"
|
|
287
|
+
(coding?.code as string) === "${code}")
|
|
288
288
|
);
|
|
289
289
|
if (!${varName}Valid) {
|
|
290
290
|
errors.push("${humanRel} must include a coding with code '${code}'");
|
|
@@ -306,7 +306,7 @@ valueSets // Optional ValueSet map for runtime validation
|
|
|
306
306
|
// Pattern constraint for ${humanRel}: at least one element must have system="${system}" and code="${code}"
|
|
307
307
|
if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
|
|
308
308
|
const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = resource.${rel}.some(item =>
|
|
309
|
-
item?.system === "${system}" && item?.code === "${code}"
|
|
309
|
+
(item?.system as string) === "${system}" && (item?.code as string) === "${code}"
|
|
310
310
|
);
|
|
311
311
|
if (!${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid) {
|
|
312
312
|
errors.push("${humanRel} must include at least one element with system '${system}' and code '${code}'");
|
|
@@ -318,7 +318,7 @@ valueSets // Optional ValueSet map for runtime validation
|
|
|
318
318
|
// Pattern constraint for ${humanRel}: at least one element must have code="${code}"
|
|
319
319
|
if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
|
|
320
320
|
const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = resource.${rel}.some(item =>
|
|
321
|
-
item?.code === "${code}"
|
|
321
|
+
(item?.code as string) === "${code}")
|
|
322
322
|
);
|
|
323
323
|
if (!${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid) {
|
|
324
324
|
errors.push("${humanRel} must include at least one element with code '${code}'");
|
|
@@ -332,7 +332,7 @@ valueSets // Optional ValueSet map for runtime validation
|
|
|
332
332
|
patternValidations.push(`
|
|
333
333
|
// Pattern constraint for ${humanRel}: must have system="${system}" and code="${code}"
|
|
334
334
|
if (resource.${rel}) {
|
|
335
|
-
if (resource.${rel}.system !== "${system}" || resource.${rel}.code !== "${code}") {
|
|
335
|
+
if ((resource.${rel}.system as string) !== "${system}" || (resource.${rel}.code as string) !== "${code}") {
|
|
336
336
|
errors.push("${humanRel} must have system '${system}' and code '${code}'");
|
|
337
337
|
}
|
|
338
338
|
}`);
|
|
@@ -341,7 +341,7 @@ valueSets // Optional ValueSet map for runtime validation
|
|
|
341
341
|
patternValidations.push(`
|
|
342
342
|
// Pattern constraint for ${humanRel}: must have code="${code}"
|
|
343
343
|
if (resource.${rel}) {
|
|
344
|
-
if (resource.${rel}.code !== "${code}") {
|
|
344
|
+
if ((resource.${rel}.code as string) !== "${code}") {
|
|
345
345
|
errors.push("${humanRel} must have code '${code}'");
|
|
346
346
|
}
|
|
347
347
|
}`);
|
package/out/src/main.js
CHANGED
|
@@ -113,8 +113,47 @@ function npmInstall(packagePath) {
|
|
|
113
113
|
return new Promise((resolve, reject) => {
|
|
114
114
|
const pm = detectPackageManager();
|
|
115
115
|
const pmName = path.basename(pm.cmd).replace(/\.(cmd|exe)$/, '');
|
|
116
|
+
// Check if the package already exists in package.json to avoid dependency loops.
|
|
117
|
+
// When `bun add` targets a tgz that's already a dependency, bun detects a
|
|
118
|
+
// circular resolution. In that case, update the reference and run `install` instead.
|
|
119
|
+
const projectPkgPath = path.join(process.cwd(), 'package.json');
|
|
120
|
+
let useInstallOnly = false;
|
|
121
|
+
if (fs.existsSync(projectPkgPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const tgzRelPath = './' + path.relative(process.cwd(), packagePath).replace(/\\/g, '/');
|
|
124
|
+
const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8'));
|
|
125
|
+
const deps = projectPkg.dependencies || {};
|
|
126
|
+
// Find if any dependency already points to the same tgz (or same package name)
|
|
127
|
+
for (const [name, value] of Object.entries(deps)) {
|
|
128
|
+
if (typeof value === 'string' && (value === tgzRelPath || value === packagePath)) {
|
|
129
|
+
console.log(`Package ${name} already in dependencies, running ${pmName} install`);
|
|
130
|
+
useInstallOnly = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// If not found, check by reading the tgz package name
|
|
135
|
+
if (!useInstallOnly) {
|
|
136
|
+
const tarPkgJsonPath = path.join(path.dirname(packagePath), '..', '.temp-install', 'generated', 'package.json');
|
|
137
|
+
// The tgz name follows the pattern: <sanitized-package-name>.tgz
|
|
138
|
+
const tgzBaseName = path.basename(packagePath, '.tgz').replace(/^-/, '');
|
|
139
|
+
for (const name of Object.keys(deps)) {
|
|
140
|
+
if (name.replace(/[@/]/g, '-') === tgzBaseName) {
|
|
141
|
+
// Update the dependency to point to the new tgz
|
|
142
|
+
deps[name] = tgzRelPath;
|
|
143
|
+
projectPkg.dependencies = deps;
|
|
144
|
+
fs.writeFileSync(projectPkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
|
|
145
|
+
console.log(`Updated ${name} dependency to ${tgzRelPath}`);
|
|
146
|
+
useInstallOnly = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch { /* ignore, fall through to normal add */ }
|
|
153
|
+
}
|
|
154
|
+
const args = useInstallOnly ? ['install'] : [...pm.args, packagePath];
|
|
116
155
|
console.log(`Installing package with ${pmName}...`);
|
|
117
|
-
const child = spawn(pm.cmd,
|
|
156
|
+
const child = spawn(pm.cmd, args, {
|
|
118
157
|
stdio: 'inherit',
|
|
119
158
|
shell: true
|
|
120
159
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.40",
|
|
4
4
|
"description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4 StructureDefinitions (profiles) directly inside package archives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "out/src/main.js",
|