babelfhir-ts 1.1.0 → 1.1.3
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 +2 -0
- package/out/src/generator/{constants.js → core/constants.js} +2 -1
- package/out/src/generator/{utils.js → core/utils.js} +3 -3
- package/out/src/generator/{classGenerator.js → emitters/class/classGenerator.js} +30 -9
- package/out/src/generator/{classHeuristics.js → emitters/class/classHeuristics.js} +16 -2
- package/out/src/generator/{enrichResourceHandlers.js → emitters/class/enrichResourceHandlers.js} +42 -11
- package/out/src/generator/{enrichResourceTemplate.js → emitters/class/enrichResourceTemplate.js} +4 -2
- package/out/src/generator/{fieldPatternExtractor.js → emitters/class/fieldPatternExtractor.js} +1 -1
- package/out/src/generator/{randomUtilitiesTemplate.js → emitters/class/randomUtilitiesTemplate.js} +5 -3
- package/out/src/generator/{sliceElementGenerator.js → emitters/class/sliceElementGenerator.js} +21 -4
- package/out/src/generator/{clientGenerator.js → emitters/client/clientGenerator.js} +13 -10
- package/out/src/generator/{clientReadmeGenerator.js → emitters/client/clientReadmeGenerator.js} +6 -3
- package/out/src/generator/{smartAuthGenerator.js → emitters/client/smartAuthGenerator.js} +1 -1
- package/out/src/generator/{importManager.js → emitters/interface/importManager.js} +17 -17
- package/out/src/generator/{interfaceFieldProcessor.js → emitters/interface/interfaceFieldProcessor.js} +14 -14
- package/out/src/generator/{interfaceGenerator.js → emitters/interface/interfaceGenerator.js} +9 -11
- package/out/src/generator/{postProcessExtensions.js → emitters/interface/postProcessExtensions.js} +4 -4
- package/out/src/generator/{processFlatField.js → emitters/interface/processFlatField.js} +11 -7
- package/out/src/generator/{processNestedField.js → emitters/interface/processNestedField.js} +10 -7
- package/out/src/generator/{sliceValidatorGenerator.js → emitters/validator/sliceValidatorGenerator.js} +0 -2
- package/out/src/generator/{validatorGenerator.js → emitters/validator/validatorGenerator.js} +5 -4
- package/out/src/generator/emitters/zod/zodSchemaGenerator.js +552 -0
- package/out/src/generator/emitters/zod/zodStubInstaller.js +77 -0
- package/out/src/generator/emitters/zod/zodTypes.js +27 -0
- package/out/src/generator/fhir/r4/context.js +21 -0
- package/out/src/generator/fhir/r4/context.ts +24 -0
- package/out/src/generator/fhir/r4/dataTypes.json +858 -0
- package/out/src/generator/fhir/r4/interfaces.json +664 -0
- package/out/src/generator/fhir/r4/resources.json +150 -0
- package/out/src/generator/{fhirR4Rules.js → fhir/r4/rules.js} +3 -3
- package/out/src/generator/fhir/r4/rules.ts +226 -0
- package/out/src/generator/fhir/r4b/context.js +21 -0
- package/out/src/generator/fhir/r4b/context.ts +24 -0
- package/out/src/generator/fhir/r4b/dataTypes.json +860 -0
- package/out/src/generator/fhir/r4b/interfaces.json +684 -0
- package/out/src/generator/fhir/r4b/resources.json +145 -0
- package/out/src/generator/fhir/r4b/rules.js +171 -0
- package/out/src/generator/fhir/r4b/rules.ts +190 -0
- package/out/src/generator/fhir/types.js +27 -0
- package/out/src/generator/fhir/types.ts +80 -0
- package/out/src/generator/fhir/versionContext.js +52 -0
- package/out/src/generator/fhir/versionContext.ts +66 -0
- package/out/src/generator/index.js +72 -33
- package/out/src/generator/{packageManager.js → parser/packageManager.js} +2 -2
- package/out/src/generator/{packageParser.js → parser/packageParser.js} +10 -6
- package/out/src/generator/{sdFetcher.js → parser/sdFetcher.js} +25 -19
- package/out/src/generator/{sdParser.js → parser/sdParser.js} +28 -28
- package/out/src/generator/{sliceAggregator.js → parser/sliceAggregator.js} +7 -4
- package/out/src/generator/{txClient.js → parser/txClient.js} +9 -4
- package/out/src/generator/{vsParser.js → parser/vsParser.js} +4 -4
- package/out/src/generator/sdProcessor.js +36 -15
- package/out/src/main.js +33 -8
- package/package.json +5 -3
- package/out/src/generator/fhirDataTypes.json +0 -858
- package/out/src/generator/fhirInterfaces.json +0 -665
- package/out/src/generator/fhirResources.json +0 -157
- /package/out/src/generator/{cacheConfig.js → core/cacheConfig.js} +0 -0
- /package/out/src/generator/{fetchUtils.js → core/fetchUtils.js} +0 -0
- /package/out/src/generator/{sdTypes.js → core/sdTypes.js} +0 -0
- /package/out/src/generator/{randomSupportGenerator.js → emitters/class/randomSupportGenerator.js} +0 -0
- /package/out/src/generator/{valueSetGenerator.js → emitters/valueset/valueSetGenerator.js} +0 -0
- /package/out/src/generator/{base-r4 → fhir/r4/base}/Bundle.json +0 -0
- /package/out/src/generator/{base-r4 → fhir/r4/base}/Composition.json +0 -0
- /package/out/src/generator/{base-r4 → fhir/r4/base}/Endpoint.json +0 -0
- /package/out/src/generator/{base-r4 → fhir/r4/base}/README.md +0 -0
- /package/out/src/generator/{base-r4 → fhir/r4/base}/Subscription.json +0 -0
- /package/out/src/generator/{base-r4 → fhir/r4/base}/ValueSet.json +0 -0
- /package/out/src/generator/{base-r4 → fhir/r4/base}/backport-subscription.json +0 -0
- /package/out/src/generator/{base-r4 → fhir/r4/base}/well-known-system-codes.json +0 -0
package/README.md
CHANGED
|
@@ -135,6 +135,8 @@ Options:
|
|
|
135
135
|
--no-cache Delete .cache folder after generation
|
|
136
136
|
--no-classes Only generate interfaces and types (skip class generation)
|
|
137
137
|
--no-client Skip FHIR client generation (client generated by default)
|
|
138
|
+
--schema <format> Generate schema files alongside outputs (supported: zod)
|
|
139
|
+
--fhir-version <ver> FHIR version to target: r4 (default) or r4b
|
|
138
140
|
--package <pkg@version> Download FHIR package from registry and process it
|
|
139
141
|
--registry <url> FHIR package registry URL (default: https://packages.simplifier.net)
|
|
140
142
|
--tx-server <url> Terminology server URL for ValueSet expansion (e.g., https://tx.fhir.org/r4)
|
|
@@ -10,10 +10,11 @@ import { fileURLToPath } from 'url';
|
|
|
10
10
|
import path from 'path';
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
// Resolve package.json — works from
|
|
13
|
+
// Resolve package.json — works from src/generator/core (dev) and out/src/generator/core (compiled)
|
|
14
14
|
const _pkgPath = [
|
|
15
15
|
path.resolve(__dirname, '../../package.json'),
|
|
16
16
|
path.resolve(__dirname, '../../../package.json'),
|
|
17
|
+
path.resolve(__dirname, '../../../../package.json'),
|
|
17
18
|
].find(p => {
|
|
18
19
|
try {
|
|
19
20
|
return JSON.parse(require('fs').readFileSync(p, 'utf8')).name === 'babelfhir-ts';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { logger } from "
|
|
4
|
-
import
|
|
3
|
+
import { logger } from "../../logger.js";
|
|
4
|
+
import { ctx } from '../fhir/versionContext.js';
|
|
5
5
|
/**
|
|
6
6
|
* Strips the version suffix from a FHIR canonical URL.
|
|
7
7
|
* FHIR canonical URLs use '|' to separate the URL from the version.
|
|
@@ -82,7 +82,7 @@ export function getResourceTypes(options) {
|
|
|
82
82
|
if (extendsMatch) {
|
|
83
83
|
const baseType = extendsMatch[1];
|
|
84
84
|
// Check if it's a known FHIR resource type
|
|
85
|
-
if (
|
|
85
|
+
if (ctx().resourceNames.includes(baseType)) {
|
|
86
86
|
resourceTypes.set(profileName, baseType);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { TS, } from '
|
|
1
|
+
import { ctx } from '../../fhir/versionContext.js';
|
|
2
|
+
import { TS, } from '../../core/constants.js';
|
|
3
3
|
import { primitivePlaceholder, generateCodeDefault, makeCodeableConcept, chooseChoiceVariant, applyRequiredFieldHeuristic, deriveIdentifierConfig, } from './classHeuristics.js';
|
|
4
4
|
import { generateSliceElement, buildNestedRequirementsObject } from './sliceElementGenerator.js';
|
|
5
5
|
/**
|
|
@@ -83,6 +83,7 @@ function generatePatternMetadata(fieldPatterns, forbiddenFields, valueSetBinding
|
|
|
83
83
|
}
|
|
84
84
|
export function generateClass(className, interfaceName, baseResource, requiredFields = [], profileUrl, fieldPatterns = [], forbiddenFields = [], valueSetBindings = {}, fieldOrder = []) {
|
|
85
85
|
// Decide whether to emit resourceType. If baseResource points at a known resource or ends with 'Resource'.
|
|
86
|
+
const CORE_RESOURCE_BASES = ctx().coreResourceBases;
|
|
86
87
|
const addResourceType = !!baseResource && CORE_RESOURCE_BASES.has(baseResource);
|
|
87
88
|
const resourceTypeLine = addResourceType && baseResource && baseResource !== 'Resource' ? `resourceType: '${baseResource}',` : (addResourceType ? `resourceType: 'Resource',` : '');
|
|
88
89
|
// Only true FHIR Resources (not datatypes like Identifier, Attachment, Extension) can carry meta.profile.
|
|
@@ -117,7 +118,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
117
118
|
};
|
|
118
119
|
// Helper to check if a field's type (or baseType) matches a given type name
|
|
119
120
|
// Handles profiled types like 'CodeableConcept-uv-ips' mapping back to 'CodeableConcept'
|
|
120
|
-
const
|
|
121
|
+
const _isType = (rf, typeName) => rf.type === typeName || rf.baseType === typeName;
|
|
121
122
|
for (const rf0 of requiredFields) {
|
|
122
123
|
if (rf0.name === 'id' || rf0.name === 'resourceType')
|
|
123
124
|
continue;
|
|
@@ -367,10 +368,25 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
367
368
|
requiredInits.push(`${key(rf.name)}: ${initExpr}`);
|
|
368
369
|
continue;
|
|
369
370
|
}
|
|
370
|
-
|
|
371
|
+
// Handle all FHIR primitive types: string-like (uri, url, canonical, etc.), number-like (decimal, integer, etc.)
|
|
372
|
+
if (['string', 'uri', 'url', 'canonical', 'oid', 'uuid', 'id', 'markdown', 'base64Binary', 'xhtml'].includes(rf.type)) {
|
|
373
|
+
const initExpr = buildValueExpression(rf, primitivePlaceholder('string'));
|
|
374
|
+
requiredInits.push(`${key(rf.name)}: ${initExpr}`);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (['number', 'decimal', 'integer', 'positiveInt', 'unsignedInt'].includes(rf.type)) {
|
|
378
|
+
const initExpr = buildValueExpression(rf, primitivePlaceholder('number'));
|
|
379
|
+
requiredInits.push(`${key(rf.name)}: ${initExpr}`);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (rf.type === 'boolean') {
|
|
383
|
+
const initExpr = buildValueExpression(rf, primitivePlaceholder('boolean'));
|
|
384
|
+
requiredInits.push(`${key(rf.name)}: ${initExpr}`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (rf.type === 'code') {
|
|
371
388
|
// For 'code' type without binding, generate based on field name to pick a sensible default
|
|
372
|
-
const
|
|
373
|
-
const initExpr = buildValueExpression(rf, expr);
|
|
389
|
+
const initExpr = buildValueExpression(rf, generateCodeDefault(rf.name));
|
|
374
390
|
requiredInits.push(`${key(rf.name)}: ${initExpr}`);
|
|
375
391
|
continue;
|
|
376
392
|
}
|
|
@@ -546,7 +562,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
546
562
|
// Skip FHIR primitive types - they cannot have nested properties (only _fieldName extensions)
|
|
547
563
|
if (rf.nestedRequirements && rf.nestedRequirements.length > 0 &&
|
|
548
564
|
rf.type !== 'Reference' && rf.type !== 'Extension' &&
|
|
549
|
-
!
|
|
565
|
+
!ctx().primitiveTypes.has(rf.type)) {
|
|
550
566
|
const objectLiteral = buildNestedRequirementsObject(rf.nestedRequirements);
|
|
551
567
|
if (objectLiteral) {
|
|
552
568
|
const initExpr = buildValueExpression(rf, objectLiteral, { isObjectLiteral: true });
|
|
@@ -562,8 +578,13 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
562
578
|
requiredInits.push(`${key(rf.name)}: ${initExpr}`);
|
|
563
579
|
continue;
|
|
564
580
|
}
|
|
565
|
-
// For other complex types not in skeleton map,
|
|
566
|
-
// This
|
|
581
|
+
// For other complex types not in skeleton map, generate a minimal empty object
|
|
582
|
+
// This handles logical model types (e.g., DocumentSection) and other custom complex types
|
|
583
|
+
if (rf.type !== 'Extension' && !ctx().primitiveTypes.has(rf.type)) {
|
|
584
|
+
const initExpr = buildValueExpression(rf, '{}', { isObjectLiteral: true });
|
|
585
|
+
requiredInits.push(`${key(rf.name)}: ${initExpr}`);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
567
588
|
}
|
|
568
589
|
// Special constraint handling: Organization org-1 requires name OR identifier
|
|
569
590
|
// If neither is already in requiredInits, add name as the simplest solution
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TS, FALLBACK_CODES, CODE_FIELD_DEFAULTS, DEFAULT_CONTENT_TYPE, DEFAULT_BASE64, resolveValueSetDefault, pickPreferredCode, } from '
|
|
1
|
+
import { TS, FALLBACK_CODES, CODE_FIELD_DEFAULTS, DEFAULT_CONTENT_TYPE, DEFAULT_BASE64, resolveValueSetDefault, pickPreferredCode, } from '../../core/constants.js';
|
|
2
2
|
/**
|
|
3
3
|
* Generate a simple placeholder value expression for a primitive type.
|
|
4
4
|
* We keep this VERY small to avoid pulling in heavy util logic.
|
|
@@ -40,7 +40,21 @@ export function chooseChoiceVariant(rf) {
|
|
|
40
40
|
// Extract the type from the slice name (e.g., "valueQuantity" -> "Quantity")
|
|
41
41
|
const typeSuffix = slice.name.slice(base.length);
|
|
42
42
|
if (typeSuffix) {
|
|
43
|
-
|
|
43
|
+
// Normalize primitive type suffixes to lowercase to match typeOptions convention
|
|
44
|
+
// (e.g., "Boolean" -> "boolean", "Integer" -> "integer", "DateTime" -> "dateTime")
|
|
45
|
+
const normalizedType = typeSuffix === 'Boolean' ? 'boolean'
|
|
46
|
+
: typeSuffix === 'Integer' ? 'integer'
|
|
47
|
+
: typeSuffix === 'String' ? 'string'
|
|
48
|
+
: typeSuffix === 'DateTime' ? 'dateTime'
|
|
49
|
+
: typeSuffix === 'Instant' ? 'instant'
|
|
50
|
+
: typeSuffix === 'Decimal' ? 'decimal'
|
|
51
|
+
: typeSuffix === 'Date' ? 'date'
|
|
52
|
+
: typeSuffix === 'Time' ? 'time'
|
|
53
|
+
: typeSuffix === 'Code' ? 'code'
|
|
54
|
+
: typeSuffix === 'PositiveInt' ? 'positiveInt'
|
|
55
|
+
: typeSuffix === 'UnsignedInt' ? 'unsignedInt'
|
|
56
|
+
: typeSuffix;
|
|
57
|
+
return { propertyName: slice.name, chosenType: normalizedType };
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
}
|
package/out/src/generator/{enrichResourceHandlers.js → emitters/class/enrichResourceHandlers.js}
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { DEFAULT_COUNTRY, TS } from '
|
|
1
|
+
import { DEFAULT_COUNTRY, TS } from '../../core/constants.js';
|
|
2
|
+
import { versionString } from '../../fhir/versionContext.js';
|
|
2
3
|
export function generateEnrichResourceEnd() {
|
|
3
4
|
return ` // Device (exact match — not DeviceRequest)
|
|
4
5
|
if (/^Device$/i.test(baseResource)) {
|
|
@@ -29,7 +30,9 @@ export function generateEnrichResourceEnd() {
|
|
|
29
30
|
// ValueSet - url must be a valid absolute URI (HTTP URL or proper URN)
|
|
30
31
|
if (/ValueSet/i.test(baseResource)) {
|
|
31
32
|
if (!('status' in base)) base.status = 'active';
|
|
32
|
-
if (!('url' in base)
|
|
33
|
+
if (!('url' in base) || (typeof base.url === 'string' && !base.url.startsWith('http') && !base.url.startsWith('urn:'))) {
|
|
34
|
+
base.url = 'https://acme.org/fhir/ValueSet/' + randomId();
|
|
35
|
+
}
|
|
33
36
|
// name must be machine-readable identifier (no spaces, starts with letter)
|
|
34
37
|
if (!('name' in base)) base.name = 'ExampleValueSet' + randomId().replace(/-/g, '');
|
|
35
38
|
else if (typeof base.name === 'string' && /^[0-9a-f-]+$/i.test(base.name)) {
|
|
@@ -40,9 +43,9 @@ export function generateEnrichResourceEnd() {
|
|
|
40
43
|
if (!('compose' in base)) base.compose = { include: [{ system: '${TS.LOINC}', concept: [{ code: '4548-4', display: 'Hemoglobin A1c/Hemoglobin.total in Blood' }] }] };
|
|
41
44
|
// Handle useContext when pattern for 'code' is present (ISiK ValueSet)
|
|
42
45
|
// The pattern key 'code' refers to useContext.code
|
|
43
|
-
if (fieldPatterns && 'code' in fieldPatterns
|
|
46
|
+
if (fieldPatterns && 'code' in fieldPatterns) {
|
|
44
47
|
const codePattern = fieldPatterns['code'] as Record<string, unknown>;
|
|
45
|
-
|
|
48
|
+
const validUseContext = {
|
|
46
49
|
code: {
|
|
47
50
|
system: codePattern.system || '${TS.USAGE_CONTEXT_TYPE}',
|
|
48
51
|
code: codePattern.code || 'focus'
|
|
@@ -50,7 +53,18 @@ export function generateEnrichResourceEnd() {
|
|
|
50
53
|
valueCodeableConcept: {
|
|
51
54
|
coding: [{ system: 'http://hl7.org/fhir/resource-types', code: 'ValueSet' }]
|
|
52
55
|
}
|
|
53
|
-
}
|
|
56
|
+
};
|
|
57
|
+
if (!('useContext' in base)) {
|
|
58
|
+
base.useContext = [validUseContext];
|
|
59
|
+
} else if (Array.isArray(base.useContext)) {
|
|
60
|
+
// Repair empty useContext entries generated by skeleton fallback
|
|
61
|
+
for (let i = 0; i < (base.useContext as Array<Record<string, unknown>>).length; i++) {
|
|
62
|
+
const entry = (base.useContext as Array<Record<string, unknown>>)[i];
|
|
63
|
+
if (!entry || Object.keys(entry).length === 0) {
|
|
64
|
+
(base.useContext as Array<Record<string, unknown>>)[i] = validUseContext;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
70
|
|
|
@@ -58,7 +72,9 @@ export function generateEnrichResourceEnd() {
|
|
|
58
72
|
if (/CodeSystem/i.test(baseResource)) {
|
|
59
73
|
if (!('status' in base)) base.status = 'active';
|
|
60
74
|
if (!('content' in base)) base.content = 'complete';
|
|
61
|
-
if (!('url' in base)
|
|
75
|
+
if (!('url' in base) || (typeof base.url === 'string' && !base.url.startsWith('http') && !base.url.startsWith('urn:'))) {
|
|
76
|
+
base.url = 'https://acme.org/fhir/CodeSystem/' + randomId();
|
|
77
|
+
}
|
|
62
78
|
if (!('name' in base)) base.name = 'ExampleCodeSystem';
|
|
63
79
|
if (!('version' in base)) base.version = '1.0.0';
|
|
64
80
|
if (!('concept' in base)) base.concept = [{ code: 'example', display: 'Example Code' }];
|
|
@@ -121,9 +137,14 @@ export function generateEnrichResourceEnd() {
|
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
// Binary
|
|
124
|
-
if (
|
|
140
|
+
if (/^Binary$/i.test(baseResource)) {
|
|
125
141
|
if (!('contentType' in base)) base.contentType = 'application/pdf';
|
|
126
|
-
if (!('data' in base))
|
|
142
|
+
if (!('data' in base)) {
|
|
143
|
+
base.data = 'SGVsbG8gV29ybGQ=';
|
|
144
|
+
} else if (typeof base.data === 'string' && !/^[A-Za-z0-9+/=\\s]+$/.test(base.data)) {
|
|
145
|
+
// data must be valid base64 — replace if skeleton generated a non-base64 value
|
|
146
|
+
base.data = 'SGVsbG8gV29ybGQ=';
|
|
147
|
+
}
|
|
127
148
|
}
|
|
128
149
|
|
|
129
150
|
// Coverage
|
|
@@ -202,7 +223,7 @@ export function generateEnrichResourceEnd() {
|
|
|
202
223
|
.filter(ext => ext && typeof ext === 'object' && 'url' in ext && typeof ext.url === 'string');
|
|
203
224
|
// If no valid extensions remain, add a placeholder FHIR version extension
|
|
204
225
|
if (validExtensions.length === 0) {
|
|
205
|
-
validExtensions.push({ url: 'http://hl7.org/fhir/StructureDefinition/endpoint-fhir-version', valueCode: '
|
|
226
|
+
validExtensions.push({ url: 'http://hl7.org/fhir/StructureDefinition/endpoint-fhir-version', valueCode: '${versionString()}' });
|
|
206
227
|
}
|
|
207
228
|
base.extension = validExtensions;
|
|
208
229
|
}
|
|
@@ -393,17 +414,27 @@ export function generateEnrichResourceEnd() {
|
|
|
393
414
|
|
|
394
415
|
// Generic narrative for DomainResource derivatives
|
|
395
416
|
// Bundle, Binary, and Parameters are pure Resource (not DomainResource) and do NOT support text/narrative
|
|
396
|
-
|
|
417
|
+
// Data types (CodeableConcept, Identifier, etc.) do NOT have resourceType and must be excluded
|
|
418
|
+
if ('resourceType' in base && !/^(Bundle|Binary|Parameters)$/.test(baseResource)) {
|
|
397
419
|
if (!('text' in base)) {
|
|
398
420
|
// Check if there's a pattern constraint for text.status
|
|
399
421
|
const textStatusPattern = getPrimitivePattern('text.status');
|
|
400
422
|
const status = textStatusPattern !== undefined ? textStatusPattern : 'generated';
|
|
401
423
|
base.text = { status: status, div: '<div xmlns="http://www.w3.org/1999/xhtml">Generated</div>' };
|
|
402
424
|
} else if (typeof base.text === 'object' && base.text !== null) {
|
|
425
|
+
const textObj = base.text as Record<string, unknown>;
|
|
403
426
|
// Apply text.status pattern to existing text object
|
|
404
427
|
const textStatusPattern = getPrimitivePattern('text.status');
|
|
405
428
|
if (textStatusPattern !== undefined) {
|
|
406
|
-
|
|
429
|
+
textObj.status = textStatusPattern;
|
|
430
|
+
}
|
|
431
|
+
// Backfill div if missing — Narrative.div is required (1..1)
|
|
432
|
+
if (!('div' in textObj)) {
|
|
433
|
+
textObj.div = '<div xmlns="http://www.w3.org/1999/xhtml">Generated</div>';
|
|
434
|
+
}
|
|
435
|
+
// Backfill status if missing — Narrative.status is required (1..1)
|
|
436
|
+
if (!('status' in textObj)) {
|
|
437
|
+
textObj.status = 'generated';
|
|
407
438
|
}
|
|
408
439
|
}
|
|
409
440
|
}
|
package/out/src/generator/{enrichResourceTemplate.js → emitters/class/enrichResourceTemplate.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_COUNTRY, TS } from '
|
|
1
|
+
import { DEFAULT_COUNTRY, TS } from '../../core/constants.js';
|
|
2
2
|
export function generateEnrichResourceStart() {
|
|
3
3
|
return `
|
|
4
4
|
/** Resolved code from a ValueSet binding */
|
|
@@ -500,7 +500,9 @@ export function enrichResource(
|
|
|
500
500
|
// Force correct status - 'active' is NOT valid for QuestionnaireResponse.status
|
|
501
501
|
base.status = 'completed';
|
|
502
502
|
// questionnaire must be an absolute canonical URL, not a relative reference
|
|
503
|
-
if (!('questionnaire' in base)
|
|
503
|
+
if (!('questionnaire' in base) || (typeof base.questionnaire === 'string' && !base.questionnaire.startsWith('http') && !base.questionnaire.startsWith('urn:'))) {
|
|
504
|
+
base.questionnaire = 'urn:uuid:' + randomUUID();
|
|
505
|
+
}
|
|
504
506
|
if (!('subject' in base)) base.subject = { reference: 'Patient/' + randomId() };
|
|
505
507
|
if (!('authored' in base)) base.authored = new Date().toISOString();
|
|
506
508
|
if (!('author' in base)) base.author = { reference: 'Practitioner/' + randomId() };
|
package/out/src/generator/{fieldPatternExtractor.js → emitters/class/fieldPatternExtractor.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchStructureDefinition } from '
|
|
1
|
+
import { fetchStructureDefinition } from '../../parser/sdParser.js';
|
|
2
2
|
export async function extractFieldPatternsAndMetadata(patternSources, baseResource, sd, requiredFields, candidateRequiredFields, nestedRequirementsMap, resolvedProfilePatterns) {
|
|
3
3
|
const patternMap = new Map();
|
|
4
4
|
for (const src of patternSources) {
|
package/out/src/generator/{randomUtilitiesTemplate.js → emitters/class/randomUtilitiesTemplate.js}
RENAMED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { DEFAULT_COUNTRY, TS } from '
|
|
1
|
+
import { DEFAULT_COUNTRY, TS } from '../../core/constants.js';
|
|
2
|
+
import { fhirModuleSpecifier } from '../../fhir/versionContext.js';
|
|
2
3
|
export function generateRandomUtilities() {
|
|
4
|
+
const fhirModule = fhirModuleSpecifier();
|
|
3
5
|
return `// Auto-generated helper for random resource generation with optional deterministic seeding
|
|
4
|
-
// Uses official FHIR
|
|
5
|
-
import type { HumanName, Address, Coding, CodeableConcept, Identifier, Reference, Period, ContactPoint, Quantity } from '
|
|
6
|
+
// Uses official FHIR TypeScript definitions
|
|
7
|
+
import type { HumanName, Address, Coding, CodeableConcept, Identifier, Reference, Period, ContactPoint, Quantity } from '${fhirModule}';
|
|
6
8
|
let __seed = Date.now() & 0xffffffff;
|
|
7
9
|
export function setRandomSeed(seed: number) { __seed = seed >>> 0; }
|
|
8
10
|
function rng(): number { // simple LCG (Numerical Recipes variant)
|
package/out/src/generator/{sliceElementGenerator.js → emitters/class/sliceElementGenerator.js}
RENAMED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import { logger } from '
|
|
1
|
+
import { logger } from '../../../logger.js';
|
|
2
2
|
import { capitalise, deriveIdentifierConfig } from './classHeuristics.js';
|
|
3
|
-
import { TS, DEFAULT_BASE64, DEFAULT_BASE64_HELLO, DEFAULT_CONTENT_TYPE, EXAMPLE_ID_SYSTEM, DEFAULT_COUNTRY, RESOURCE_SKELETONS, compositionSkeleton, bundleEntry, matchSkeletonBySliceName, } from '
|
|
3
|
+
import { TS, DEFAULT_BASE64, DEFAULT_BASE64_HELLO, DEFAULT_CONTENT_TYPE, EXAMPLE_ID_SYSTEM, DEFAULT_COUNTRY, RESOURCE_SKELETONS, compositionSkeleton, bundleEntry, matchSkeletonBySliceName, } from '../../core/constants.js';
|
|
4
4
|
const log = logger.withTag('classGenerator');
|
|
5
|
+
/**
|
|
6
|
+
* Fields that are arrays in FHIR base complex types (0..*) and must always
|
|
7
|
+
* be serialized as JSON arrays, even when a profile constrains max to 1.
|
|
8
|
+
* FHIR JSON encoding is governed by the base StructureDefinition cardinality.
|
|
9
|
+
*/
|
|
10
|
+
const FHIR_BASE_ARRAY_FIELDS = new Set([
|
|
11
|
+
'coding', // CodeableConcept.coding is 0..*
|
|
12
|
+
'extension', // Element.extension is 0..*
|
|
13
|
+
'line', // Address.line is 0..*
|
|
14
|
+
'given', // HumanName.given is 0..*
|
|
15
|
+
'prefix', // HumanName.prefix is 0..*
|
|
16
|
+
'suffix', // HumanName.suffix is 0..*
|
|
17
|
+
]);
|
|
5
18
|
/**
|
|
6
19
|
* Build an object literal expression from nested requirements.
|
|
7
20
|
* This handles BackboneElement and other complex types with deeply nested required fields.
|
|
@@ -116,8 +129,12 @@ export function buildNestedRequirementsObject(nestedRequirements) {
|
|
|
116
129
|
// Type-based defaults
|
|
117
130
|
value = getDefaultValueForType(req.type, lastPart);
|
|
118
131
|
}
|
|
119
|
-
// If the final segment is an array segment, wrap the value in an array
|
|
120
|
-
if
|
|
132
|
+
// If the final segment is an array segment, wrap the value in an array.
|
|
133
|
+
// Also wrap if the field is inherently an array in FHIR base types
|
|
134
|
+
// (profiles may constrain max=1 but JSON encoding stays as array).
|
|
135
|
+
const shouldWrap = (allArraySegments.has(lastPart) || FHIR_BASE_ARRAY_FIELDS.has(lastPart))
|
|
136
|
+
&& !Array.isArray(value);
|
|
137
|
+
if (shouldWrap) {
|
|
121
138
|
current[lastPart] = [value];
|
|
122
139
|
}
|
|
123
140
|
else {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { getResourceTypes, getBaseResourceType } from "
|
|
4
|
-
import { logger } from "
|
|
3
|
+
import { getResourceTypes, getBaseResourceType } from "../../core/utils.js";
|
|
4
|
+
import { logger } from "../../../logger.js";
|
|
5
5
|
import { generateSmartAuth, generateSmartClient } from './smartAuthGenerator.js';
|
|
6
6
|
import { generateReadme, installBaseClientTypes } from './clientReadmeGenerator.js';
|
|
7
|
+
import { versionSlug } from '../../fhir/versionContext.js';
|
|
7
8
|
const log = logger.withTag('client');
|
|
8
9
|
/**
|
|
9
10
|
* Generate FHIR client code for all resource types
|
|
@@ -34,7 +35,7 @@ export function generateClient(options) {
|
|
|
34
35
|
generateSmartClient(clientDir, resourceTypes);
|
|
35
36
|
generateIndex(clientDir, resourceTypes);
|
|
36
37
|
generateReadme(clientDir);
|
|
37
|
-
// Install base client type declarations so tsc can resolve @babelfhir-ts/client
|
|
38
|
+
// Install base client type declarations so tsc can resolve @babelfhir-ts/client-<version>
|
|
38
39
|
installBaseClientTypes(options.outputDir);
|
|
39
40
|
log.success(`Generated FHIR client with ${resourceTypes.length} resource types + SMART auth`);
|
|
40
41
|
}
|
|
@@ -545,17 +546,19 @@ function generateFhirClient(clientDir, resourceTypes) {
|
|
|
545
546
|
}`;
|
|
546
547
|
})
|
|
547
548
|
.join("\n\n");
|
|
549
|
+
const slug = versionSlug();
|
|
550
|
+
const clientPkg = `@babelfhir-ts/client-${slug}`;
|
|
548
551
|
const content = `import {
|
|
549
552
|
FhirReadClient as BaseFhirReadClient,
|
|
550
553
|
FhirWriteClient as BaseFhirWriteClient,
|
|
551
554
|
type FetchFn,
|
|
552
|
-
} from "
|
|
555
|
+
} from "${clientPkg}";
|
|
553
556
|
import type * as GeneratedTypes from "../index.js";
|
|
554
557
|
|
|
555
558
|
/**
|
|
556
559
|
* Profile-specific FHIR Read Client.
|
|
557
|
-
* Extends the base
|
|
558
|
-
* Inherits all base
|
|
560
|
+
* Extends the base ${slug.toUpperCase()} read client with typed profile accessors.
|
|
561
|
+
* Inherits all base ${slug.toUpperCase()} resource methods from ${clientPkg}.
|
|
559
562
|
*/
|
|
560
563
|
export class FhirReadClient extends BaseFhirReadClient {
|
|
561
564
|
${readerMethods}
|
|
@@ -563,8 +566,8 @@ ${readerMethods}
|
|
|
563
566
|
|
|
564
567
|
/**
|
|
565
568
|
* Profile-specific FHIR Write Client.
|
|
566
|
-
* Extends the base
|
|
567
|
-
* Inherits all base
|
|
569
|
+
* Extends the base ${slug.toUpperCase()} write client with typed profile accessors.
|
|
570
|
+
* Inherits all base ${slug.toUpperCase()} resource methods from ${clientPkg}.
|
|
568
571
|
*/
|
|
569
572
|
export class FhirWriteClient extends BaseFhirWriteClient {
|
|
570
573
|
${writerMethods}
|
|
@@ -572,7 +575,7 @@ ${writerMethods}
|
|
|
572
575
|
|
|
573
576
|
/**
|
|
574
577
|
* Main FHIR Client — works with plain fetch or an authenticated fetch wrapper.
|
|
575
|
-
* Provides both base
|
|
578
|
+
* Provides both base ${slug.toUpperCase()} accessors (inherited) and profile-specific accessors.
|
|
576
579
|
*
|
|
577
580
|
* @example
|
|
578
581
|
* // Unauthenticated
|
|
@@ -581,7 +584,7 @@ ${writerMethods}
|
|
|
581
584
|
* // With custom fetch (e.g. from SmartFhirClient)
|
|
582
585
|
* const client = new FhirClient("https://fhir.example.com", authenticatedFetch);
|
|
583
586
|
*
|
|
584
|
-
* // Base
|
|
587
|
+
* // Base ${slug.toUpperCase()} methods (inherited from ${clientPkg})
|
|
585
588
|
* const pt = await client.read().patient().read("123");
|
|
586
589
|
*
|
|
587
590
|
* // Profile-specific methods (generated)
|
package/out/src/generator/{clientReadmeGenerator.js → emitters/client/clientReadmeGenerator.js}
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { versionSlug } from '../../fhir/versionContext.js';
|
|
3
4
|
/**
|
|
4
5
|
* Generate README.md
|
|
5
6
|
*/
|
|
@@ -189,16 +190,18 @@ const client = new FhirClient('https://fhir.example.com/fhir', loggingFetch);
|
|
|
189
190
|
fs.writeFileSync(path.join(clientDir, "README.md"), content);
|
|
190
191
|
}
|
|
191
192
|
/**
|
|
192
|
-
* Install @babelfhir-ts/client
|
|
193
|
+
* Install @babelfhir-ts/client-<version> type declarations into the generated output's
|
|
193
194
|
* node_modules so tsc can resolve the import during compilation.
|
|
194
195
|
* At runtime, the consumer installs the actual package via npm.
|
|
195
196
|
*/
|
|
196
197
|
export function installBaseClientTypes(outputDir) {
|
|
197
|
-
const
|
|
198
|
+
const slug = versionSlug();
|
|
199
|
+
const clientPkg = `client-${slug}`;
|
|
200
|
+
const pkgDir = path.join(outputDir, "node_modules", "@babelfhir-ts", clientPkg);
|
|
198
201
|
fs.mkdirSync(pkgDir, { recursive: true });
|
|
199
202
|
// Minimal package.json
|
|
200
203
|
fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify({
|
|
201
|
-
name:
|
|
204
|
+
name: `@babelfhir-ts/${clientPkg}`,
|
|
202
205
|
version: "0.0.0-types",
|
|
203
206
|
types: "index.d.ts"
|
|
204
207
|
}, null, 2));
|
|
@@ -504,7 +504,7 @@ export class SmartAuth {
|
|
|
504
504
|
/**
|
|
505
505
|
* Generate smart-client.ts — SmartFhirClient that wraps FhirClient with SMART auth
|
|
506
506
|
*/
|
|
507
|
-
export function generateSmartClient(clientDir,
|
|
507
|
+
export function generateSmartClient(clientDir, _resourceTypes) {
|
|
508
508
|
const content = `import { FhirClient, FhirReadClient, FhirWriteClient } from "./fhir-client.js";
|
|
509
509
|
import { SmartAuth } from "./smart-auth.js";
|
|
510
510
|
import type { SmartConfig, SmartToken, LaunchMode } from "./smart-auth.js";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '
|
|
3
|
-
import { logger } from "
|
|
1
|
+
import { ctx, fhirModuleSpecifier } from '../../fhir/versionContext.js';
|
|
2
|
+
import { sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
|
|
3
|
+
import { logger } from "../../../logger.js";
|
|
4
4
|
/**
|
|
5
5
|
* Manages TypeScript import statements for generated FHIR interfaces.
|
|
6
6
|
* Handles three types of imports:
|
|
7
|
-
* 1. FHIR base types from '@types/fhir' (via 'fhir
|
|
7
|
+
* 1. FHIR base types from '@types/fhir' (via 'fhir/<version>')
|
|
8
8
|
* 2. Custom local types (other generated profiles in the same package)
|
|
9
9
|
* 3. External profiled types (to be fetched/generated separately)
|
|
10
10
|
* 4. Path-based imports (custom relative paths, e.g., ValueSets in subdirectories)
|
|
@@ -162,10 +162,10 @@ export class ImportManager {
|
|
|
162
162
|
*/
|
|
163
163
|
generateImportStatements() {
|
|
164
164
|
const statements = [];
|
|
165
|
-
// FHIR imports from
|
|
165
|
+
// FHIR imports from version-specific module
|
|
166
166
|
if (this.fhirImports.size > 0) {
|
|
167
167
|
const sortedFhirImports = Array.from(this.fhirImports).sort();
|
|
168
|
-
statements.push(`import { ${sortedFhirImports.join(', ')} } from "
|
|
168
|
+
statements.push(`import { ${sortedFhirImports.join(', ')} } from "${fhirModuleSpecifier()}";`);
|
|
169
169
|
}
|
|
170
170
|
// Path-based imports (e.g., ValueSets from subdirectories)
|
|
171
171
|
const sortedPathBasedImports = Array.from(this.pathBasedImports.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
@@ -196,8 +196,8 @@ export class ImportManager {
|
|
|
196
196
|
* Removes import statements for types that aren't actually used.
|
|
197
197
|
*/
|
|
198
198
|
pruneUnusedImports(output) {
|
|
199
|
-
// Prune unused FHIR imports
|
|
200
|
-
output = output.replace(/import \{([^}]+)\} from "fhir\/
|
|
199
|
+
// Prune unused FHIR imports (match any fhir/<version> module specifier)
|
|
200
|
+
output = output.replace(/import \{([^}]+)\} from "fhir\/[^"]+";?/g, (full, inside) => {
|
|
201
201
|
const used = [];
|
|
202
202
|
const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
|
|
203
203
|
for (const name of specifiers) {
|
|
@@ -206,7 +206,7 @@ export class ImportManager {
|
|
|
206
206
|
used.push(name);
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
-
return used.length ? `import { ${used.join(', ')} } from "
|
|
209
|
+
return used.length ? `import { ${used.join(', ')} } from "${fhirModuleSpecifier()}";` : '';
|
|
210
210
|
});
|
|
211
211
|
// Prune unused custom imports (both path-based and standard custom imports)
|
|
212
212
|
output = output.replace(/import \{([^}]+)\} from "\.\/(.+?)";?/g, (full, inside, path) => {
|
|
@@ -226,14 +226,14 @@ export class ImportManager {
|
|
|
226
226
|
}
|
|
227
227
|
/**
|
|
228
228
|
* Reorganizes imports to prefer local types over FHIR types where appropriate.
|
|
229
|
-
* Moves locally-generated types from fhir
|
|
229
|
+
* Moves locally-generated types from fhir/<version> imports to local imports.
|
|
230
230
|
*/
|
|
231
231
|
reorganizeImports(output) {
|
|
232
232
|
if (!this.existingProfiles || this.existingProfiles.size === 0) {
|
|
233
233
|
return output;
|
|
234
234
|
}
|
|
235
|
-
// Extract ALL types from fhir
|
|
236
|
-
const fhirImportRegex = /import \{([^}]+)\} from "fhir\/
|
|
235
|
+
// Extract ALL types from fhir/<version> imports (there may be multiple import lines)
|
|
236
|
+
const fhirImportRegex = /import \{([^}]+)\} from "fhir\/[^"]+";/g;
|
|
237
237
|
const allFhirTypes = [];
|
|
238
238
|
let match;
|
|
239
239
|
while ((match = fhirImportRegex.exec(output)) !== null) {
|
|
@@ -247,7 +247,7 @@ export class ImportManager {
|
|
|
247
247
|
const remainingFhirTypes = [];
|
|
248
248
|
for (const typeName of allFhirTypes) {
|
|
249
249
|
if (this.existingProfiles.has(typeName)) {
|
|
250
|
-
// Never move FhirResource to local imports; it must stay from fhir
|
|
250
|
+
// Never move FhirResource to local imports; it must stay from fhir/<version>
|
|
251
251
|
if (typeName !== 'FhirResource') {
|
|
252
252
|
localTypes.push(typeName);
|
|
253
253
|
}
|
|
@@ -259,12 +259,12 @@ export class ImportManager {
|
|
|
259
259
|
remainingFhirTypes.push(typeName);
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
|
-
// Remove ALL old fhir
|
|
263
|
-
output = output.replace(/import \{[^}]+\} from "fhir\/
|
|
262
|
+
// Remove ALL old fhir/<version> import lines
|
|
263
|
+
output = output.replace(/import \{[^}]+\} from "fhir\/[^"]+";\n?/g, '');
|
|
264
264
|
// Add back the imports at the top
|
|
265
265
|
let newImports = '';
|
|
266
266
|
if (remainingFhirTypes.length > 0) {
|
|
267
|
-
newImports += `import { ${remainingFhirTypes.join(', ')} } from "
|
|
267
|
+
newImports += `import { ${remainingFhirTypes.join(', ')} } from "${fhirModuleSpecifier()}";\n`;
|
|
268
268
|
}
|
|
269
269
|
for (const localType of localTypes) {
|
|
270
270
|
newImports += `import { ${localType} } from "./${localType}";\n`;
|
|
@@ -287,7 +287,7 @@ export class ImportManager {
|
|
|
287
287
|
* Checks if a type is a known FHIR type from @types/fhir.
|
|
288
288
|
*/
|
|
289
289
|
isFhirType(type) {
|
|
290
|
-
return type === 'FhirResource' ||
|
|
290
|
+
return type === 'FhirResource' || ctx().interfaceNames.includes(type);
|
|
291
291
|
}
|
|
292
292
|
/**
|
|
293
293
|
* Checks if a type name looks like a primitive that shouldn't be imported.
|