babelfhir-ts 1.0.12 → 1.0.13
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/index.js +66 -14
- package/out/generator/interfaceGenerator.js +231 -47
- package/out/generator/sdParser.js +62 -3
- package/package.json +1 -1
package/out/generator/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import { generateClass } from './classGenerator';
|
|
4
4
|
import { generateInterfaces } from './interfaceGenerator';
|
|
5
5
|
import { extractPackage, readStructureDefinitionsFromDir, createPackageFromDir, readValueSetCodesFromDir, readValueSetsFromDir } from './packageParser';
|
|
6
|
-
import { fetchStructureDefinitions, parseStructureDefinition, fetchStructureDefinition } from './sdParser';
|
|
6
|
+
import { fetchStructureDefinitions, parseStructureDefinition, fetchStructureDefinition, registerLocalStructureDefinitions, buildFhirChildTypeMap } from './sdParser';
|
|
7
7
|
import { writeInterfaceAndValidatorToFile, mergeFields, alignFields, ensureDirectoryExists, writeToFile, getBaseResource, downloadFile, sanitizeIdentifier, toPascalCase } from './utils';
|
|
8
8
|
import { generateValidateProfileFunction } from './validatorGenerator';
|
|
9
9
|
import { loadExamplesFromPackage, generateTestFile } from './testGenerator';
|
|
@@ -55,7 +55,7 @@ export function skeletonQuantity(): Quantity { return { value: randomInt(1,100),
|
|
|
55
55
|
}
|
|
56
56
|
// Reusable processor for a single StructureDefinition
|
|
57
57
|
async function processStructureDefinition(sd, ctx) {
|
|
58
|
-
const { outputDir, fhirSourceHint, valueSetCodesMap, valueSets, examplesMap, existingStructureDefinitions, profileIdToName, flags } = ctx;
|
|
58
|
+
const { outputDir, fhirSourceHint, valueSetCodesMap, valueSets, examplesMap, existingStructureDefinitions, profileIdToName, flags, fhirChildTypeMap } = ctx;
|
|
59
59
|
const rawInterfaceName = sd.name || sd.id || 'UnnamedInterface';
|
|
60
60
|
const interfaceName = toPascalCase(rawInterfaceName);
|
|
61
61
|
let baseResource = sanitizeIdentifier(getBaseResource(sd.baseDefinition));
|
|
@@ -72,7 +72,7 @@ async function processStructureDefinition(sd, ctx) {
|
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
const aligned = alignFields(parsed.newFields, parsed.oldFields);
|
|
75
|
-
const interfaceResult = generateInterfaces(interfaceName, aligned.alignedNewFields, baseResource, parsed.oldFields, valueSets, sd.type, existingStructureDefinitions);
|
|
75
|
+
const interfaceResult = generateInterfaces(interfaceName, aligned.alignedNewFields, baseResource, parsed.oldFields, valueSets, sd.type, existingStructureDefinitions, fhirChildTypeMap);
|
|
76
76
|
const interfaceContent = interfaceResult.content;
|
|
77
77
|
// Generate type alias files for external profiled identifiers that don't exist as StructureDefinitions
|
|
78
78
|
for (const [typeName, profileInfo] of interfaceResult.referencedExternalProfiles) {
|
|
@@ -200,6 +200,13 @@ export async function generate(fhirSource, outputDir, flags) {
|
|
|
200
200
|
}
|
|
201
201
|
console.log(`Fetched ${structureDefinitions.length} StructureDefinitions.`);
|
|
202
202
|
ensureDirectoryExists(outputDir);
|
|
203
|
+
// Precompute child type map for common core datatypes (best effort; silent on failures)
|
|
204
|
+
let fhirChildTypeMap = new Map();
|
|
205
|
+
try {
|
|
206
|
+
const coreTypes = ['Identifier', 'CodeableConcept', 'Coding', 'Reference', 'Period', 'HumanName', 'Address'];
|
|
207
|
+
fhirChildTypeMap = await buildFhirChildTypeMap(coreTypes, 'R4');
|
|
208
|
+
}
|
|
209
|
+
catch { /* non-fatal */ }
|
|
203
210
|
// Build set of existing StructureDefinition names to avoid generating type aliases for them
|
|
204
211
|
const existingStructureDefinitions = new Set();
|
|
205
212
|
const profileIdToName = new Map(); // Map from profile ID to interface name
|
|
@@ -216,7 +223,7 @@ export async function generate(fhirSource, outputDir, flags) {
|
|
|
216
223
|
}
|
|
217
224
|
const examples = loadExamplesFromPackage(fhirSource);
|
|
218
225
|
for (const sd of structureDefinitions) {
|
|
219
|
-
await processStructureDefinition(sd, { outputDir, fhirSourceHint: fhirSource, valueSetCodesMap, valueSets, examplesMap: examples, existingStructureDefinitions, profileIdToName, flags });
|
|
226
|
+
await processStructureDefinition(sd, { outputDir, fhirSourceHint: fhirSource, valueSetCodesMap, valueSets, examplesMap: examples, existingStructureDefinitions, profileIdToName, flags, fhirChildTypeMap });
|
|
220
227
|
}
|
|
221
228
|
}
|
|
222
229
|
/**
|
|
@@ -237,9 +244,9 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
237
244
|
const outputDir = path.join(extractedRoot, 'generated');
|
|
238
245
|
ensureDirectoryExists(outputDir);
|
|
239
246
|
// Generate TypeScript files for ValueSets inside the embedded package
|
|
247
|
+
let didGenerateValueSets = false;
|
|
240
248
|
if (valueSets.size > 0) {
|
|
241
249
|
const valueSetOutputDir = path.join(outputDir, 'valuesets');
|
|
242
|
-
ensureDirectoryExists(valueSetOutputDir);
|
|
243
250
|
let generatedValueSetCount = 0;
|
|
244
251
|
let skippedEmptyCount = 0;
|
|
245
252
|
for (const valueSet of valueSets.values()) {
|
|
@@ -248,6 +255,10 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
248
255
|
continue;
|
|
249
256
|
}
|
|
250
257
|
try {
|
|
258
|
+
// Only create directory when we actually have files to write
|
|
259
|
+
if (generatedValueSetCount === 0) {
|
|
260
|
+
ensureDirectoryExists(valueSetOutputDir);
|
|
261
|
+
}
|
|
251
262
|
const { filename, content } = generateValueSetTypeScript(valueSet);
|
|
252
263
|
fs.writeFileSync(path.join(valueSetOutputDir, filename), content, 'utf-8');
|
|
253
264
|
generatedValueSetCount++;
|
|
@@ -260,9 +271,29 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
260
271
|
const registryContent = generateValueSetRegistry(valueSets);
|
|
261
272
|
fs.writeFileSync(path.join(valueSetOutputDir, 'index.ts'), registryContent, 'utf-8');
|
|
262
273
|
console.log(`Generated ${generatedValueSetCount} ValueSet files (skipped ${skippedEmptyCount} empty ValueSets) in ${valueSetOutputDir}`);
|
|
274
|
+
didGenerateValueSets = true;
|
|
263
275
|
}
|
|
264
|
-
else
|
|
265
|
-
|
|
276
|
+
else {
|
|
277
|
+
if (skippedEmptyCount > 0) {
|
|
278
|
+
console.log(`Skipped ${skippedEmptyCount} empty ValueSets (no files generated)`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Ensure no stale valuesets directory remains if we didn't generate any files
|
|
283
|
+
if (!didGenerateValueSets) {
|
|
284
|
+
const vdir = path.join(path.join(extractedRoot, 'generated'), 'valuesets');
|
|
285
|
+
try {
|
|
286
|
+
if (fs.existsSync(vdir)) {
|
|
287
|
+
fs.rmSync(vdir, { recursive: true, force: true });
|
|
288
|
+
if (process.env.DEBUG_FHIR_GEN === 'true') {
|
|
289
|
+
console.debug(`Removed stale ValueSets directory: ${vdir}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
if (process.env.DEBUG_FHIR_GEN === 'true') {
|
|
295
|
+
console.warn(`Failed to remove stale ValueSets directory ${vdir}: ${e.message}`);
|
|
296
|
+
}
|
|
266
297
|
}
|
|
267
298
|
}
|
|
268
299
|
// Build set of existing StructureDefinition names
|
|
@@ -278,6 +309,8 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
278
309
|
}
|
|
279
310
|
}
|
|
280
311
|
}
|
|
312
|
+
// Register all local StructureDefinitions for resolution before HTTP fetches
|
|
313
|
+
registerLocalStructureDefinitions(structureDefinitions);
|
|
281
314
|
const examples = loadExamplesFromPackage(extractedRoot);
|
|
282
315
|
for (const sd of structureDefinitions) {
|
|
283
316
|
await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, examplesMap: examples, existingStructureDefinitions, profileIdToName, flags });
|
|
@@ -338,7 +371,6 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
338
371
|
console.log(`Loaded ${valueSets.size} ValueSets from ${inputDir} (${Array.from(valueSets.values()).filter(vs => vs.isSmall).length} suitable for union types)`);
|
|
339
372
|
// Generate TypeScript files for ValueSets
|
|
340
373
|
const valueSetOutputDir = path.join(outputDir, 'valuesets');
|
|
341
|
-
ensureDirectoryExists(valueSetOutputDir);
|
|
342
374
|
let generatedValueSetCount = 0;
|
|
343
375
|
let skippedEmptyCount = 0;
|
|
344
376
|
for (const valueSet of valueSets.values()) {
|
|
@@ -348,6 +380,10 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
348
380
|
continue;
|
|
349
381
|
}
|
|
350
382
|
try {
|
|
383
|
+
// Only create directory when we actually have files to write
|
|
384
|
+
if (generatedValueSetCount === 0) {
|
|
385
|
+
ensureDirectoryExists(valueSetOutputDir);
|
|
386
|
+
}
|
|
351
387
|
const { filename, content } = generateValueSetTypeScript(valueSet);
|
|
352
388
|
const outputPath = path.join(valueSetOutputDir, filename);
|
|
353
389
|
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
@@ -364,8 +400,24 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
364
400
|
fs.writeFileSync(registryPath, registryContent, 'utf-8');
|
|
365
401
|
console.log(`Generated ${generatedValueSetCount} ValueSet files (skipped ${skippedEmptyCount} empty ValueSets) in ${valueSetOutputDir}`);
|
|
366
402
|
}
|
|
367
|
-
else
|
|
368
|
-
|
|
403
|
+
else {
|
|
404
|
+
if (skippedEmptyCount > 0) {
|
|
405
|
+
console.log(`Skipped ${skippedEmptyCount} empty ValueSets (no files generated)`);
|
|
406
|
+
}
|
|
407
|
+
// Remove any stale valuesets directory from previous runs
|
|
408
|
+
try {
|
|
409
|
+
if (fs.existsSync(valueSetOutputDir)) {
|
|
410
|
+
fs.rmSync(valueSetOutputDir, { recursive: true, force: true });
|
|
411
|
+
if (process.env.DEBUG_FHIR_GEN === 'true') {
|
|
412
|
+
console.debug(`Removed stale ValueSets directory: ${valueSetOutputDir}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
if (process.env.DEBUG_FHIR_GEN === 'true') {
|
|
418
|
+
console.warn(`Failed to remove stale ValueSets directory ${valueSetOutputDir}: ${e.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
369
421
|
}
|
|
370
422
|
const entries = fs.readdirSync(inputDir);
|
|
371
423
|
const archives = entries.filter(f => f.endsWith('.tgz') || f.endsWith('.zip'));
|
|
@@ -377,11 +429,13 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
377
429
|
// Build set of existing StructureDefinition names from JSON files
|
|
378
430
|
const existingStructureDefinitions = new Set();
|
|
379
431
|
const profileIdToName = new Map();
|
|
432
|
+
const localStructureDefinitions = [];
|
|
380
433
|
for (const jf of jsonFiles) {
|
|
381
434
|
try {
|
|
382
435
|
const raw = JSON.parse(fs.readFileSync(path.join(inputDir, jf), 'utf-8'));
|
|
383
436
|
if (typeof raw === 'object' && raw !== null && raw.resourceType === 'StructureDefinition') {
|
|
384
437
|
const sd = raw;
|
|
438
|
+
localStructureDefinitions.push(sd); // Collect for local resolution
|
|
385
439
|
const name = toPascalCase(sd.name || sd.id || '');
|
|
386
440
|
if (name) {
|
|
387
441
|
existingStructureDefinitions.add(name);
|
|
@@ -394,6 +448,8 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
394
448
|
}
|
|
395
449
|
catch { /* ignore parse errors in pre-scan */ }
|
|
396
450
|
}
|
|
451
|
+
// Register all local StructureDefinitions for resolution before HTTP fetches
|
|
452
|
+
registerLocalStructureDefinitions(localStructureDefinitions);
|
|
397
453
|
for (const file of archives) {
|
|
398
454
|
const fullPath = path.join(inputDir, file);
|
|
399
455
|
console.log(`Processing package: ${file}`);
|
|
@@ -411,10 +467,6 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
411
467
|
const full = path.join(inputDir, jf);
|
|
412
468
|
try {
|
|
413
469
|
const raw = JSON.parse(fs.readFileSync(full, 'utf-8'));
|
|
414
|
-
if (typeof raw !== 'object' || raw === null || raw.resourceType !== 'StructureDefinition') {
|
|
415
|
-
console.warn(`Skipping non-StructureDefinition JSON: ${jf}`);
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
470
|
await processStructureDefinition(raw, { outputDir, fhirSourceHint: full, valueSets, existingStructureDefinitions, profileIdToName, flags });
|
|
419
471
|
}
|
|
420
472
|
catch (err) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fhirInterfaceNames from "./fhirInterfaces.json";
|
|
2
|
-
import { capitalize, sanitizeIdentifier } from './utils';
|
|
2
|
+
import { capitalize, sanitizeIdentifier, toPascalCase } from './utils';
|
|
3
3
|
import { generateCodeUnionType, getUniformSystem } from './vsParser.js';
|
|
4
4
|
import { logger } from '../logger';
|
|
5
|
-
export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles) {
|
|
5
|
+
export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles, fhirChildTypeMap) {
|
|
6
6
|
const debug = (...args) => { if (process.env.DEBUG_FHIR_GEN === 'true')
|
|
7
|
-
|
|
7
|
+
console.log('[gen:interfaces]', ...args); };
|
|
8
8
|
const imports = new Set();
|
|
9
9
|
const customImports = new Set();
|
|
10
10
|
const interfacesRaw = [];
|
|
@@ -14,7 +14,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
14
14
|
return (...items) => {
|
|
15
15
|
for (const item of items) {
|
|
16
16
|
if (item.includes('Code') && interfaceName.includes('Condition')) {
|
|
17
|
-
|
|
17
|
+
console.log('[INTERFACES.PUSH]', { interfaceName, item: item.substring(0, 80), stack: new Error().stack?.split('\n')[2] });
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
return target.push(...items);
|
|
@@ -24,7 +24,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
24
24
|
return (...items) => {
|
|
25
25
|
for (const item of items) {
|
|
26
26
|
if (item.includes('Code') && interfaceName.includes('Condition')) {
|
|
27
|
-
|
|
27
|
+
console.log('[INTERFACES.UNSHIFT]', { interfaceName, item: item.substring(0, 80), stack: new Error().stack?.split('\n')[2] });
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
return target.unshift(...items);
|
|
@@ -138,10 +138,15 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
138
138
|
return `CodeableConcept & { coding: Array<{ code: ${codeUnion} }> }`;
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
|
-
function processFields(fields, parentInterfaceName, parentFieldType) {
|
|
141
|
+
function processFields(fields, parentInterfaceName, parentFieldType, isProcessingSlice = false) {
|
|
142
142
|
const interfaceLines = [];
|
|
143
143
|
const emittedLinesByField = new Map();
|
|
144
144
|
const processedFields = new Set();
|
|
145
|
+
// Context-aware resolver: only use base field metadata for direct children of the base resource.
|
|
146
|
+
const getBaseFldForContext = (fname, fparts, currentParent) => {
|
|
147
|
+
const isDirectBaseChild = currentParent === interfaceName && fparts.length === 2 && fparts[0] === baseResource;
|
|
148
|
+
return isDirectBaseChild ? baseFieldByLastSegment.get(fname) : undefined;
|
|
149
|
+
};
|
|
145
150
|
const canonicalToTypeName = (name) => {
|
|
146
151
|
if (!name)
|
|
147
152
|
return name;
|
|
@@ -178,11 +183,19 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
178
183
|
fields.forEach((field) => {
|
|
179
184
|
const fieldParts = field.name.split(".");
|
|
180
185
|
let fieldName = fieldParts[fieldParts.length - 1];
|
|
181
|
-
if (interfaceName.includes('address') && interfaceName.includes('0_2') && field.name.includes('line')) {
|
|
182
|
-
logger.log('[FIELD PROCESSING]', { interfaceName, 'field.name': field.name, fieldName, sliceName: field.sliceName });
|
|
183
|
-
}
|
|
184
186
|
const isRootInterface = parentInterfaceName === interfaceName;
|
|
185
187
|
const isNested = isRootInterface ? fieldParts.length > 1 : fieldParts.length >= 1;
|
|
188
|
+
// When generating a slice-specific interface, only include direct children
|
|
189
|
+
// (1 segment deep) and skip deeper nested paths (2+ segments) to avoid
|
|
190
|
+
// flattening nested structures into the slice interface.
|
|
191
|
+
if (isProcessingSlice && fieldParts.length > 1) {
|
|
192
|
+
debug('skipping deeply nested field in slice interface', {
|
|
193
|
+
fieldName: field.name,
|
|
194
|
+
parentInterfaceName,
|
|
195
|
+
depth: fieldParts.length
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
186
199
|
if (field.name.includes('bodySite.extension')) {
|
|
187
200
|
logger.log('[DEBUG ALL bodySite.extension fields]', {
|
|
188
201
|
fieldName: field.name,
|
|
@@ -226,7 +239,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
226
239
|
// Only apply nested-parent name filtering for deeper nesting (>= 3 segments like Account.coverage.extension).
|
|
227
240
|
// For profiles that extend a base type (e.g., IdentifierEfn extends Identifier), only include direct children
|
|
228
241
|
// (2 segments: Identifier.system) and skip nested paths (4+ segments: Identifier.assigner.identifier.system)
|
|
229
|
-
if (isNested && fieldParts.length
|
|
242
|
+
if (isNested && fieldParts.length >= 2) {
|
|
230
243
|
const baseTypeName = fieldParts[0]; // e.g., "Identifier" from "Identifier.assigner.identifier.system"
|
|
231
244
|
const isExtendingBaseType = baseResource && baseTypeName.toLowerCase() === baseResource.toLowerCase();
|
|
232
245
|
// Check if this field will create a nested interface (has children in fields)
|
|
@@ -234,6 +247,37 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
234
247
|
const shouldKeepForNested = field.mustSupport && hasChildren;
|
|
235
248
|
// Logical models extending Base should treat 2-segment paths as direct properties, not nested filtering
|
|
236
249
|
const isLogicalModel = baseResource === 'Base';
|
|
250
|
+
// For slice-specific interfaces (e.g., SiuAppointmentIdentifier for Appointment.identifier:doctolib),
|
|
251
|
+
// only include direct children of the slice, not deeply nested descendants like assigner.reference
|
|
252
|
+
// Detect whether this element is part of a slice by checking its ancestors
|
|
253
|
+
// and find the depth (number of segments) of the slice root. This ensures
|
|
254
|
+
// that descendant elements (e.g. assigner.type) are recognized as part of
|
|
255
|
+
// the slice and can be skipped from the slice-specific interface so that
|
|
256
|
+
// nested structures remain nested instead of being flattened.
|
|
257
|
+
let sliceRootDepth = -1;
|
|
258
|
+
for (let i = fieldParts.length; i >= 1; i--) {
|
|
259
|
+
const testPath = fieldParts.slice(0, i).join('.');
|
|
260
|
+
// Use the top-level fields list (newFields) when searching for the slice
|
|
261
|
+
// definition so nested invocations with relative names can still detect
|
|
262
|
+
// the original slice root.
|
|
263
|
+
const hasSliceAtDepth = newFields.some(f => f.name === testPath && f.sliceName);
|
|
264
|
+
if (hasSliceAtDepth) {
|
|
265
|
+
sliceRootDepth = i;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const isPartOfSlice = sliceRootDepth > -1 || !!field.sliceName;
|
|
270
|
+
// If this is part of a slice and we're more than 1 level deep from the slice root, skip it
|
|
271
|
+
// (We only want direct children of the slice root in the slice-specific interface.)
|
|
272
|
+
if (isPartOfSlice && sliceRootDepth > 0 && fieldParts.length > sliceRootDepth + 1) {
|
|
273
|
+
debug('skipping deeply nested field in slice', {
|
|
274
|
+
fieldName: field.name,
|
|
275
|
+
sliceRootDepth,
|
|
276
|
+
currentDepth: fieldParts.length,
|
|
277
|
+
parentInterfaceName
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
237
281
|
if (isExtendingBaseType && !isLogicalModel) {
|
|
238
282
|
// For profiles extending their base type, only include direct children (exactly 2 segments)
|
|
239
283
|
// UNLESS the field has mustSupport and children (will create nested interface)
|
|
@@ -286,7 +330,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
286
330
|
return;
|
|
287
331
|
}
|
|
288
332
|
if (interfaceName.includes('address') && interfaceName.includes('0_2') && field.name === 'Address.line') {
|
|
289
|
-
|
|
333
|
+
console.log('[AFTER SKIP CHECK]', { 'field.name': field.name, 'passed skip check': true });
|
|
290
334
|
}
|
|
291
335
|
if (fieldName === "value[x]")
|
|
292
336
|
fieldName = "value";
|
|
@@ -332,7 +376,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
332
376
|
if (isFhirType(field.type))
|
|
333
377
|
addTypeImport(field.type);
|
|
334
378
|
const conservative = baseFieldByLastSegment.size === 0;
|
|
335
|
-
|
|
379
|
+
// In nested contexts, direct children (single segment) should be treated as properties, not nested backbones
|
|
380
|
+
let treatAsNested = isNested && !(parentInterfaceName !== interfaceName && fieldParts.length === 1);
|
|
336
381
|
// Don't create nested interfaces for extension slices - they contribute to extension unions
|
|
337
382
|
const fieldPathParts = field.name.split('.');
|
|
338
383
|
const lastPart = fieldPathParts[fieldPathParts.length - 1];
|
|
@@ -378,7 +423,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
378
423
|
if (hasChildExtSlices && isPrimitiveType(mappedBase)) {
|
|
379
424
|
treatAsNested = false;
|
|
380
425
|
if (interfaceName.includes('address') && interfaceName.includes('0_2') && fieldName === 'line') {
|
|
381
|
-
|
|
426
|
+
console.log('[SET treatAsNested = false for line]');
|
|
382
427
|
}
|
|
383
428
|
}
|
|
384
429
|
debug('retain nested backbone due to profiled descendant', field.name);
|
|
@@ -472,8 +517,9 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
472
517
|
let baseFieldType = baseFieldByLastSegment.get(fieldName)?.type || field.type;
|
|
473
518
|
// If base type is missing/any/generic BackboneElement and this is a known backbone of the base resource,
|
|
474
519
|
// use the concrete backbone interface (e.g., DocumentReferenceContent)
|
|
520
|
+
// Use resourceType (the ultimate FHIR base like "Appointment") not baseResource (which might be a profile like "DoctolibAppointment")
|
|
475
521
|
if (!baseFieldType || baseFieldType === 'any' || baseFieldType === 'BackboneElement') {
|
|
476
|
-
const inferred = inferBackboneType(baseResource, fieldName);
|
|
522
|
+
const inferred = inferBackboneType(resourceType || baseResource, fieldName);
|
|
477
523
|
if (inferred)
|
|
478
524
|
baseFieldType = inferred;
|
|
479
525
|
}
|
|
@@ -492,10 +538,14 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
492
538
|
nestedFieldsCount: nestedFields.length
|
|
493
539
|
});
|
|
494
540
|
}
|
|
541
|
+
// Check if this nested interface is for a sliced element
|
|
542
|
+
// (there may be multiple fields at the same path with different sliceNames)
|
|
543
|
+
const isSlicedElement = fields.some(f => f.name === field.name && f.sliceName);
|
|
495
544
|
processFields(nestedFields.map((f) => ({
|
|
496
545
|
...f,
|
|
497
546
|
name: f.name.replace(`${field.name}.`, ""),
|
|
498
|
-
})), nestedInterfaceName, baseFieldType
|
|
547
|
+
})), nestedInterfaceName, baseFieldType, isSlicedElement // Pass true if ANY field at this path has a sliceName
|
|
548
|
+
);
|
|
499
549
|
let nestedDefined = interfaces.some(i => i.startsWith(`export interface ${nestedInterfaceName}`));
|
|
500
550
|
if (fieldName === 'answer' && field.mustSupport) {
|
|
501
551
|
logger.log('[DEBUG answer after processFields]', {
|
|
@@ -520,7 +570,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
520
570
|
const aliasTypes = [];
|
|
521
571
|
for (const sl of extensionSlices) {
|
|
522
572
|
for (const url of sl.profileUrls || []) {
|
|
523
|
-
const alias = sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice');
|
|
573
|
+
const alias = toPascalCase(sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice'));
|
|
524
574
|
// Generate minimal alias interface locally if not present elsewhere
|
|
525
575
|
if (!generatedAliasTypes.has(alias)) {
|
|
526
576
|
interfaces.unshift(`export interface ${alias} extends Extension { url: '${url}' }`);
|
|
@@ -576,7 +626,34 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
576
626
|
// Fall through to emit the field with updated cardinality
|
|
577
627
|
debug('emit base child with changed cardinality', field.name, { baseOptional: baseFld?.isOptional, profileOptional: field.isOptional });
|
|
578
628
|
}
|
|
629
|
+
// For nested interfaces that extend a base type (e.g., SiuAppointmentParticipant extends AppointmentParticipant),
|
|
630
|
+
// if we're only changing cardinality (not type), preserve the base field's type instead of downgrading to primitives
|
|
579
631
|
const baseFld = baseFieldByLastSegment.get(fieldName);
|
|
632
|
+
const isOnlyCardinalityChange = baseFld && !nestedDefined &&
|
|
633
|
+
(baseFld.isOptional !== field.isOptional || baseFld.isArray !== field.isArray) &&
|
|
634
|
+
field.type === baseFld.type && // Type hasn't changed
|
|
635
|
+
!field.isProfiled && !field.fixedValue; // No other constraints
|
|
636
|
+
// For nested interfaces extending FHIR base types, skip fields with no real constraints
|
|
637
|
+
// (FHIR base types like AppointmentParticipant have rich union types that we want to preserve)
|
|
638
|
+
const isNestedExtendingFhirBase = parentInterfaceName !== interfaceName && parentFieldType && isFhirType(parentFieldType);
|
|
639
|
+
// For root interfaces extending other profiles (e.g., SiuAppointment extends DoctolibAppointment),
|
|
640
|
+
// also skip fields with no constraints (they inherit from the profile chain)
|
|
641
|
+
const isRootExtendingProfile = parentInterfaceName === interfaceName && baseResource && baseResource !== interfaceName;
|
|
642
|
+
const hasChildren = fields.some((f) => f.name.startsWith(`${field.name}.`));
|
|
643
|
+
const hasNoConstraints = baseFld && !nestedDefined && !hasChildren &&
|
|
644
|
+
baseFld.isOptional === field.isOptional &&
|
|
645
|
+
baseFld.isArray === field.isArray &&
|
|
646
|
+
field.type === baseFld.type &&
|
|
647
|
+
!field.isProfiled && !field.fixedValue;
|
|
648
|
+
// Skip fields with NO constraints at all (they inherit perfectly from base)
|
|
649
|
+
// But DON'T skip fields that only change cardinality - we need to emit those with correct optionality
|
|
650
|
+
if ((isNestedExtendingFhirBase || isRootExtendingProfile) && hasNoConstraints && !isOnlyCardinalityChange) {
|
|
651
|
+
debug('skip field with no constraints - inherit from base', field.name);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
// For fields that only change cardinality, we'll emit them but use the base type
|
|
655
|
+
// (preserving union types, etc.) with the correct optionality marker
|
|
656
|
+
const useBaseTypeForCardinality = (isNestedExtendingFhirBase || isRootExtendingProfile) && isOnlyCardinalityChange;
|
|
580
657
|
// Check if this is a primitive field with child extension slices
|
|
581
658
|
const hasChildExtSlices = fields.some(f => f.name === `${field.name}.extension` && f.sliceName && (f.profileUrls || []).length > 0);
|
|
582
659
|
const mappedBaseType = mapTypeToTS((baseFld?.type || field.type || 'string'));
|
|
@@ -589,16 +666,38 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
589
666
|
// The nested interface is only for the sidecar _field element
|
|
590
667
|
// Also, for extension fields with slices, the union will be added separately
|
|
591
668
|
let resolvedType = (nestedDefined && !isPrimitiveWithExtSlices && !isExtensionWithSlices) ? nestedInterfaceName : resolveEffectiveType(field, baseFld);
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
669
|
+
// For cardinality-only changes, use the base field's type directly to preserve union types, etc.
|
|
670
|
+
if (useBaseTypeForCardinality && baseFld?.type) {
|
|
671
|
+
resolvedType = baseFld.type;
|
|
672
|
+
debug('using base type for cardinality-only change', field.name, { baseType: baseFld.type });
|
|
673
|
+
}
|
|
674
|
+
else if (!nestedDefined) {
|
|
675
|
+
// For direct children of the root resource, prefer explicit backbone inference
|
|
676
|
+
if (parentInterfaceName === interfaceName) {
|
|
677
|
+
const inferred = inferBackboneType(baseResource, fieldName);
|
|
678
|
+
if (!field.isProfiled && inferred) {
|
|
679
|
+
resolvedType = inferred;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
resolvedType = mapTypeToTS(resolvedType);
|
|
683
|
+
}
|
|
597
684
|
}
|
|
598
685
|
else {
|
|
686
|
+
// Nested contexts: do not infer resource-level backbones, just map primitives
|
|
599
687
|
resolvedType = mapTypeToTS(resolvedType);
|
|
600
688
|
}
|
|
601
689
|
}
|
|
690
|
+
// Ensure CodeableConcept.text remains string (avoid Resource.text Narrative leakage)
|
|
691
|
+
if (fieldName === 'text' && parentFieldType && sanitizeIdentifier(parentFieldType) === 'CodeableConcept') {
|
|
692
|
+
resolvedType = 'string';
|
|
693
|
+
}
|
|
694
|
+
// Special-case: Bundle.entry.resource should be FhirResource
|
|
695
|
+
if (fieldName === 'resource' && (baseResource === 'Bundle' || resourceType === 'Bundle')) {
|
|
696
|
+
resolvedType = 'FhirResource';
|
|
697
|
+
imports.add('FhirResource');
|
|
698
|
+
}
|
|
699
|
+
// Final primitive normalization for nested branch as well
|
|
700
|
+
resolvedType = mapTypeToTS(resolvedType);
|
|
602
701
|
// For profiled child elements (Attachment with CustomAttachment profile etc.), prefer the profiled type name directly.
|
|
603
702
|
if (field.isProfiled && field.type) {
|
|
604
703
|
resolvedType = canonicalToTypeName(field.type);
|
|
@@ -614,7 +713,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
614
713
|
resolvedType = sanitized;
|
|
615
714
|
}
|
|
616
715
|
}
|
|
617
|
-
|
|
716
|
+
// Only use base arrayness for direct children of the base resource; nested child fields must rely on their own cardinality
|
|
717
|
+
const baseArrFlag = getBaseFldForContext(fieldName, fieldParts, parentInterfaceName)?.isArray;
|
|
618
718
|
// If base arrayness is unknown (failed to fetch base metadata), default to array for known repeating fields
|
|
619
719
|
// like Account.coverage when emitting the root-level property.
|
|
620
720
|
const defaultArrayForKnownRepeatsNested = (baseArrFlag === undefined &&
|
|
@@ -776,14 +876,14 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
776
876
|
const mustSupportChanged = baseFld && (!baseFld.mustSupport && field.mustSupport);
|
|
777
877
|
const cardinalityChanged = baseFld && (baseFld.isOptional !== field.isOptional || baseFld.isArray !== field.isArray);
|
|
778
878
|
if (interfaceName.includes('address') && interfaceName.includes('0_2') && fieldName === 'line') {
|
|
779
|
-
|
|
879
|
+
console.log('[PRIMITIVE OVERRIDE CHECK]', { isPrimitive: isPrimitiveType(mapped), hasExtensionSlices, hasIdentifierSlices, mustSupportChanged, cardinalityChanged, willReturn: isPrimitiveType(mapped) && !hasExtensionSlices && !hasIdentifierSlices && !mustSupportChanged && !cardinalityChanged });
|
|
780
880
|
}
|
|
781
881
|
if (isPrimitiveType(mapped) && !hasExtensionSlices && !hasIdentifierSlices && !mustSupportChanged && !cardinalityChanged) {
|
|
782
882
|
debug('skip primitive override of base child', field.name, 'mappedType=', mapped);
|
|
783
883
|
return;
|
|
784
884
|
}
|
|
785
885
|
if (interfaceName.includes('address') && interfaceName.includes('0_2') && fieldName === 'line') {
|
|
786
|
-
|
|
886
|
+
console.log('[AFTER PRIMITIVE OVERRIDE CHECK - continuing]');
|
|
787
887
|
}
|
|
788
888
|
// Also check if cardinality changed (optional -> required) - if so, emit for documentation
|
|
789
889
|
if (!cardinalityChanged && !mustSupportChanged && !isPrimitiveType(mapped) && !hasIdentifierSlices) {
|
|
@@ -809,10 +909,11 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
809
909
|
}
|
|
810
910
|
}
|
|
811
911
|
// Heuristic: if base field exists and we're about to widen/narrow it improperly, skip emission.
|
|
812
|
-
const baseField =
|
|
912
|
+
const baseField = getBaseFldForContext(fieldName, fieldParts, parentInterfaceName);
|
|
813
913
|
const enumeratedSkip = new Set(['status', 'gender', 'class', 'category', 'concept', 'entry', 'clinicalStatus', 'verificationStatus']);
|
|
814
914
|
const mustSupportChanged = baseField && (!baseField.mustSupport && field.mustSupport);
|
|
815
|
-
|
|
915
|
+
const cardinalityChanged = baseField && (baseField.isOptional !== field.isOptional || baseField.isArray !== field.isArray);
|
|
916
|
+
if (parentInterfaceName === interfaceName && !field.fixedValue && !mustSupportChanged && !cardinalityChanged) {
|
|
816
917
|
if (enumeratedSkip.has(fieldName))
|
|
817
918
|
return;
|
|
818
919
|
if (baseResource === 'UsageContext' && fieldName === 'code' && field.type === 'string')
|
|
@@ -842,7 +943,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
842
943
|
// If new type is primitive (or code -> string) while base is non-primitive, skip to retain richer base definition.
|
|
843
944
|
const mapped = mapTypeToTS(field.type || 'string');
|
|
844
945
|
if (interfaceName.includes('address') && interfaceName.includes('0_2') && fieldName === 'line') {
|
|
845
|
-
|
|
946
|
+
console.log('[MAPPED TYPE CHECK]', { mapped, isPrimitive: isPrimitiveType(mapped), baseFieldType: baseField.type, baseFieldIsPrimitive: baseField.type ? isPrimitiveType(baseField.type) : 'no baseField.type', hasExtensionSlices, willReturn: isPrimitiveType(mapped) && baseField.type && !isPrimitiveType(baseField.type) && !hasExtensionSlices });
|
|
846
947
|
}
|
|
847
948
|
if (isPrimitiveType(mapped) && baseField.type && !isPrimitiveType(baseField.type) && !hasExtensionSlices) {
|
|
848
949
|
return; // rely on inherited definition
|
|
@@ -851,7 +952,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
851
952
|
// Additional guard: Sometimes differential loses specific complex type information and we parse as 'any' or 'string'.
|
|
852
953
|
// If base field exists and is an array but new appears singular primitive, skip overriding to keep array.
|
|
853
954
|
if (fieldName === 'line') {
|
|
854
|
-
|
|
955
|
+
console.log('[ARRAY CHECK 768]', {
|
|
855
956
|
baseFieldIsArray: baseField.isArray,
|
|
856
957
|
fieldIsArray: field.isArray,
|
|
857
958
|
fieldType: field.type,
|
|
@@ -866,7 +967,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
866
967
|
// Special cases: known enumerated code fields (status, gender, intent, class, category) -> avoid overriding unless fixed or mustSupport changed
|
|
867
968
|
const mustSupportChanged = baseField.mustSupport !== field.mustSupport && field.mustSupport === true;
|
|
868
969
|
if (fieldName === 'line') {
|
|
869
|
-
|
|
970
|
+
console.log('[ENUM CHECK 772]', {
|
|
870
971
|
fieldName,
|
|
871
972
|
fixedValue: field.fixedValue,
|
|
872
973
|
mustSupportChanged,
|
|
@@ -882,11 +983,11 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
882
983
|
return;
|
|
883
984
|
if (fieldName === 'beneficiary' && baseField.type === 'Reference')
|
|
884
985
|
return;
|
|
885
|
-
if (['concept', 'entry'].includes(fieldName) && !field.fixedValue)
|
|
986
|
+
if (['concept', 'entry', 'content'].includes(fieldName) && !field.fixedValue)
|
|
886
987
|
return;
|
|
887
988
|
// Additional guard: avoid overriding well-known enum/union fields with raw string
|
|
888
989
|
// Even if mustSupport changes, we should inherit the base enum type, not override with string
|
|
889
|
-
if (['gender', 'status', 'intent', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus'].includes(fieldName) && isPrimitiveType(mapped)) {
|
|
990
|
+
if (['gender', 'status', 'intent', 'lifecycleStatus', 'clinicalStatus', 'verificationStatus', 'content'].includes(fieldName) && isPrimitiveType(mapped)) {
|
|
890
991
|
return;
|
|
891
992
|
}
|
|
892
993
|
// Skip if identical to base (same type, array flag, optionality, AND mustSupport)
|
|
@@ -894,7 +995,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
894
995
|
const mappedType = mapTypeToTS(field.type || 'string');
|
|
895
996
|
const mustSupportSame = baseField.mustSupport === field.mustSupport;
|
|
896
997
|
if (fieldName === 'line') {
|
|
897
|
-
|
|
998
|
+
console.log('[IDENTICAL CHECK 796]', {
|
|
898
999
|
baseFieldType: baseField.type,
|
|
899
1000
|
fieldType: field.type,
|
|
900
1001
|
mappedType,
|
|
@@ -923,7 +1024,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
923
1024
|
}
|
|
924
1025
|
}
|
|
925
1026
|
}
|
|
926
|
-
const baseFld =
|
|
1027
|
+
const baseFld = getBaseFldForContext(fieldName, fieldParts, parentInterfaceName);
|
|
927
1028
|
let fieldType = resolveEffectiveType(field, baseFld);
|
|
928
1029
|
// Map raw FHIR primitive codes (e.g., dateTime) to TS primitives unless the field is explicitly profiled.
|
|
929
1030
|
if (!field.isProfiled) {
|
|
@@ -937,9 +1038,21 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
937
1038
|
fieldType = mapTypeToTS(fieldType);
|
|
938
1039
|
}
|
|
939
1040
|
else {
|
|
940
|
-
|
|
1041
|
+
// In nested contexts where parent is a known FHIR type, prefer known child types to avoid degrading to primitives
|
|
1042
|
+
if (parentFieldType && isFhirType(parentFieldType)) {
|
|
1043
|
+
const inferredChild = inferFhirChildType(parentFieldType, fieldName);
|
|
1044
|
+
fieldType = inferredChild ? inferredChild : mapTypeToTS(fieldType);
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
fieldType = mapTypeToTS(fieldType);
|
|
1048
|
+
}
|
|
941
1049
|
}
|
|
942
1050
|
}
|
|
1051
|
+
// Special-case: Bundle.entry.resource should be FhirResource, not Resource
|
|
1052
|
+
if (fieldName === 'resource' && (baseResource === 'Bundle' || resourceType === 'Bundle')) {
|
|
1053
|
+
fieldType = 'FhirResource';
|
|
1054
|
+
imports.add('FhirResource');
|
|
1055
|
+
}
|
|
943
1056
|
// Normalize canonical URLs to local type names
|
|
944
1057
|
if (!isPrimitiveType(fieldType) && !isFhirType(fieldType)) {
|
|
945
1058
|
fieldType = canonicalToTypeName(fieldType);
|
|
@@ -951,7 +1064,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
951
1064
|
const aliasTypes = [];
|
|
952
1065
|
for (const sl of samePathSlices) {
|
|
953
1066
|
for (const url of sl.profileUrls || []) {
|
|
954
|
-
const alias = sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice');
|
|
1067
|
+
const alias = toPascalCase(sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice'));
|
|
955
1068
|
if (!generatedAliasTypes.has(alias)) {
|
|
956
1069
|
interfaces.unshift(`export interface ${alias} extends Extension { url: '${url}' }`);
|
|
957
1070
|
generatedAliasTypes.add(alias);
|
|
@@ -1007,22 +1120,22 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1007
1120
|
// If this field has extension slices on its child '.extension', synthesize a nested interface
|
|
1008
1121
|
// BUT: if the current field IS "extension" itself, skip this - extension slices are handled elsewhere
|
|
1009
1122
|
if (interfaceName.includes('address') && interfaceName.includes('0_2') && fieldName === 'line') {
|
|
1010
|
-
|
|
1123
|
+
console.log('[BEFORE CHILD EXT CHECK]', { 'field.name': field.name, fieldName, 'reached this point': true });
|
|
1011
1124
|
}
|
|
1012
1125
|
const childExtensionSlices = fields.filter(f => f.name === `${field.name}.extension` && f.sliceName && (f.profileUrls || []).length > 0);
|
|
1013
1126
|
if (interfaceName.includes('address') && interfaceName.includes('0_2') && field.name === 'Address.line') {
|
|
1014
|
-
|
|
1127
|
+
console.log('[CHILD EXT CHECK - Address.line]', { 'field.name': field.name, childExtensionSlicesLength: childExtensionSlices.length, fieldName, filterCheck: fields.filter(f => f.name === `${field.name}.extension` && f.sliceName).map(f => ({ name: f.name, sliceName: f.sliceName, profileUrls: f.profileUrls })) });
|
|
1015
1128
|
}
|
|
1016
1129
|
if (childExtensionSlices.length > 0 && fieldName !== 'extension') {
|
|
1017
1130
|
if (interfaceName.includes('address') && fieldName === 'line') {
|
|
1018
|
-
|
|
1131
|
+
console.log('[CREATING SIDECAR]', { interfaceName, parentInterfaceName, fieldName, childExtensionSlicesLength: childExtensionSlices.length });
|
|
1019
1132
|
}
|
|
1020
1133
|
const nestedInterfaceName = `${parentInterfaceName}${capitalize(fieldName)}`;
|
|
1021
1134
|
// Build union members and alias types
|
|
1022
1135
|
const aliasTypes = [];
|
|
1023
1136
|
for (const sl of childExtensionSlices) {
|
|
1024
1137
|
for (const url of sl.profileUrls || []) {
|
|
1025
|
-
const alias = sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice');
|
|
1138
|
+
const alias = toPascalCase(sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice'));
|
|
1026
1139
|
if (!generatedAliasTypes.has(alias)) {
|
|
1027
1140
|
interfaces.unshift(`export interface ${alias} extends Extension { url: '${url}' }`);
|
|
1028
1141
|
generatedAliasTypes.add(alias);
|
|
@@ -1035,7 +1148,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1035
1148
|
const union = ['Extension', ...aliasTypes].join(' | ');
|
|
1036
1149
|
const extLine = `extension?: (${union})[];`;
|
|
1037
1150
|
// Determine parent type to extend
|
|
1038
|
-
const baseFld2 =
|
|
1151
|
+
const baseFld2 = getBaseFldForContext(fieldName, fieldParts, parentInterfaceName);
|
|
1039
1152
|
let parentT = resolveEffectiveType(field, baseFld2);
|
|
1040
1153
|
if (!field.isProfiled)
|
|
1041
1154
|
parentT = mapTypeToTS(parentT || 'string');
|
|
@@ -1068,7 +1181,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1068
1181
|
// For root interfaces, check baseResource; for nested, check parentFieldType
|
|
1069
1182
|
const effectiveParentType = parentFieldType || baseResource;
|
|
1070
1183
|
if (fieldName === 'line' || fieldName === 'prefix') {
|
|
1071
|
-
|
|
1184
|
+
console.log('[SIDECAR DEBUG]', {
|
|
1072
1185
|
profileName: interfaceName,
|
|
1073
1186
|
parentInterfaceName,
|
|
1074
1187
|
fieldName,
|
|
@@ -1089,7 +1202,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1089
1202
|
}
|
|
1090
1203
|
}
|
|
1091
1204
|
if (fieldName === 'line' || fieldName === 'prefix') {
|
|
1092
|
-
|
|
1205
|
+
console.log('[SIDECAR RESULT]', { parentInterfaceName, fieldName, finalSidecarIsArray: sidecarIsArray });
|
|
1093
1206
|
}
|
|
1094
1207
|
const sidecarType = sidecarIsArray ? `${elementIface}[]` : elementIface;
|
|
1095
1208
|
// Prefer optional sidecar by default to match base optionality and avoid duplicate required/optional declarations
|
|
@@ -1162,7 +1275,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1162
1275
|
// Preserve arrayness from base when known; if base metadata is missing and field name is a known repeating element (e.g., 'coverage' on Account),
|
|
1163
1276
|
// default to array to avoid incorrect narrowing.
|
|
1164
1277
|
// Skip adding [] if fieldType already contains array syntax (e.g., from identifier/extension union types)
|
|
1165
|
-
const baseArrKnown =
|
|
1278
|
+
const baseArrKnown = getBaseFldForContext(fieldName, fieldParts, parentInterfaceName)?.isArray;
|
|
1166
1279
|
const defaultArrayForKnownRepeats = (!isNested && fieldName === 'coverage' && baseResource === 'Account');
|
|
1167
1280
|
const alreadyHasArraySyntax = fieldType.endsWith('[]');
|
|
1168
1281
|
// For nested contexts (or when extending FHIR types), check if the parent type is a known FHIR type with known field cardinality
|
|
@@ -1174,6 +1287,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1174
1287
|
const isArrayOut = alreadyHasArraySyntax ? false : ((baseArrKnown !== undefined) ? baseArrKnown :
|
|
1175
1288
|
(fhirParentArrayKnown !== undefined) ? fhirParentArrayKnown :
|
|
1176
1289
|
(field.isArray || defaultArrayForKnownRepeats));
|
|
1290
|
+
// Final primitive normalization to ensure uri/url/etc are mapped to string
|
|
1291
|
+
fieldType = mapTypeToTS(fieldType);
|
|
1177
1292
|
const line = `${fieldName}${field.isOptional ? '?' : ''}: ${fieldType}${isArrayOut ? '[]' : ''};`;
|
|
1178
1293
|
debug(`About to check line for ${field.name}: fieldType=${fieldType}, line="${line}"`);
|
|
1179
1294
|
if (parentInterfaceName === interfaceName && writtenLines.has(line)) {
|
|
@@ -1308,7 +1423,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1308
1423
|
}
|
|
1309
1424
|
// lines preserved; accidental duplicates removed
|
|
1310
1425
|
function isFhirType(type) {
|
|
1311
|
-
return fhirInterfaceNames.includes(type);
|
|
1426
|
+
return type === 'FhirResource' || fhirInterfaceNames.includes(type);
|
|
1312
1427
|
}
|
|
1313
1428
|
// Returns true if a field on a known FHIR type is an array
|
|
1314
1429
|
function isFhirFieldArray(parentType, fieldName) {
|
|
@@ -1338,6 +1453,69 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1338
1453
|
return undefined; // unknown parent type
|
|
1339
1454
|
return arrayFields.has(fieldName);
|
|
1340
1455
|
}
|
|
1456
|
+
// Returns the known FHIR child type for a given parent FHIR type and field name, to preserve correct types in nested contexts
|
|
1457
|
+
function inferFhirChildType(parentType, fieldName) {
|
|
1458
|
+
// Prefer data-driven map if available
|
|
1459
|
+
const parentSan = sanitizeIdentifier(parentType);
|
|
1460
|
+
const fromMap = fhirChildTypeMap?.get(parentSan)?.get(fieldName)?.type;
|
|
1461
|
+
if (fromMap) {
|
|
1462
|
+
// Normalize primitives like uri/url/canonical to string
|
|
1463
|
+
return mapTypeToTS(fromMap);
|
|
1464
|
+
}
|
|
1465
|
+
// Fallback minimal hardcoded hints for common types
|
|
1466
|
+
const map = {
|
|
1467
|
+
Identifier: {
|
|
1468
|
+
type: 'CodeableConcept',
|
|
1469
|
+
period: 'Period',
|
|
1470
|
+
assigner: 'Reference',
|
|
1471
|
+
system: 'string',
|
|
1472
|
+
value: 'string',
|
|
1473
|
+
use: 'string',
|
|
1474
|
+
},
|
|
1475
|
+
Reference: {
|
|
1476
|
+
identifier: 'Identifier',
|
|
1477
|
+
type: 'string',
|
|
1478
|
+
reference: 'string',
|
|
1479
|
+
display: 'string',
|
|
1480
|
+
},
|
|
1481
|
+
CodeableConcept: {
|
|
1482
|
+
coding: 'Coding',
|
|
1483
|
+
text: 'string',
|
|
1484
|
+
},
|
|
1485
|
+
Coding: {
|
|
1486
|
+
system: 'string',
|
|
1487
|
+
version: 'string',
|
|
1488
|
+
code: 'string',
|
|
1489
|
+
display: 'string',
|
|
1490
|
+
userSelected: 'boolean',
|
|
1491
|
+
},
|
|
1492
|
+
Period: {
|
|
1493
|
+
start: 'string',
|
|
1494
|
+
end: 'string',
|
|
1495
|
+
},
|
|
1496
|
+
HumanName: {
|
|
1497
|
+
use: 'string',
|
|
1498
|
+
text: 'string',
|
|
1499
|
+
family: 'string',
|
|
1500
|
+
given: 'string',
|
|
1501
|
+
prefix: 'string',
|
|
1502
|
+
suffix: 'string',
|
|
1503
|
+
},
|
|
1504
|
+
Address: {
|
|
1505
|
+
use: 'string',
|
|
1506
|
+
type: 'string',
|
|
1507
|
+
text: 'string',
|
|
1508
|
+
city: 'string',
|
|
1509
|
+
district: 'string',
|
|
1510
|
+
state: 'string',
|
|
1511
|
+
postalCode: 'string',
|
|
1512
|
+
country: 'string',
|
|
1513
|
+
period: 'Period',
|
|
1514
|
+
line: 'string',
|
|
1515
|
+
},
|
|
1516
|
+
};
|
|
1517
|
+
return map[parentSan]?.[fieldName];
|
|
1518
|
+
}
|
|
1341
1519
|
if (baseResource && !isPrimitiveType(baseResource) && sanitizeIdentifier(baseResource) !== 'Base') {
|
|
1342
1520
|
const sanitizedBase = sanitizeIdentifier(baseResource);
|
|
1343
1521
|
// Skip custom German coverage base pseudo-types that we cannot resolve
|
|
@@ -1384,7 +1562,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1384
1562
|
const containerPath = f.name.replace(/\.extension$/, '');
|
|
1385
1563
|
const arr = byContainer.get(containerPath) || [];
|
|
1386
1564
|
for (const url of f.profileUrls) {
|
|
1387
|
-
const alias = sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice');
|
|
1565
|
+
const alias = toPascalCase(sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice'));
|
|
1388
1566
|
arr.push({ url, alias });
|
|
1389
1567
|
}
|
|
1390
1568
|
byContainer.set(containerPath, arr);
|
|
@@ -1641,7 +1819,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1641
1819
|
const aliases = [];
|
|
1642
1820
|
for (const sl of rootExtSlices) {
|
|
1643
1821
|
for (const url of sl.profileUrls || []) {
|
|
1644
|
-
const alias = sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice');
|
|
1822
|
+
const alias = toPascalCase(sanitizeIdentifier(url.split('/').pop() || 'ExtensionSlice'));
|
|
1645
1823
|
if (!generatedAliasTypes.has(alias)) {
|
|
1646
1824
|
interfaces.unshift(`export interface ${alias} extends Extension { url: '${url}' }`);
|
|
1647
1825
|
generatedAliasTypes.add(alias);
|
|
@@ -1711,7 +1889,13 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1711
1889
|
for (const typeName of allFhirTypes) {
|
|
1712
1890
|
if (existingProfiles.has(typeName)) {
|
|
1713
1891
|
logger.log('[DEBUG local type]', { typeName });
|
|
1714
|
-
|
|
1892
|
+
// Never move FhirResource to local imports; it must stay from fhir/r4
|
|
1893
|
+
if (typeName !== 'FhirResource') {
|
|
1894
|
+
localTypes.push(typeName);
|
|
1895
|
+
}
|
|
1896
|
+
else {
|
|
1897
|
+
remainingFhirTypes.push(typeName);
|
|
1898
|
+
}
|
|
1715
1899
|
}
|
|
1716
1900
|
else {
|
|
1717
1901
|
logger.log('[DEBUG fhir type]', { typeName });
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
import { logger } from "../logger";
|
|
5
4
|
// Cache directory for fetched FHIR resources
|
|
6
5
|
const CACHE_DIR = path.join(process.cwd(), ".cache", "fhir-resources");
|
|
6
|
+
// Local StructureDefinitions indexed by canonical URL for fast lookup
|
|
7
|
+
const localStructureDefinitions = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Register local StructureDefinitions to enable resolution before HTTP fetch
|
|
10
|
+
*/
|
|
11
|
+
export function registerLocalStructureDefinitions(structureDefinitions) {
|
|
12
|
+
localStructureDefinitions.clear();
|
|
13
|
+
for (const sd of structureDefinitions) {
|
|
14
|
+
if (sd.url) {
|
|
15
|
+
localStructureDefinitions.set(sd.url, sd);
|
|
16
|
+
console.debug(`[sdParser] Registered local SD: ${sd.url}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
console.log(`[sdParser] Registered ${localStructureDefinitions.size} local StructureDefinitions`);
|
|
20
|
+
}
|
|
7
21
|
function ensureCacheDir() {
|
|
8
22
|
if (!fs.existsSync(CACHE_DIR)) {
|
|
9
23
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
@@ -19,6 +33,11 @@ function urlToFilename(url) {
|
|
|
19
33
|
* Fetch a StructureDefinition from cache or network
|
|
20
34
|
*/
|
|
21
35
|
export async function fetchStructureDefinition(url, fallbackUrl) {
|
|
36
|
+
// First, check if we have this SD registered locally (from input directory)
|
|
37
|
+
if (localStructureDefinitions.has(url)) {
|
|
38
|
+
console.debug(`[fetchSD] Found local StructureDefinition for ${url}`);
|
|
39
|
+
return localStructureDefinitions.get(url);
|
|
40
|
+
}
|
|
22
41
|
const cacheFile = path.join(CACHE_DIR, urlToFilename(url));
|
|
23
42
|
// Try to load from cache first
|
|
24
43
|
if (fs.existsSync(cacheFile)) {
|
|
@@ -180,11 +199,11 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
180
199
|
const baseField = baseFields.find(f => f.name === element.path);
|
|
181
200
|
if (baseField) {
|
|
182
201
|
// Use base resource's array flag when profile doesn't override max
|
|
183
|
-
|
|
202
|
+
console.log(`[sdParser] Using base cardinality for ${element.path}: baseField.isArray=${baseField.isArray}, element.max=${element.max}`);
|
|
184
203
|
isArray = baseField.isArray;
|
|
185
204
|
}
|
|
186
205
|
else {
|
|
187
|
-
|
|
206
|
+
console.log(`[sdParser] No base field found for ${element.path}, using calculated isArray=${isArray}`);
|
|
188
207
|
}
|
|
189
208
|
}
|
|
190
209
|
const rawMax = element.max ?? '';
|
|
@@ -303,6 +322,15 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
303
322
|
max,
|
|
304
323
|
typeOptions: typeOptions.length ? typeOptions : undefined,
|
|
305
324
|
};
|
|
325
|
+
// Debug logging for mustSupport
|
|
326
|
+
if (process.env.DEBUG_FHIR_GEN === 'true' && (element.path?.includes('identifier') || element.path?.includes('status') || element.path?.includes('category'))) {
|
|
327
|
+
console.log(`[sdParser] Creating field for ${element.path}:`, {
|
|
328
|
+
elementMustSupport: element.mustSupport,
|
|
329
|
+
fieldMustSupport: field.mustSupport,
|
|
330
|
+
isOptional: field.isOptional,
|
|
331
|
+
min: element.min,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
306
334
|
return field;
|
|
307
335
|
});
|
|
308
336
|
return { newFields, oldFields: baseFields };
|
|
@@ -319,3 +347,34 @@ export async function fetchStructureDefinitions(fhirServerUrl) {
|
|
|
319
347
|
}
|
|
320
348
|
return resources;
|
|
321
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Build a child field map for core FHIR types using official StructureDefinitions.
|
|
352
|
+
* Returns a Map where key is parent type name (e.g., 'Identifier') and value is a Map of
|
|
353
|
+
* child field name -> { type, isArray } based on the base (snapshot) definition.
|
|
354
|
+
*/
|
|
355
|
+
export async function buildFhirChildTypeMap(typeNames, fhirVersion = 'R4') {
|
|
356
|
+
const result = new Map();
|
|
357
|
+
for (const typeName of typeNames) {
|
|
358
|
+
try {
|
|
359
|
+
const sdUrl = `http://hl7.org/fhir/${fhirVersion}/StructureDefinition/${typeName}`;
|
|
360
|
+
const sd = await fetchStructureDefinition(sdUrl, '');
|
|
361
|
+
if (!sd)
|
|
362
|
+
continue;
|
|
363
|
+
const { newFields } = await parseStructureDefinition(sd, '');
|
|
364
|
+
const map = new Map();
|
|
365
|
+
for (const f of newFields) {
|
|
366
|
+
// Interested only in direct children of the type (e.g., Identifier.system)
|
|
367
|
+
const parts = f.name.split('.');
|
|
368
|
+
if (parts.length === 2 && parts[0] === typeName) {
|
|
369
|
+
const child = parts[1];
|
|
370
|
+
map.set(child, { type: f.type || 'any', isArray: f.isArray });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
result.set(typeName, map);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// Best-effort; skip on failure
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return result;
|
|
380
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
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/main.js",
|