babelfhir-ts 1.0.33 → 1.0.34
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/out/generator/classGenerator.js +96 -12
- package/out/generator/importManager.js +7 -1
- package/out/generator/index.js +176 -18
- package/out/generator/interfaceGenerator.js +23 -20
- package/out/generator/packageManager.js +76 -0
- package/out/generator/packageParser.js +7 -17
- package/out/generator/randomSupportGenerator.js +185 -219
- package/out/generator/sdParser.js +129 -23
- package/out/generator/validatorGenerator.js +5 -6
- package/out/logger.js +68 -42
- package/out/main.js +30 -9
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { logger } from '../logger.js';
|
|
2
|
+
const log = logger.withTag('classGenerator');
|
|
1
3
|
// FHIR primitive types - these cannot have nested child properties (only _fieldName extensions)
|
|
2
4
|
const FHIR_PRIMITIVE_TYPES = new Set([
|
|
3
5
|
'string', 'boolean', 'integer', 'decimal', 'uri', 'url', 'canonical', 'base64Binary',
|
|
@@ -23,6 +25,36 @@ function primitivePlaceholder(type) {
|
|
|
23
25
|
default: return `randomId()`; // fallback
|
|
24
26
|
}
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate a sensible default value for a 'code' type field based on its name.
|
|
30
|
+
* Common FHIR code fields have known value sets - we pick a valid default.
|
|
31
|
+
*/
|
|
32
|
+
function generateCodeDefault(fieldName) {
|
|
33
|
+
const name = fieldName.toLowerCase();
|
|
34
|
+
// Map common field names to sensible defaults
|
|
35
|
+
if (name === 'language')
|
|
36
|
+
return `'en'`; // ISO 639-1 language code
|
|
37
|
+
if (name === 'confidentiality')
|
|
38
|
+
return `'N'`; // HL7 v3 Confidentiality (Normal)
|
|
39
|
+
if (name === 'status')
|
|
40
|
+
return `'active'`;
|
|
41
|
+
if (name === 'mode')
|
|
42
|
+
return `'snapshot'`;
|
|
43
|
+
if (name === 'intent')
|
|
44
|
+
return `'proposal'`;
|
|
45
|
+
if (name === 'priority')
|
|
46
|
+
return `'routine'`;
|
|
47
|
+
if (name === 'gender')
|
|
48
|
+
return `'unknown'`;
|
|
49
|
+
if (name === 'use')
|
|
50
|
+
return `'usual'`;
|
|
51
|
+
if (name === 'type')
|
|
52
|
+
return `'document'`;
|
|
53
|
+
if (name === 'content')
|
|
54
|
+
return `'complete'`;
|
|
55
|
+
// Fallback to a simple code pattern
|
|
56
|
+
return `'unknown'`;
|
|
57
|
+
}
|
|
26
58
|
const OBSERVATION_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/observation-category';
|
|
27
59
|
const CONDITION_CATEGORY_SYSTEM = 'http://terminology.hl7.org/CodeSystem/condition-category';
|
|
28
60
|
const ROLE_CODE_SYSTEM = 'http://terminology.hl7.org/CodeSystem/v3-RoleCode';
|
|
@@ -668,11 +700,22 @@ function generateSliceElement(slice, elementType) {
|
|
|
668
700
|
}
|
|
669
701
|
}
|
|
670
702
|
}
|
|
703
|
+
// Handle CodeableConcept slices with binding codes but no patternConstraint
|
|
704
|
+
// This synthesizes a valid CodeableConcept from the first binding code
|
|
705
|
+
// Critical for slices like category:us-core which require codes from a ValueSet
|
|
706
|
+
else if (elementType === 'CodeableConcept' && slice.binding?.codes && slice.binding.codes.length > 0) {
|
|
707
|
+
const firstCode = slice.binding.codes[0];
|
|
708
|
+
const system = firstCode.system || 'http://terminology.hl7.org/CodeSystem/v3-NullFlavor';
|
|
709
|
+
const code = firstCode.code;
|
|
710
|
+
const display = firstCode.display || code;
|
|
711
|
+
return JSON.stringify({
|
|
712
|
+
coding: [{ system, code, display }]
|
|
713
|
+
});
|
|
714
|
+
}
|
|
671
715
|
// Handle required children with their constraints
|
|
672
716
|
// Only process direct children (single-segment paths) to avoid complex nested object generation
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
console.log(`[generateSliceElement] Composition slice requiredChildren: ${JSON.stringify(slice.requiredChildren?.map(c => ({ path: c.path, isRequired: c.isRequired, profileUrls: c.profileUrls })))}`);
|
|
717
|
+
if (slice.name?.toLowerCase().includes('composition')) {
|
|
718
|
+
log.debug(`Composition slice requiredChildren: ${JSON.stringify(slice.requiredChildren?.map(c => ({ path: c.path, isRequired: c.isRequired, profileUrls: c.profileUrls })))}`);
|
|
676
719
|
}
|
|
677
720
|
if (slice.requiredChildren && slice.requiredChildren.length > 0) {
|
|
678
721
|
for (const child of slice.requiredChildren) {
|
|
@@ -713,10 +756,7 @@ function generateSliceElement(slice, elementType) {
|
|
|
713
756
|
const sliceName = slice.name?.toLowerCase() || '';
|
|
714
757
|
const profileUrls = child.profileUrls || [];
|
|
715
758
|
const profileUrl = profileUrls[0]; // Primary profile URL
|
|
716
|
-
|
|
717
|
-
if (process.env.DEBUG_FHIR_GEN === 'true') {
|
|
718
|
-
console.log(`[generateSliceElement] Resource child - sliceName: ${sliceName}, profileUrls: ${JSON.stringify(profileUrls)}, profileUrl: ${profileUrl}`);
|
|
719
|
-
}
|
|
759
|
+
log.debug(`Resource child - sliceName: ${sliceName}, profileUrls: ${JSON.stringify(profileUrls)}, profileUrl: ${profileUrl}`);
|
|
720
760
|
const metaProfile = profileUrl ? `, meta: { profile: ['${profileUrl}'] }` : '';
|
|
721
761
|
// Extract resource type from profile URL if available (e.g., Composition-uv-ips -> Composition)
|
|
722
762
|
// Also check child.type which may contain the base resource type
|
|
@@ -830,9 +870,8 @@ function generateSliceElement(slice, elementType) {
|
|
|
830
870
|
const sliceProfileUrls = slice.profileUrls || resourceChild?.profileUrls || [];
|
|
831
871
|
const primaryProfileUrl = sliceProfileUrls[0];
|
|
832
872
|
const metaProfileExpr = primaryProfileUrl ? `, meta: { profile: ['${primaryProfileUrl}'] }` : '';
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
console.log(`[generateSliceElement] Composition slice - name: ${slice.name}, profileUrls: ${JSON.stringify(slice.profileUrls)}, resourceChild?.profileUrls: ${JSON.stringify(resourceChild?.profileUrls)}, primaryProfileUrl: ${primaryProfileUrl}`);
|
|
873
|
+
if (sliceName.includes('composition') || sliceName.includes('document')) {
|
|
874
|
+
log.debug(`Composition slice - name: ${slice.name}, profileUrls: ${JSON.stringify(slice.profileUrls)}, resourceChild?.profileUrls: ${JSON.stringify(resourceChild?.profileUrls)}, primaryProfileUrl: ${primaryProfileUrl}`);
|
|
836
875
|
}
|
|
837
876
|
if (sliceName.includes('composition') || sliceName.includes('document')) {
|
|
838
877
|
// Composition slice - requires: text (narrative), identifier, author.display
|
|
@@ -883,6 +922,21 @@ function generateSliceElement(slice, elementType) {
|
|
|
883
922
|
// Device slice
|
|
884
923
|
return `{ fullUrl: 'urn:uuid:' + crypto.randomUUID(), resource: { resourceType: 'Device', id: randomId()${metaProfileExpr}, type: { coding: [{ system: 'http://snomed.info/sct', code: '49062001', display: 'Device' }] } } }`;
|
|
885
924
|
}
|
|
925
|
+
// Handle non-Bundle element types - don't return fullUrl structure for simple types
|
|
926
|
+
if (elementType === 'CodeableConcept') {
|
|
927
|
+
// For CodeableConcept slices (like category:us-core), return a valid CodeableConcept
|
|
928
|
+
// The binding/pattern should ideally provide the code, but fallback to a generic structure
|
|
929
|
+
return `{ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/data-absent-reason', code: 'unknown', display: 'Unknown' }] }`;
|
|
930
|
+
}
|
|
931
|
+
if (elementType === 'Coding') {
|
|
932
|
+
return `{ system: 'http://terminology.hl7.org/CodeSystem/data-absent-reason', code: 'unknown', display: 'Unknown' }`;
|
|
933
|
+
}
|
|
934
|
+
if (elementType === 'Identifier') {
|
|
935
|
+
return `{ system: 'http://example.org/ids', value: randomId() }`;
|
|
936
|
+
}
|
|
937
|
+
if (elementType === 'Reference') {
|
|
938
|
+
return `{ reference: 'Resource/' + randomId() }`;
|
|
939
|
+
}
|
|
886
940
|
// For BackboneElement without specific type, return a minimal placeholder
|
|
887
941
|
return `{ fullUrl: 'urn:uuid:' + randomId() }`;
|
|
888
942
|
}
|
|
@@ -1232,8 +1286,9 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
1232
1286
|
requiredInits.push(`${key(rf.name)}: ${initExpr}`);
|
|
1233
1287
|
continue;
|
|
1234
1288
|
}
|
|
1235
|
-
if (['string', 'number', 'boolean'].includes(rf.type)) {
|
|
1236
|
-
|
|
1289
|
+
if (['string', 'number', 'boolean', 'code'].includes(rf.type)) {
|
|
1290
|
+
// For 'code' type without binding, generate based on field name to pick a sensible default
|
|
1291
|
+
const expr = rf.type === 'code' ? generateCodeDefault(rf.name) : primitivePlaceholder(rf.type);
|
|
1237
1292
|
const initExpr = buildValueExpression(rf, expr);
|
|
1238
1293
|
requiredInits.push(`${key(rf.name)}: ${initExpr}`);
|
|
1239
1294
|
continue;
|
|
@@ -1310,6 +1365,35 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
1310
1365
|
code = JSON.stringify(parentBindingCode.code);
|
|
1311
1366
|
}
|
|
1312
1367
|
}
|
|
1368
|
+
// Check requiredChildren from type profile for required fields like 'version' and 'system'
|
|
1369
|
+
// This handles cases like CodingOPS which requires version field
|
|
1370
|
+
// and CodingAlphaID which has a fixed system value
|
|
1371
|
+
if (slice.requiredChildren && slice.requiredChildren.length > 0) {
|
|
1372
|
+
for (const child of slice.requiredChildren) {
|
|
1373
|
+
if (child.path === 'version' && !version) {
|
|
1374
|
+
// Check for fixed or pattern value first
|
|
1375
|
+
if (child.fixedValue && typeof child.fixedValue === 'string') {
|
|
1376
|
+
version = child.fixedValue;
|
|
1377
|
+
}
|
|
1378
|
+
else if (child.patternConstraint && typeof child.patternConstraint === 'string') {
|
|
1379
|
+
version = child.patternConstraint;
|
|
1380
|
+
}
|
|
1381
|
+
else if (child.isRequired) {
|
|
1382
|
+
// Version is required by type profile but not set - use current year
|
|
1383
|
+
version = new Date().getFullYear().toString();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (child.path === 'system') {
|
|
1387
|
+
// Check for fixed or pattern value for system
|
|
1388
|
+
if (child.fixedValue && typeof child.fixedValue === 'string') {
|
|
1389
|
+
system = child.fixedValue;
|
|
1390
|
+
}
|
|
1391
|
+
else if (child.patternConstraint && typeof child.patternConstraint === 'string') {
|
|
1392
|
+
system = child.patternConstraint;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1313
1397
|
codingProps.push(`system: ${JSON.stringify(system)}`);
|
|
1314
1398
|
if (version) {
|
|
1315
1399
|
codingProps.push(`version: ${JSON.stringify(version)}`);
|
|
@@ -75,12 +75,16 @@ export class ImportManager {
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
// Check if we already have an external profile registered with this URL
|
|
78
|
-
// If so, use the existing name to avoid duplicates
|
|
78
|
+
// If so, use the existing name to avoid duplicates, but still register baseType info
|
|
79
79
|
if (profileUrl) {
|
|
80
80
|
for (const [existingName, existingMeta] of this.externalProfiles) {
|
|
81
81
|
if (existingMeta.profileUrl === profileUrl) {
|
|
82
82
|
const existingNormalized = toPascalCase(sanitizeIdentifier(existingName));
|
|
83
83
|
this.customImports.add(existingNormalized);
|
|
84
|
+
// Still register baseType info even for duplicates (important for extension detection)
|
|
85
|
+
if (baseType) {
|
|
86
|
+
this.registerExternalProfile(typeName, profileUrl, baseType, isFhirBase);
|
|
87
|
+
}
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
90
|
}
|
|
@@ -139,9 +143,11 @@ export class ImportManager {
|
|
|
139
143
|
const existing = meta.baseTypes.get(sanitizedBase);
|
|
140
144
|
if (!existing) {
|
|
141
145
|
meta.baseTypes.set(sanitizedBase, { isFhir: isFhirBase });
|
|
146
|
+
logger.debug(`[ImportManager] Registered baseType '${sanitizedBase}' (isFhir=${isFhirBase}) for external profile '${normalizedTypeName}' (URL: ${resolvedProfileUrl})`);
|
|
142
147
|
}
|
|
143
148
|
else if (isFhirBase && !existing.isFhir) {
|
|
144
149
|
existing.isFhir = true;
|
|
150
|
+
logger.debug(`[ImportManager] Updated baseType '${sanitizedBase}' to isFhir=true for '${normalizedTypeName}'`);
|
|
145
151
|
}
|
|
146
152
|
}
|
|
147
153
|
}
|
package/out/generator/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { generateClass } from './classGenerator.js';
|
|
5
5
|
import { generateInterfaces } from './interfaceGenerator.js';
|
|
6
6
|
import { extractPackage, readStructureDefinitionsFromDir, createPackageFromDir, readValueSetCodesFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir } from './packageParser.js';
|
|
7
|
-
import { fetchStructureDefinitions, parseStructureDefinition, fetchStructureDefinition, registerLocalStructureDefinitions } from './sdParser.js';
|
|
7
|
+
import { fetchStructureDefinitions, parseStructureDefinition, fetchStructureDefinition, registerLocalStructureDefinitions, clearLocalStructureDefinitions } from './sdParser.js';
|
|
8
8
|
import { writeInterfaceAndValidatorToFile, mergeFields, alignFields, ensureDirectoryExists, writeToFile, getBaseResource, downloadFile, sanitizeIdentifier, toPascalCase, capitalize } from './utils.js';
|
|
9
9
|
import { generateValidateProfileFunction } from './validatorGenerator.js';
|
|
10
10
|
import { generateValueSetTypeScript, generateValueSetRegistry } from './valueSetGenerator.js';
|
|
@@ -294,14 +294,35 @@ async function processStructureDefinition(sd, ctx) {
|
|
|
294
294
|
failedFetchProfiles.push(profileInfo.profileUrl || normalizedTypeName);
|
|
295
295
|
const baseTypeCandidates = Array.from(profileInfo.baseTypes.entries());
|
|
296
296
|
const preferred = baseTypeCandidates.find(([, meta]) => meta.isFhir);
|
|
297
|
+
// Log detailed info about the external profile for debugging
|
|
298
|
+
log.info(`[ExternalProfile] ${normalizedTypeName}:`);
|
|
299
|
+
log.info(` - URL: ${profileInfo.profileUrl || 'N/A'}`);
|
|
300
|
+
log.info(` - baseTypes: ${baseTypeCandidates.map(([t, m]) => `${t}(isFhir=${m.isFhir})`).join(', ') || 'none'}`);
|
|
301
|
+
log.info(` - isExtensionProfile: ${profileInfo.isExtensionProfile ?? 'undefined'}`);
|
|
297
302
|
// Smart detection: if the URL or name contains "extension", or baseTypes includes Extension, use Extension as base type
|
|
303
|
+
// Also check if the profile is used in an extension context (isExtensionProfile flag from interfaceGenerator)
|
|
298
304
|
let baseType = preferred?.[0] || 'Element';
|
|
299
305
|
const urlLower = (profileInfo.profileUrl || '').toLowerCase();
|
|
300
306
|
const nameLower = normalizedTypeName.toLowerCase();
|
|
301
307
|
const hasExtensionInBaseTypes = baseTypeCandidates.some(([type]) => type === 'Extension');
|
|
302
|
-
|
|
308
|
+
const isUsedAsExtension = profileInfo.isExtensionProfile === true;
|
|
309
|
+
log.info(` - Initial baseType: ${baseType}`);
|
|
310
|
+
log.info(` - urlLower.includes('extension'): ${urlLower.includes('extension')}`);
|
|
311
|
+
log.info(` - nameLower.includes('extension'): ${nameLower.includes('extension')}`);
|
|
312
|
+
log.info(` - hasExtensionInBaseTypes: ${hasExtensionInBaseTypes}`);
|
|
313
|
+
log.info(` - isUsedAsExtension: ${isUsedAsExtension}`);
|
|
314
|
+
if (urlLower.includes('extension') || nameLower.includes('extension') || hasExtensionInBaseTypes || isUsedAsExtension) {
|
|
303
315
|
baseType = 'Extension';
|
|
316
|
+
log.info(` - Overriding baseType to Extension`);
|
|
304
317
|
}
|
|
318
|
+
// Safety: NEVER use 'Element' as base type for extension profiles - it causes TypeScript errors
|
|
319
|
+
// when the generated type is used in extension?: Extension[] arrays
|
|
320
|
+
if (baseType === 'Element') {
|
|
321
|
+
log.warn(`[ExternalProfile] Type alias for ${normalizedTypeName} is using 'Element' as base type. ` +
|
|
322
|
+
`If this type is used in extension arrays, it may cause TypeScript compilation errors. ` +
|
|
323
|
+
`URL: ${profileInfo.profileUrl || 'N/A'}, baseTypes: ${baseTypeCandidates.map(([t]) => t).join(', ') || 'none'}`);
|
|
324
|
+
}
|
|
325
|
+
log.info(` - Final baseType: ${baseType}`);
|
|
305
326
|
const importLine = `import type { ${baseType} } from "fhir/r4";\n\n`;
|
|
306
327
|
const typeAliasContent = `${importLine}// Type alias for external profile\n// Profile: ${profileInfo.profileUrl}\n// Note: Full StructureDefinition could not be fetched; using type alias based on ${baseType}\nexport type ${normalizedTypeName} = ${baseType};\n`;
|
|
307
328
|
writeToFile(path.join(outputDir, `${normalizedTypeName}.ts`), typeAliasContent);
|
|
@@ -690,6 +711,75 @@ async function processStructureDefinition(sd, ctx) {
|
|
|
690
711
|
key = parentPart.split(':')[0];
|
|
691
712
|
}
|
|
692
713
|
}
|
|
714
|
+
// For coding slices with profile URLs, resolve the profile to get required fields like 'version'
|
|
715
|
+
// This handles cases like coding:ICD-10-GM -> ISiKICD10GMCoding -> CodingICD10GM where version is required
|
|
716
|
+
if (lastPart === 'coding' && f.sliceName && f.profileUrls && f.profileUrls.length > 0) {
|
|
717
|
+
for (const profileUrl of f.profileUrls) {
|
|
718
|
+
// Check if already resolved
|
|
719
|
+
let resolved = resolvedProfilePatterns.get(profileUrl);
|
|
720
|
+
if (!resolved) {
|
|
721
|
+
try {
|
|
722
|
+
const profileSd = await fetchStructureDefinition(profileUrl);
|
|
723
|
+
if (profileSd) {
|
|
724
|
+
const patternData = {};
|
|
725
|
+
// Track required fields from this profile and its base
|
|
726
|
+
const requiredFields = [];
|
|
727
|
+
// Check elements for required fields
|
|
728
|
+
const checkElements = (elements) => {
|
|
729
|
+
if (!elements)
|
|
730
|
+
return;
|
|
731
|
+
for (const elem of elements) {
|
|
732
|
+
// Check for required version field (min >= 1)
|
|
733
|
+
if (elem.path?.endsWith('.version') && elem.min && elem.min >= 1) {
|
|
734
|
+
requiredFields.push('version');
|
|
735
|
+
}
|
|
736
|
+
// Check for fixedUri on system
|
|
737
|
+
if (elem.path?.endsWith('.system') && elem.fixedUri) {
|
|
738
|
+
patternData.system = elem.fixedUri;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
// Check this profile's elements
|
|
743
|
+
checkElements(profileSd.snapshot?.element);
|
|
744
|
+
checkElements(profileSd.differential?.element);
|
|
745
|
+
// Also check the base profile if there is one (e.g., CodingICD10GM requires version)
|
|
746
|
+
if (profileSd.baseDefinition) {
|
|
747
|
+
try {
|
|
748
|
+
const baseSd = await fetchStructureDefinition(profileSd.baseDefinition);
|
|
749
|
+
if (baseSd) {
|
|
750
|
+
checkElements(baseSd.snapshot?.element);
|
|
751
|
+
checkElements(baseSd.differential?.element);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
// Base profile resolution failed
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// If version is required, add it to the pattern
|
|
759
|
+
if (requiredFields.includes('version')) {
|
|
760
|
+
// Use the current year as the default version (common for ICD-10 codes)
|
|
761
|
+
patternData.version = new Date().getFullYear().toString();
|
|
762
|
+
}
|
|
763
|
+
if (Object.keys(patternData).length > 0) {
|
|
764
|
+
resolvedProfilePatterns.set(profileUrl, patternData);
|
|
765
|
+
resolved = patternData;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
// Profile resolution failed
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// Merge resolved profile pattern with existing patternConstraint
|
|
774
|
+
if (resolved && typeof resolved === 'object') {
|
|
775
|
+
const existingPattern = (f.patternConstraint && typeof f.patternConstraint === 'object')
|
|
776
|
+
? f.patternConstraint
|
|
777
|
+
: {};
|
|
778
|
+
// Merge: existing pattern takes precedence for system, resolved adds version
|
|
779
|
+
f.patternConstraint = { ...resolved, ...existingPattern };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
693
783
|
// For identifier slices with profile URLs, resolve the profile to get system constraint
|
|
694
784
|
// This handles cases like identifier:Arztnummer -> identifier-lanr where the system
|
|
695
785
|
// is defined in the referenced profile, not in the slicing profile itself
|
|
@@ -777,6 +867,35 @@ async function processStructureDefinition(sd, ctx) {
|
|
|
777
867
|
}
|
|
778
868
|
}
|
|
779
869
|
}
|
|
870
|
+
// Also extract binding sample codes for nested primitive paths (like valueQuantity.code)
|
|
871
|
+
// This handles cases where the binding is on a nested field, not a slice
|
|
872
|
+
// IMPORTANT: Only apply to truly nested paths (2+ parts after removing resource type)
|
|
873
|
+
// Top-level slices like category:us-core should NOT be handled here - they're handled below
|
|
874
|
+
else if (f.binding?.sampleCode && typeof f.binding.sampleCode === 'object') {
|
|
875
|
+
const elementId = f.elementId || f.name;
|
|
876
|
+
const idParts = elementId.split('.');
|
|
877
|
+
// Remove the resource type prefix (first part)
|
|
878
|
+
if (idParts.length > 2) { // Changed from > 1 to > 2: need at least 3 parts (Resource.field.nested)
|
|
879
|
+
idParts.shift();
|
|
880
|
+
// Build nested key, extracting slice names where present
|
|
881
|
+
const nestedParts = [];
|
|
882
|
+
for (const part of idParts) {
|
|
883
|
+
if (part.includes(':')) {
|
|
884
|
+
nestedParts.push(part.split(':')[1]);
|
|
885
|
+
}
|
|
886
|
+
else if (!part.includes('[x]')) {
|
|
887
|
+
nestedParts.push(part);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (nestedParts.length > 1) { // Changed from > 0 to > 1: need at least 2 nested parts for truly nested path
|
|
891
|
+
const nestedKey = nestedParts.join('.');
|
|
892
|
+
// Only add if we don't already have this key
|
|
893
|
+
if (!patternMap.has(nestedKey)) {
|
|
894
|
+
patternMap.set(nestedKey, f.binding.sampleCode.code);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
780
899
|
// Otherwise, if this is a slice with binding codes, synthesize a pattern
|
|
781
900
|
else if (!patternMap.has(key) && f.sliceName && f.binding && f.binding.codes && f.binding.codes.length > 0) {
|
|
782
901
|
// Create a CodeableConcept pattern from the first binding code
|
|
@@ -802,13 +921,38 @@ async function processStructureDefinition(sd, ctx) {
|
|
|
802
921
|
const refReqKey = `_refReq_${parentFieldName}`;
|
|
803
922
|
if (!patternMap.has(refReqKey)) {
|
|
804
923
|
// Store as an object with the nested requirements paths
|
|
924
|
+
// For extension fields, store profile URL so enrichResource can generate correct extension
|
|
805
925
|
const reqPaths = {};
|
|
806
926
|
for (const req of requirements) {
|
|
807
|
-
|
|
927
|
+
if (req.path === 'extension' && req.profileUrl) {
|
|
928
|
+
// Store extension profile URL for primitive element extension generation
|
|
929
|
+
reqPaths[req.path] = { profile: req.profileUrl };
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
reqPaths[req.path] = true; // Mark each required path
|
|
933
|
+
}
|
|
808
934
|
}
|
|
809
935
|
patternMap.set(refReqKey, reqPaths);
|
|
810
936
|
}
|
|
811
937
|
}
|
|
938
|
+
// Add type option markers for choice elements (like value[x]) constrained to specific types
|
|
939
|
+
// This enables enrichResource to use the correct value[x] variant (e.g., valueCodeableConcept instead of valueQuantity)
|
|
940
|
+
// IMPORTANT: Check candidateRequiredFields (all fields), not just requiredFields, because value[x] is typically optional (min=0)
|
|
941
|
+
for (const f of candidateRequiredFields) {
|
|
942
|
+
// Only consider choice type fields (ending with [x]) that have type constraints
|
|
943
|
+
if (f.name.endsWith('[x]') && f.typeOptions && f.typeOptions.length > 0) {
|
|
944
|
+
// Extract the field name from the path (e.g., "Observation.value[x]" -> "value[x]")
|
|
945
|
+
const parts = f.name.split('.');
|
|
946
|
+
if (parts.length === 2) {
|
|
947
|
+
const fieldName = parts[1];
|
|
948
|
+
const typeMarkerKey = `_choiceType_${fieldName}`;
|
|
949
|
+
if (!patternMap.has(typeMarkerKey)) {
|
|
950
|
+
// Store the allowed types so enrichResource can pick the right variant
|
|
951
|
+
patternMap.set(typeMarkerKey, f.typeOptions.map(t => t.code));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
812
956
|
const fieldPatterns = Array.from(patternMap.entries()).map(([fieldName, pattern]) => ({ fieldName, pattern }));
|
|
813
957
|
// Merge patterns from fieldPatterns into requiredFields so applyRequiredFieldHeuristic can use them
|
|
814
958
|
// IMPORTANT: Don't add patternConstraint if the field has requiredSlices - those contain the correct pattern info
|
|
@@ -846,6 +990,11 @@ async function processStructureDefinition(sd, ctx) {
|
|
|
846
990
|
return interfaceName;
|
|
847
991
|
}
|
|
848
992
|
export async function generate(fhirSource, outputDir, flags) {
|
|
993
|
+
// Initialize logger at the start of generation
|
|
994
|
+
const logDest = flags?.logDestination || flags?.logLevel || 'console';
|
|
995
|
+
logger.setLevel(logDest, outputDir, flags?.logVerbosity || 'info');
|
|
996
|
+
// Clear local StructureDefinitions from any previous generation
|
|
997
|
+
clearLocalStructureDefinitions();
|
|
849
998
|
logger.log(`Fetching StructureDefinitions from: ${fhirSource}`);
|
|
850
999
|
let structureDefinitions = [];
|
|
851
1000
|
let localPath = fhirSource;
|
|
@@ -929,12 +1078,23 @@ export async function generate(fhirSource, outputDir, flags) {
|
|
|
929
1078
|
* folder, then repackage to the specified output archive (or alongside original).
|
|
930
1079
|
*/
|
|
931
1080
|
export async function generateIntoPackage(packageArchivePath, outArchivePath, flags) {
|
|
1081
|
+
// Initialize logger at the start of generation
|
|
1082
|
+
const logDest = flags?.logDestination || flags?.logLevel || 'console';
|
|
1083
|
+
logger.setLevel(logDest, path.dirname(outArchivePath || packageArchivePath), flags?.logVerbosity || 'info');
|
|
1084
|
+
// Clear local StructureDefinitions from any previous generation
|
|
1085
|
+
clearLocalStructureDefinitions();
|
|
932
1086
|
if (!packageArchivePath.endsWith('.tgz') && !packageArchivePath.endsWith('.zip'))
|
|
933
1087
|
throw new Error('Only .tgz or .zip supported.');
|
|
934
1088
|
const extractResult = await extractPackage(packageArchivePath);
|
|
935
1089
|
const extractedRoot = extractResult.path;
|
|
936
1090
|
const shouldCleanup = !extractResult.isCached; // Only cleanup temp directories, not cache
|
|
937
1091
|
try {
|
|
1092
|
+
// Load dependencies from package.json BEFORE reading StructureDefinitions
|
|
1093
|
+
// This ensures base profiles (like coverage-de-basis) are available for resolution
|
|
1094
|
+
const { getPackageManager } = await import('./packageManager.js');
|
|
1095
|
+
const packageManager = getPackageManager();
|
|
1096
|
+
packageManager.loadDependenciesFromExtractedPackage(extractedRoot);
|
|
1097
|
+
logger.log(`Loaded package dependencies (${packageManager.getTotalStructureDefinitionCount()} total SDs available)`);
|
|
938
1098
|
const structureDefinitions = readStructureDefinitionsFromDir(extractedRoot);
|
|
939
1099
|
// Load ValueSets from package AND its dependencies (for binding resolution)
|
|
940
1100
|
const valueSetCodesMap = readValueSetCodesWithDependencies(extractedRoot);
|
|
@@ -985,15 +1145,11 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
985
1145
|
try {
|
|
986
1146
|
if (fs.existsSync(vdir)) {
|
|
987
1147
|
fs.rmSync(vdir, { recursive: true, force: true });
|
|
988
|
-
|
|
989
|
-
log.debug(`Removed stale ValueSets directory: ${vdir}`);
|
|
990
|
-
}
|
|
1148
|
+
log.debug(`Removed stale ValueSets directory: ${vdir}`);
|
|
991
1149
|
}
|
|
992
1150
|
}
|
|
993
1151
|
catch (e) {
|
|
994
|
-
|
|
995
|
-
log.warn(`Failed to remove stale ValueSets directory ${vdir}: ${e.message}`);
|
|
996
|
-
}
|
|
1152
|
+
log.debug(`Failed to remove stale ValueSets directory ${vdir}: ${e.message}`);
|
|
997
1153
|
}
|
|
998
1154
|
}
|
|
999
1155
|
// Build set of existing StructureDefinition names
|
|
@@ -1099,8 +1255,7 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
1099
1255
|
fs.rmSync(extractedRoot, { recursive: true, force: true });
|
|
1100
1256
|
}
|
|
1101
1257
|
catch (err) {
|
|
1102
|
-
|
|
1103
|
-
log.warn(`Cleanup failed: ${err.message}`);
|
|
1258
|
+
log.debug(`Cleanup failed: ${err.message}`);
|
|
1104
1259
|
}
|
|
1105
1260
|
}
|
|
1106
1261
|
}
|
|
@@ -1114,7 +1269,10 @@ function deriveOutputArchiveName(input) {
|
|
|
1114
1269
|
*/
|
|
1115
1270
|
export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
1116
1271
|
// Initialize logger
|
|
1117
|
-
|
|
1272
|
+
const logDest = flags?.logDestination || flags?.logLevel || 'console';
|
|
1273
|
+
logger.setLevel(logDest, outputDir, flags?.logVerbosity || 'info');
|
|
1274
|
+
// Clear local StructureDefinitions from any previous generation
|
|
1275
|
+
clearLocalStructureDefinitions();
|
|
1118
1276
|
if (!fs.existsSync(inputDir))
|
|
1119
1277
|
throw new Error(`Input directory not found: ${inputDir}`);
|
|
1120
1278
|
if (!fs.existsSync(outputDir))
|
|
@@ -1162,15 +1320,11 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
1162
1320
|
try {
|
|
1163
1321
|
if (fs.existsSync(valueSetOutputDir)) {
|
|
1164
1322
|
fs.rmSync(valueSetOutputDir, { recursive: true, force: true });
|
|
1165
|
-
|
|
1166
|
-
log.debug(`Removed stale ValueSets directory: ${valueSetOutputDir}`);
|
|
1167
|
-
}
|
|
1323
|
+
log.debug(`Removed stale ValueSets directory: ${valueSetOutputDir}`);
|
|
1168
1324
|
}
|
|
1169
1325
|
}
|
|
1170
1326
|
catch (e) {
|
|
1171
|
-
|
|
1172
|
-
log.warn(`Failed to remove stale ValueSets directory ${valueSetOutputDir}: ${e.message}`);
|
|
1173
|
-
}
|
|
1327
|
+
log.debug(`Failed to remove stale ValueSets directory ${valueSetOutputDir}: ${e.message}`);
|
|
1174
1328
|
}
|
|
1175
1329
|
}
|
|
1176
1330
|
const entries = fs.readdirSync(inputDir);
|
|
@@ -1259,6 +1413,8 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
1259
1413
|
}
|
|
1260
1414
|
}
|
|
1261
1415
|
export async function generateFromJsonFile(jsonFilePath, outputDir, flags) {
|
|
1416
|
+
// Clear local StructureDefinitions from any previous generation
|
|
1417
|
+
clearLocalStructureDefinitions();
|
|
1262
1418
|
if (!fs.existsSync(jsonFilePath))
|
|
1263
1419
|
throw new Error(`File not found: ${jsonFilePath}`);
|
|
1264
1420
|
const raw = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8'));
|
|
@@ -1274,6 +1430,8 @@ export async function generateFromJsonFile(jsonFilePath, outputDir, flags) {
|
|
|
1274
1430
|
* @param flags - Generation options
|
|
1275
1431
|
*/
|
|
1276
1432
|
export async function generateFromProfileUrl(profileUrl, outputDir, flags) {
|
|
1433
|
+
// Clear local StructureDefinitions from any previous generation
|
|
1434
|
+
clearLocalStructureDefinitions();
|
|
1277
1435
|
log.info(`Fetching profile: ${profileUrl}`);
|
|
1278
1436
|
// Load all cached packages to make profile resolution work
|
|
1279
1437
|
const { getPackageManager } = await import('./packageManager.js');
|
|
@@ -7,8 +7,7 @@ import { r4Rules } from './fhirR4Rules.js';
|
|
|
7
7
|
import { postProcessExtensions, safetyNetRootExtensions } from './postProcessExtensions.js';
|
|
8
8
|
const log = logger.withTag('interfaces');
|
|
9
9
|
export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles, fhirChildTypeMap, profileIdToName, profileUrlToName) {
|
|
10
|
-
const debug = (...args) =>
|
|
11
|
-
log.debug(...args); };
|
|
10
|
+
const debug = (...args) => log.debug(...args);
|
|
12
11
|
const interfaces = [];
|
|
13
12
|
const localInterfaceNames = new Set();
|
|
14
13
|
const writtenLines = new Set();
|
|
@@ -25,6 +24,18 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
25
24
|
}
|
|
26
25
|
importManager.addType(typeName, profileUrl, baseType, isFhirBase);
|
|
27
26
|
};
|
|
27
|
+
// Helper to determine if a field is an extension field and get the correct base type
|
|
28
|
+
// Extension fields should always use 'Extension' as base type for proper fallback aliases
|
|
29
|
+
const getBaseTypeForField = (fieldName, fallbackBaseType) => {
|
|
30
|
+
const fieldNameLower = fieldName.toLowerCase();
|
|
31
|
+
const isExtensionField = fieldNameLower.endsWith('.extension') ||
|
|
32
|
+
fieldNameLower.endsWith('.modifierextension') ||
|
|
33
|
+
fieldNameLower.includes('extension:') ||
|
|
34
|
+
fieldNameLower.includes('modifierextension:');
|
|
35
|
+
const baseTypeName = isExtensionField ? 'Extension' : fallbackBaseType;
|
|
36
|
+
const baseIsFhir = isExtensionField || (baseTypeName ? isFhirType(baseTypeName) : false);
|
|
37
|
+
return { baseTypeName, baseIsFhir };
|
|
38
|
+
};
|
|
28
39
|
// Helper to find exact interface block index (avoid prefix collisions like Foo and FooBar)
|
|
29
40
|
const findInterfaceIndex = (name) => {
|
|
30
41
|
const re = new RegExp(`^export interface ${name}(?:[\n\r\t ]|extends)`);
|
|
@@ -171,16 +182,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
171
182
|
if (typeName && !isFhirType(typeName) && !isPrimitiveType(typeName)) {
|
|
172
183
|
// We don't have baseFld yet, so get it from the base fields
|
|
173
184
|
const baseFldForProfile = baseFields.find(bf => bf.name === field.name);
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
// so fallback aliases are compatible with extension arrays
|
|
177
|
-
const fieldNameLower = field.name.toLowerCase();
|
|
178
|
-
const isExtensionField = fieldNameLower.endsWith('.extension') ||
|
|
179
|
-
fieldNameLower.endsWith('.modifierextension') ||
|
|
180
|
-
fieldNameLower.includes('extension:') ||
|
|
181
|
-
fieldNameLower.includes('modifierextension:');
|
|
182
|
-
const baseTypeName = isExtensionField ? 'Extension' : (baseFldForProfile?.type || field.type);
|
|
183
|
-
const baseIsFhir = isExtensionField || (baseTypeName ? isFhirType(baseTypeName) : false);
|
|
185
|
+
// Use helper to determine base type - extension fields get 'Extension' as base type
|
|
186
|
+
const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFldForProfile?.type || field.type);
|
|
184
187
|
addTypeImport(typeName, profileUrl, baseTypeName, baseIsFhir);
|
|
185
188
|
}
|
|
186
189
|
}
|
|
@@ -917,8 +920,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
917
920
|
const line = `${fieldName}${field.isOptional ? '?' : ''}: ${resolvedType}${isArrayFinal ? '[]' : ''};`;
|
|
918
921
|
if (field.isProfiled && field.profileUrls?.[0] && field.type && !isPrimitiveType(field.type) && !isFhirType(field.type)) {
|
|
919
922
|
const profileUrl = field.profileUrls[0];
|
|
920
|
-
|
|
921
|
-
const baseIsFhir =
|
|
923
|
+
// Use helper to determine base type - extension fields get 'Extension' as base type
|
|
924
|
+
const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFld?.type);
|
|
922
925
|
addTypeImport(field.type, profileUrl, baseTypeName, baseIsFhir);
|
|
923
926
|
}
|
|
924
927
|
// Register external profiles for any field with profile URLs, regardless of nesting
|
|
@@ -927,8 +930,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
927
930
|
const profileUrl = field.profileUrls[0];
|
|
928
931
|
// Strip version suffix from canonical URL before extracting name
|
|
929
932
|
const strippedUrl = stripVersionFromCanonicalUrl(profileUrl);
|
|
930
|
-
|
|
931
|
-
const baseIsFhir =
|
|
933
|
+
// Use helper to determine base type - extension fields get 'Extension' as base type
|
|
934
|
+
const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFld?.type);
|
|
932
935
|
// Use profileUrlToName to get the actual interface name, fallback to URL-derived name
|
|
933
936
|
const typeName = profileUrlToName?.get(strippedUrl) || profileUrlToName?.get(profileUrl) || toPascalCase(sanitizeIdentifier(strippedUrl.split('/').pop() || resolvedType));
|
|
934
937
|
// Only register if it's not a primitive and not already a FHIR type
|
|
@@ -942,8 +945,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
942
945
|
!isFhirType(resolvedType) &&
|
|
943
946
|
!localInterfaceNames.has(resolvedType)) {
|
|
944
947
|
const profileUrl = field.profileUrls?.[0] || (field.type && field.type.includes('http') ? field.type : undefined);
|
|
945
|
-
|
|
946
|
-
const baseIsFhir =
|
|
948
|
+
// Use helper to determine base type - extension fields get 'Extension' as base type
|
|
949
|
+
const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFld?.type);
|
|
947
950
|
addTypeImport(resolvedType, profileUrl, baseTypeName, baseIsFhir);
|
|
948
951
|
}
|
|
949
952
|
if (!isPrimitiveType(resolvedType)) {
|
|
@@ -983,8 +986,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
983
986
|
else {
|
|
984
987
|
// Pass profile metadata if available
|
|
985
988
|
const profileUrl = field.profileUrls?.[0];
|
|
986
|
-
|
|
987
|
-
const baseIsFhir =
|
|
989
|
+
// Use helper to determine base type - extension fields get 'Extension' as base type
|
|
990
|
+
const { baseTypeName, baseIsFhir } = getBaseTypeForField(field.name, baseFld?.type);
|
|
988
991
|
if (profileUrl || baseTypeName) {
|
|
989
992
|
addTypeImport(sanitizeIdentifier(resolvedType), profileUrl, baseTypeName, baseIsFhir);
|
|
990
993
|
}
|