babelfhir-ts 1.6.0 → 1.6.2
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/src/generator/core/constants.js +2 -1
- package/out/src/generator/core/extensionSdReader.js +2 -0
- package/out/src/generator/core/utils.js +22 -9
- package/out/src/generator/emitters/class/classGenerator.js +14 -8
- package/out/src/generator/emitters/class/classGeneratorHelpers.js +12 -7
- package/out/src/generator/emitters/class/classHeuristics.js +4 -2
- package/out/src/generator/emitters/class/fieldPatternExtractor.js +17 -13
- package/out/src/generator/emitters/class/sliceElementDefaults.js +11 -5
- package/out/src/generator/emitters/class/sliceElementGenerator.js +3 -3
- package/out/src/generator/emitters/interface/backboneSliceTyping.js +21 -13
- package/out/src/generator/emitters/interface/importManager.js +1 -1
- package/out/src/generator/emitters/interface/interfaceGenerator.js +14 -9
- package/out/src/generator/emitters/interface/postProcessExtensions.js +24 -14
- package/out/src/generator/emitters/interface/processFlatField.js +9 -7
- package/out/src/generator/emitters/interface/processNestedField.js +5 -4
- package/out/src/generator/emitters/prefab/prefabTypeMapper.js +2 -0
- package/out/src/generator/emitters/validator/sliceDelegation.js +8 -6
- package/out/src/generator/emitters/validator/sliceExtensionValidation.js +3 -3
- package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +26 -14
- package/out/src/generator/emitters/validator/sliceValidatorUtils.js +3 -2
- package/out/src/generator/emitters/validator/validatorBindingBuilder.js +4 -3
- package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +5 -5
- package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +8 -6
- package/out/src/generator/emitters/validator/validatorGenerator.js +12 -7
- package/out/src/generator/emitters/validator/validatorTemplates.js +4 -1
- package/out/src/generator/emitters/zod/zodRefinementBuilder.js +9 -5
- package/out/src/generator/emitters/zod/zodSchemaGenerator.js +15 -16
- package/out/src/generator/fhir/corePackageResolver.js +2 -3
- package/out/src/generator/fhir/corePackageResolver.ts +2 -3
- package/out/src/generator/index.js +4 -2
- package/out/src/generator/parser/packageManager.js +4 -4
- package/out/src/generator/parser/packageParser.js +40 -64
- package/out/src/generator/parser/sdFetcher.js +5 -3
- package/out/src/generator/parser/sdParser.js +1 -1
- package/out/src/generator/parser/sliceAggregator.js +13 -10
- package/out/src/generator/parser/sliceAggregatorComposition.js +11 -2
- package/out/src/generator/parser/vsParser.js +3 -2
- package/out/src/generator/sdProcessorRequiredFields.js +3 -2
- package/package.json +2 -2
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* maintenance trivial.
|
|
7
7
|
*/
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
|
+
import { stripVersionFromCanonicalUrl } from './utils.js';
|
|
9
10
|
import { fileURLToPath } from 'url';
|
|
10
11
|
import path from 'path';
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
@@ -244,7 +245,7 @@ export const VALUESET_DEFAULTS = {
|
|
|
244
245
|
*/
|
|
245
246
|
export function resolveValueSetDefault(bindingUri) {
|
|
246
247
|
// Strip version suffix: "http://...ValueSet/foo|4.0.1" → "http://...ValueSet/foo"
|
|
247
|
-
const bare = bindingUri
|
|
248
|
+
const bare = stripVersionFromCanonicalUrl(bindingUri);
|
|
248
249
|
return VALUESET_DEFAULTS[bare];
|
|
249
250
|
}
|
|
250
251
|
export const FALLBACK_CODES = {
|
|
@@ -70,6 +70,8 @@ export function collectSubExtensionSlices(els) {
|
|
|
70
70
|
if (!match)
|
|
71
71
|
continue;
|
|
72
72
|
const sliceName = el.sliceName ?? match[1];
|
|
73
|
+
if (!sliceName)
|
|
74
|
+
continue;
|
|
73
75
|
const urlEl = byId.get(`Extension.extension:${sliceName}.url`);
|
|
74
76
|
const url = typeof urlEl?.fixedUri === 'string' ? urlEl.fixedUri : undefined;
|
|
75
77
|
if (!url)
|
|
@@ -28,6 +28,16 @@ export function stripSliceSuffix(path) {
|
|
|
28
28
|
const colonIndex = path.indexOf(':');
|
|
29
29
|
return colonIndex >= 0 ? path.substring(0, colonIndex) : path;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The slice name after the colon: `extension:Stadtteil` -> `Stadtteil`.
|
|
33
|
+
*
|
|
34
|
+
* Undefined when the segment carries no slice, which is the distinction callers
|
|
35
|
+
* act on — the counterpart to {@link stripSliceSuffix}, which returns the base.
|
|
36
|
+
*/
|
|
37
|
+
export function sliceSuffix(segment) {
|
|
38
|
+
const colonIndex = segment.indexOf(':');
|
|
39
|
+
return colonIndex >= 0 ? segment.substring(colonIndex + 1) : undefined;
|
|
40
|
+
}
|
|
31
41
|
/**
|
|
32
42
|
* The first dot-separated segment of a path: `code.coding.system` -> `code`.
|
|
33
43
|
*
|
|
@@ -123,7 +133,9 @@ export function getResourceTypes(options) {
|
|
|
123
133
|
const exportRegex = /export\s+(?:\*|{[^}]+})\s+from\s+['"]\.\/([\w-]+)\.js['"]/g;
|
|
124
134
|
let match;
|
|
125
135
|
while ((match = exportRegex.exec(indexContent)) !== null) {
|
|
126
|
-
|
|
136
|
+
const moduleName = match[1];
|
|
137
|
+
if (moduleName)
|
|
138
|
+
exportedTypes.add(moduleName);
|
|
127
139
|
}
|
|
128
140
|
}
|
|
129
141
|
for (const file of files) {
|
|
@@ -136,9 +148,9 @@ export function getResourceTypes(options) {
|
|
|
136
148
|
if (actualFile) {
|
|
137
149
|
const content = fs.readFileSync(actualFile, "utf-8");
|
|
138
150
|
// Check if it has resourceType property (base resource)
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
resourceTypes.set(profileName,
|
|
151
|
+
const declaredResourceType = content.match(/resourceType:\s*["'](\w+)["']/)?.[1];
|
|
152
|
+
if (declaredResourceType) {
|
|
153
|
+
resourceTypes.set(profileName, declaredResourceType);
|
|
142
154
|
continue;
|
|
143
155
|
}
|
|
144
156
|
// Check if it extends a FHIR resource (handles both `extends Foo` and `extends Omit<Foo, ...>`)
|
|
@@ -163,15 +175,16 @@ export function getBaseResourceType(profileName, outputDir) {
|
|
|
163
175
|
}
|
|
164
176
|
const content = fs.readFileSync(actualFile, "utf-8");
|
|
165
177
|
// Check for explicit resourceType property
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
168
|
-
return
|
|
178
|
+
const declaredResourceType = content.match(/resourceType:\s*["'](\w+)["']/)?.[1];
|
|
179
|
+
if (declaredResourceType) {
|
|
180
|
+
return declaredResourceType;
|
|
169
181
|
}
|
|
170
182
|
// Check for extends clause (handles both `extends Foo` and `extends Omit<Foo, ...>`)
|
|
171
183
|
const baseTypes = extractBaseTypes(content);
|
|
172
|
-
|
|
184
|
+
const [firstBase] = baseTypes;
|
|
185
|
+
if (firstBase) {
|
|
173
186
|
// Prefer a known resource type, fall back to the first candidate
|
|
174
|
-
return baseTypes.find(bt => ctx().resourceNames.includes(bt)) ??
|
|
187
|
+
return baseTypes.find(bt => ctx().resourceNames.includes(bt)) ?? firstBase;
|
|
175
188
|
}
|
|
176
189
|
return profileName; // fallback
|
|
177
190
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ctx } from '../../fhir/versionContext.js';
|
|
2
|
+
import { stripVersionFromCanonicalUrl } from '../../core/utils.js';
|
|
2
3
|
import { TS, } from '../../core/constants.js';
|
|
3
4
|
import { primitivePlaceholder, generateCodeDefault, makeCodeableConcept, chooseChoiceVariant, applyRequiredFieldHeuristic, deriveIdentifierConfig, } from './classHeuristics.js';
|
|
4
5
|
import { generateSliceElement, buildNestedRequirementsObject } from './sliceElementGenerator.js';
|
|
@@ -187,7 +188,11 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
187
188
|
.filter((s) => typeof s === 'string' && s.length > 0);
|
|
188
189
|
const uniqueSystems = Array.from(new Set(systems));
|
|
189
190
|
const singleSystem = uniqueSystems.length === 1 ? JSON.stringify(uniqueSystems[0]) : undefined;
|
|
191
|
+
// bindingCodes is non-empty per the guard above, but indexing does not say
|
|
192
|
+
// so; without a code there is no skeleton value to emit.
|
|
190
193
|
const primary = rf.bindingCodes[0];
|
|
194
|
+
if (!primary)
|
|
195
|
+
continue;
|
|
191
196
|
if (rf.type === 'CodeableConcept') {
|
|
192
197
|
const expr = singleSystem
|
|
193
198
|
? `skeletonCodeableConcept(${singleSystem}, ${codesArray})`
|
|
@@ -349,8 +354,8 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
349
354
|
}
|
|
350
355
|
}
|
|
351
356
|
// Check profile URLs for known coding profiles
|
|
352
|
-
|
|
353
|
-
|
|
357
|
+
const profileUrl = slice.profileUrls?.[0];
|
|
358
|
+
if (profileUrl) {
|
|
354
359
|
const profileUrlLower = profileUrl.toLowerCase();
|
|
355
360
|
// ISiK SNOMED-CT coding profile
|
|
356
361
|
if (profileUrlLower.includes('snomedctcoding') || profileUrlLower.includes('snomed')) {
|
|
@@ -376,7 +381,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
376
381
|
// code from a different context and should be deferred to runtime resolution.
|
|
377
382
|
const sliceHasOwnBinding = !!(slice.binding?.uri && slice.binding.uri.length > 0);
|
|
378
383
|
const sliceBindingMatchesParent = !rf.bindingUri ||
|
|
379
|
-
(sliceHasOwnBinding && slice.binding.uri
|
|
384
|
+
(sliceHasOwnBinding && stripVersionFromCanonicalUrl(slice.binding.uri) === stripVersionFromCanonicalUrl(rf.bindingUri));
|
|
380
385
|
if (sliceBindingMatchesParent && slice.binding?.sampleCode) {
|
|
381
386
|
if (slice.binding.sampleCode.system) {
|
|
382
387
|
system = slice.binding.sampleCode.system;
|
|
@@ -384,7 +389,7 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
384
389
|
code = JSON.stringify(slice.binding.sampleCode.code);
|
|
385
390
|
codeResolved = true;
|
|
386
391
|
}
|
|
387
|
-
else if (sliceBindingMatchesParent && slice.binding?.codes
|
|
392
|
+
else if (sliceBindingMatchesParent && slice.binding?.codes?.[0]) {
|
|
388
393
|
// Fallback to first code from the binding's codes array
|
|
389
394
|
const firstCode = slice.binding.codes[0];
|
|
390
395
|
if (firstCode.system) {
|
|
@@ -458,8 +463,8 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
458
463
|
// Handle CodeableConcept with pattern constraint - use the pattern instead of skeleton
|
|
459
464
|
if (rf.type === 'CodeableConcept' && rf.patternConstraint && typeof rf.patternConstraint === 'object') {
|
|
460
465
|
const pc = rf.patternConstraint;
|
|
461
|
-
|
|
462
|
-
|
|
466
|
+
const firstCoding = Array.isArray(pc.coding) ? pc.coding[0] : undefined;
|
|
467
|
+
if (firstCoding) {
|
|
463
468
|
const systemStr = JSON.stringify(firstCoding.system || TS.NULL_FLAVOR);
|
|
464
469
|
const codeStr = JSON.stringify(firstCoding.code || 'UNK');
|
|
465
470
|
const displayStr = firstCoding.display ? `, ${JSON.stringify(firstCoding.display)}` : '';
|
|
@@ -522,8 +527,9 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
522
527
|
}
|
|
523
528
|
// Complex type skeletons (also check baseType for profiled types like CodeableConcept-uv-ips)
|
|
524
529
|
const skeletonType = COMPLEX_SKELETON_MAP[rf.type] ? rf.type : (rf.baseType && COMPLEX_SKELETON_MAP[rf.baseType] ? rf.baseType : undefined);
|
|
525
|
-
|
|
526
|
-
|
|
530
|
+
const skeletonExpr = skeletonType ? COMPLEX_SKELETON_MAP[skeletonType] : undefined;
|
|
531
|
+
if (skeletonType && skeletonExpr) {
|
|
532
|
+
const expr = skeletonExpr;
|
|
527
533
|
const initExpr = buildValueExpression(rf, expr);
|
|
528
534
|
requiredInits.push(`${propKey(rf.name)}: ${initExpr}`);
|
|
529
535
|
continue;
|
|
@@ -61,15 +61,14 @@ export function buildValueExpression(rf, expr, opts) {
|
|
|
61
61
|
export function buildCrossReferencedEntryArray(sliceElements) {
|
|
62
62
|
const compositionIdx = [];
|
|
63
63
|
const resourceEntries = [];
|
|
64
|
-
for (
|
|
65
|
-
const expr = sliceElements[i];
|
|
64
|
+
for (const [i, expr] of sliceElements.entries()) {
|
|
66
65
|
if (/resourceType:\s*'Composition'/.test(expr)) {
|
|
67
66
|
compositionIdx.push(i);
|
|
68
67
|
}
|
|
69
68
|
else {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
resourceEntries.push({ idx: i, type
|
|
69
|
+
const type = expr.match(/resourceType:\s*'(\w+)'/)?.[1];
|
|
70
|
+
if (type)
|
|
71
|
+
resourceEntries.push({ idx: i, type });
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
74
|
if (compositionIdx.length === 0 || resourceEntries.length === 0)
|
|
@@ -79,10 +78,16 @@ export function buildCrossReferencedEntryArray(sliceElements) {
|
|
|
79
78
|
for (const { idx, type } of resourceEntries) {
|
|
80
79
|
const varName = `_${type.charAt(0).toLowerCase() + type.slice(1)}Url`;
|
|
81
80
|
decls.push(`const ${varName} = 'urn:uuid:' + crypto.randomUUID();`);
|
|
82
|
-
|
|
81
|
+
const entry = rewritten[idx];
|
|
82
|
+
if (entry !== undefined) {
|
|
83
|
+
rewritten[idx] = entry.replace(/fullUrl:\s*'urn:uuid:'\s*\+\s*crypto\.randomUUID\(\)/, `fullUrl: ${varName}`);
|
|
84
|
+
}
|
|
83
85
|
for (const ci of compositionIdx) {
|
|
84
86
|
const refPattern = new RegExp(`reference:\\s*'${type}/\\s*'\\s*\\+\\s*randomId\\(\\)`);
|
|
85
|
-
|
|
87
|
+
const composition = rewritten[ci];
|
|
88
|
+
if (composition !== undefined) {
|
|
89
|
+
rewritten[ci] = composition.replace(refPattern, `reference: ${varName}`);
|
|
90
|
+
}
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
return `(() => { ${decls.join(' ')} return [${rewritten.join(', ')}]; })()`;
|
|
@@ -91,6 +91,8 @@ export function chooseChoiceVariant(rf) {
|
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
93
93
|
let chosen = options[0];
|
|
94
|
+
if (chosen === undefined)
|
|
95
|
+
return null;
|
|
94
96
|
if (lowerBase === 'value' && preferCodeableConcept) {
|
|
95
97
|
chosen = 'CodeableConcept';
|
|
96
98
|
}
|
|
@@ -210,8 +212,8 @@ export function applyRequiredFieldHeuristic(rf, ctx) {
|
|
|
210
212
|
return { expr: makeCoding(system, picked.code), isObjectLiteral: true };
|
|
211
213
|
}
|
|
212
214
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
const [first] = rf.bindingCodes;
|
|
216
|
+
if (isCodeableConcept && first) {
|
|
215
217
|
const system = first.system || TS.SNOMED;
|
|
216
218
|
return { expr: `skeletonCodeableConcept('${system}', ['${first.code}'])` };
|
|
217
219
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fetchStructureDefinition } from '../../parser/sdParser.js';
|
|
2
|
+
import { stripSliceSuffix, sliceSuffix } from '../../core/utils.js';
|
|
2
3
|
export async function extractFieldPatternsAndMetadata(patternSources, baseResource, sd, requiredFields, candidateRequiredFields, nestedRequirementsMap, resolvedProfilePatterns) {
|
|
3
4
|
const patternMap = new Map();
|
|
4
5
|
for (const src of patternSources) {
|
|
@@ -7,7 +8,7 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
7
8
|
continue;
|
|
8
9
|
// Extract the field path parts
|
|
9
10
|
const parts = f.name.split('.');
|
|
10
|
-
const lastPart = (parts.pop()
|
|
11
|
+
const lastPart = stripSliceSuffix(parts.pop() ?? f.name); // base field name without slice
|
|
11
12
|
// For patterns on '.coding' elements, associate with the parent CodeableConcept field
|
|
12
13
|
// e.g., for "Coverage.type.coding:VersicherungsArtDeBasis", use "type" as key
|
|
13
14
|
let key = lastPart;
|
|
@@ -15,7 +16,7 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
15
16
|
// This is a coding inside a CodeableConcept - use the parent field name
|
|
16
17
|
const parentPart = parts.pop();
|
|
17
18
|
if (parentPart) {
|
|
18
|
-
key = parentPart
|
|
19
|
+
key = stripSliceSuffix(parentPart);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
// For nested fields (children of children, e.g., identifier.type vs top-level type),
|
|
@@ -28,8 +29,9 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
28
29
|
idParts.shift(); // remove resource type prefix
|
|
29
30
|
const nestedParts = [];
|
|
30
31
|
for (const part of idParts) {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const partSliceName = sliceSuffix(part);
|
|
33
|
+
if (partSliceName !== undefined) {
|
|
34
|
+
nestedParts.push(partSliceName); // use slice name
|
|
33
35
|
}
|
|
34
36
|
else if (!part.includes('[x]')) {
|
|
35
37
|
nestedParts.push(part);
|
|
@@ -204,9 +206,10 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
204
206
|
// e.g., "value[x]:valueQuantity.unit" -> "valueQuantity.unit"
|
|
205
207
|
const nestedParts = [];
|
|
206
208
|
for (const part of idParts) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
// Slice name, e.g. "value[x]:valueQuantity" -> "valueQuantity"
|
|
210
|
+
const partSliceName = sliceSuffix(part);
|
|
211
|
+
if (partSliceName !== undefined) {
|
|
212
|
+
nestedParts.push(partSliceName);
|
|
210
213
|
}
|
|
211
214
|
else if (!part.includes('[x]')) {
|
|
212
215
|
nestedParts.push(part);
|
|
@@ -235,8 +238,9 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
235
238
|
// Build nested key, extracting slice names where present
|
|
236
239
|
const nestedParts = [];
|
|
237
240
|
for (const part of idParts) {
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
const partSliceName = sliceSuffix(part);
|
|
242
|
+
if (partSliceName !== undefined) {
|
|
243
|
+
nestedParts.push(partSliceName);
|
|
240
244
|
}
|
|
241
245
|
else if (!part.includes('[x]')) {
|
|
242
246
|
nestedParts.push(part);
|
|
@@ -252,7 +256,7 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
252
256
|
}
|
|
253
257
|
}
|
|
254
258
|
// Otherwise, if this is a slice with binding codes, synthesize a pattern
|
|
255
|
-
else if (!patternMap.has(key) && f.sliceName && f.binding
|
|
259
|
+
else if (!patternMap.has(key) && f.sliceName && f.binding?.codes?.[0]) {
|
|
256
260
|
// Create a CodeableConcept pattern from the first binding code
|
|
257
261
|
const firstCode = f.binding.codes[0];
|
|
258
262
|
const synthesizedPattern = {
|
|
@@ -363,9 +367,9 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
363
367
|
const [basePart, sliceName] = p.split(':', 2);
|
|
364
368
|
// For choice types (e.g., value[x]:valueQuantity), use the slice name
|
|
365
369
|
// as the actual JS property name
|
|
366
|
-
if (basePart
|
|
370
|
+
if (basePart?.includes('[x]') && sliceName)
|
|
367
371
|
return sliceName;
|
|
368
|
-
return basePart; // strip slice qualifier for non-choice types
|
|
372
|
+
return basePart ?? p; // strip slice qualifier for non-choice types
|
|
369
373
|
}
|
|
370
374
|
return p;
|
|
371
375
|
});
|
|
@@ -413,7 +417,7 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
413
417
|
const parts = elemId.split('.');
|
|
414
418
|
if (parts.length !== 2)
|
|
415
419
|
continue;
|
|
416
|
-
const fieldName = (parts[1] || '')
|
|
420
|
+
const fieldName = stripSliceSuffix(parts[1] || '');
|
|
417
421
|
if (fieldName && !seenFieldNames.has(fieldName)) {
|
|
418
422
|
seenFieldNames.add(fieldName);
|
|
419
423
|
fieldOrder.push(fieldName);
|
|
@@ -399,11 +399,17 @@ export function getTypePlaceholder(fhirType) {
|
|
|
399
399
|
return '"https://babelfhir.dev"';
|
|
400
400
|
case 'code':
|
|
401
401
|
return '"code"';
|
|
402
|
+
// Dates are emitted as a call, not a value. This function returns code that
|
|
403
|
+
// lands in a generated class, so interpolating new Date() here froze the
|
|
404
|
+
// generation-time wall clock into the source: every regeneration produced a
|
|
405
|
+
// different file, and every consumer got the same instant forever. The
|
|
406
|
+
// sibling getDefaultValueForType already defers to randomDate() through its
|
|
407
|
+
// __DATE_EXPR__ placeholder; this now agrees with it.
|
|
402
408
|
case 'dateTime':
|
|
403
409
|
case 'instant':
|
|
404
|
-
return '
|
|
410
|
+
return 'randomDate()';
|
|
405
411
|
case 'date':
|
|
406
|
-
return '
|
|
412
|
+
return 'randomDate().slice(0, 10)';
|
|
407
413
|
case 'time':
|
|
408
414
|
return '"12:00:00"';
|
|
409
415
|
case 'Reference':
|
|
@@ -423,7 +429,7 @@ export function getTypePlaceholder(fhirType) {
|
|
|
423
429
|
case 'ContactPoint':
|
|
424
430
|
return '{ system: "phone", value: "555-1234" }';
|
|
425
431
|
case 'Period':
|
|
426
|
-
return '{ start:
|
|
432
|
+
return '{ start: randomDate() }';
|
|
427
433
|
case 'Attachment':
|
|
428
434
|
return '{ contentType: "text/plain" }';
|
|
429
435
|
case 'Narrative':
|
|
@@ -447,9 +453,9 @@ export function getTypePlaceholder(fhirType) {
|
|
|
447
453
|
case 'Annotation':
|
|
448
454
|
return '{ text: "Note" }';
|
|
449
455
|
case 'Signature':
|
|
450
|
-
return `{ type: [{ system: "urn:iso-astm:E1762-95:2013", code: "1.2.840.10065.1.12.1.1" }], when:
|
|
456
|
+
return `{ type: [{ system: "urn:iso-astm:E1762-95:2013", code: "1.2.840.10065.1.12.1.1" }], when: randomDate(), who: { reference: "Practitioner/example" } }`;
|
|
451
457
|
case 'Meta':
|
|
452
|
-
return '{ lastUpdated:
|
|
458
|
+
return '{ lastUpdated: randomDate() }';
|
|
453
459
|
case 'ContactDetail':
|
|
454
460
|
return '{ name: "Contact" }';
|
|
455
461
|
case 'UsageContext':
|
|
@@ -102,8 +102,8 @@ export function generateSliceElement(slice, elementType) {
|
|
|
102
102
|
// Detect already-resolved choice type variants
|
|
103
103
|
if (!wasResolvedFromChoiceNotation) {
|
|
104
104
|
const choiceMatch = childPath.match(/^([a-z]+)(Boolean|Integer|Decimal|DateTime|Date|Time|Instant|String|Uri|Url|Canonical|Base64Binary|Code|Oid|Id|Uuid|Markdown|UnsignedInt|PositiveInt|Coding|CodeableConcept|Quantity|Range|Period|Ratio|SampledData|Attachment|Duration|Distance|Count|Money|Age|Annotation|Signature|HumanName|Address|ContactPoint|Timing|Reference|Identifier|Meta|Dosage|Narrative|Expression|TriggerDefinition|DataRequirement|ParameterDefinition|RelatedArtifact|ContactDetail|UsageContext)$/);
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
const baseName = choiceMatch?.[1];
|
|
106
|
+
if (baseName) {
|
|
107
107
|
if (resolvedChoiceBases.has(baseName)) {
|
|
108
108
|
continue;
|
|
109
109
|
}
|
|
@@ -169,7 +169,7 @@ export function generateSliceElement(slice, elementType) {
|
|
|
169
169
|
else if (profileUrl) {
|
|
170
170
|
const profileName = profileUrl.split('/').pop() || '';
|
|
171
171
|
if (profileName.includes('-')) {
|
|
172
|
-
resourceType = capitalise(profileName.split('-')[0]);
|
|
172
|
+
resourceType = capitalise(profileName.split('-')[0] ?? profileName);
|
|
173
173
|
}
|
|
174
174
|
else {
|
|
175
175
|
resourceType = capitalise(profileName);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* }
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier } from '../../core/utils.js';
|
|
21
|
+
import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier, firstSegment } from '../../core/utils.js';
|
|
22
22
|
import { getRules } from '../../fhir/versionContext.js';
|
|
23
23
|
import { logger } from '../../../logger.js';
|
|
24
24
|
const log = logger.withTag('backbone-slices');
|
|
@@ -35,7 +35,11 @@ export function generateBackboneSliceTypes(ctx) {
|
|
|
35
35
|
return;
|
|
36
36
|
for (const group of sliceGroups) {
|
|
37
37
|
const { parentField, sliceFields, childFields, slicingRules } = group;
|
|
38
|
-
const fieldName = parentField.name.split('.').
|
|
38
|
+
const fieldName = parentField.name.split('.').at(-1);
|
|
39
|
+
// A grouped parent always has a dotted path, so this is unreachable; skipping
|
|
40
|
+
// is right if it ever is not, and it replaces a non-null assertion.
|
|
41
|
+
if (!fieldName)
|
|
42
|
+
continue;
|
|
39
43
|
// Determine the base backbone type for this field
|
|
40
44
|
let backboneType = inferBackboneType(baseResource, fieldName);
|
|
41
45
|
if (!backboneType) {
|
|
@@ -100,8 +104,9 @@ export function generateBackboneSliceTypes(ctx) {
|
|
|
100
104
|
if (isOpen) {
|
|
101
105
|
unionMembers.push(backboneType);
|
|
102
106
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
107
|
+
const [soleMember] = unionMembers;
|
|
108
|
+
const unionType = unionMembers.length === 1 && soleMember
|
|
109
|
+
? `${soleMember}[]`
|
|
105
110
|
: `(${unionMembers.join(' | ')})[]`;
|
|
106
111
|
// Find and update the field in the root interface
|
|
107
112
|
updateRootInterfaceField(interfaces, interfaceName, fieldName, parentField.isOptional, unionType, baseResource);
|
|
@@ -181,7 +186,7 @@ function isEligibleBackbonePath(baseType, path, baseResource) {
|
|
|
181
186
|
const parts = path.split('.');
|
|
182
187
|
if (parts.length !== 2)
|
|
183
188
|
return false;
|
|
184
|
-
if (baseResource &&
|
|
189
|
+
if (baseResource && firstSegment(path) !== baseResource)
|
|
185
190
|
return false;
|
|
186
191
|
return true;
|
|
187
192
|
}
|
|
@@ -224,9 +229,10 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
|
|
|
224
229
|
const childId = child.elementId || child.name;
|
|
225
230
|
const relativePath = childId.substring(sliceElementId.length + 1);
|
|
226
231
|
const match = choiceSlicePattern.exec(relativePath);
|
|
227
|
-
|
|
232
|
+
const choiceParent = match?.[1]; // e.g. "value[x]"
|
|
233
|
+
if (choiceParent) {
|
|
228
234
|
choiceTypeSlices.push(child);
|
|
229
|
-
choiceTypeParents.add(
|
|
235
|
+
choiceTypeParents.add(choiceParent);
|
|
230
236
|
}
|
|
231
237
|
}
|
|
232
238
|
// Collect deeply nested fixed values for object literal synthesis.
|
|
@@ -240,7 +246,9 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
|
|
|
240
246
|
if (relativePath.includes('.')) {
|
|
241
247
|
if (child.fixedValue !== undefined) {
|
|
242
248
|
const segments = relativePath.split('.');
|
|
243
|
-
|
|
249
|
+
// firstSegment is segments[0] without the possibly-undefined read; segments
|
|
250
|
+
// is still needed for the tail below.
|
|
251
|
+
const rawPropName = firstSegment(relativePath);
|
|
244
252
|
const propName = rawPropName.replace(/\[x\]$/, '');
|
|
245
253
|
// Skip nested values under:
|
|
246
254
|
// - choice-type elements ([x]) — handled by direct child or sub-slices
|
|
@@ -425,8 +433,8 @@ function updateRootInterfaceField(interfaces, interfaceName, fieldName, isOption
|
|
|
425
433
|
const newFieldLine = ` ${fieldName}${optMark}: ${unionType};`;
|
|
426
434
|
// Try to find an existing interface declaration
|
|
427
435
|
const rootIdx = interfaces.findIndex(i => i.startsWith(`export interface ${interfaceName} `) || i.startsWith(`export interface ${interfaceName}<`));
|
|
428
|
-
|
|
429
|
-
|
|
436
|
+
const iface = rootIdx >= 0 ? interfaces[rootIdx] : undefined;
|
|
437
|
+
if (iface !== undefined) {
|
|
430
438
|
const fieldPattern = new RegExp(`^(\\s+)${fieldName}[?]?:\\s*.+;`, 'm');
|
|
431
439
|
if (fieldPattern.test(iface)) {
|
|
432
440
|
// Replace existing field line
|
|
@@ -443,10 +451,10 @@ function updateRootInterfaceField(interfaces, interfaceName, fieldName, isOption
|
|
|
443
451
|
}
|
|
444
452
|
// Try to find a type alias (e.g., "export type BloodPressure = Observation;")
|
|
445
453
|
const aliasIdx = interfaces.findIndex(i => i.startsWith(`export type ${interfaceName} =`));
|
|
446
|
-
|
|
454
|
+
const alias = aliasIdx >= 0 ? interfaces[aliasIdx] : undefined;
|
|
455
|
+
if (alias !== undefined) {
|
|
447
456
|
// Convert type alias to interface with the field
|
|
448
|
-
const
|
|
449
|
-
const baseType = aliasMatch ? aliasMatch[1] : baseResource || '';
|
|
457
|
+
const baseType = alias.match(/export type \S+ = (\S+);/)?.[1] ?? baseResource ?? '';
|
|
450
458
|
interfaces[aliasIdx] = `export interface ${interfaceName} extends ${baseType} {\n${newFieldLine}\n}`;
|
|
451
459
|
}
|
|
452
460
|
}
|
|
@@ -263,7 +263,7 @@ export class ImportManager {
|
|
|
263
263
|
const allFhirTypes = [];
|
|
264
264
|
let match;
|
|
265
265
|
while ((match = fhirImportRegex.exec(output)) !== null) {
|
|
266
|
-
const types = match[1].split(',').map(t => t.trim()).filter(Boolean);
|
|
266
|
+
const types = (match[1] ?? '').split(',').map(t => t.trim()).filter(Boolean);
|
|
267
267
|
allFhirTypes.push(...types);
|
|
268
268
|
}
|
|
269
269
|
if (allFhirTypes.length === 0) {
|
|
@@ -61,7 +61,9 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
61
61
|
const baseFieldAnyArray = new Map();
|
|
62
62
|
for (const bf of baseFields) {
|
|
63
63
|
const parts = bf.name.split('.');
|
|
64
|
-
const seg = parts
|
|
64
|
+
const seg = parts.at(-1);
|
|
65
|
+
if (!seg)
|
|
66
|
+
continue;
|
|
65
67
|
if (!baseFieldByLastSegment.has(seg))
|
|
66
68
|
baseFieldByLastSegment.set(seg, bf);
|
|
67
69
|
// Track the widest array flag across direct children only (not nested duplicates)
|
|
@@ -83,10 +85,11 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
83
85
|
f.sliceName &&
|
|
84
86
|
f.binding?.uri);
|
|
85
87
|
// Use the first slice binding if available (slices typically have more specific constraints)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
const firstSlice = slicesForThisField[0];
|
|
89
|
+
if (firstSlice?.binding) {
|
|
90
|
+
binding = firstSlice.binding;
|
|
88
91
|
isSlicedField = true;
|
|
89
|
-
debug(`Using binding from slice ${
|
|
92
|
+
debug(`Using binding from slice ${firstSlice.sliceName} for ${field.name}`);
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
if (!binding) {
|
|
@@ -196,6 +199,8 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
196
199
|
// This must run after all interfaces + extension unions are assembled.
|
|
197
200
|
for (let i = 0; i < interfaces.length; i++) {
|
|
198
201
|
const iface = interfaces[i];
|
|
202
|
+
if (!iface)
|
|
203
|
+
continue;
|
|
199
204
|
// Match "export interface Foo extends Bar {" or "export interface Foo extends Omit<Bar, ...> {"
|
|
200
205
|
const extendsMatch = iface.match(/^export interface (\S+) extends (Omit<)?(\S+?)([,>])? \{/);
|
|
201
206
|
if (!extendsMatch)
|
|
@@ -203,16 +208,16 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
203
208
|
const alreadyOmit = !!extendsMatch[2];
|
|
204
209
|
const baseType = extendsMatch[3];
|
|
205
210
|
// Only wrap FHIR base types (not local profile interfaces)
|
|
206
|
-
if (!isFhirType(baseType))
|
|
211
|
+
if (!baseType || !isFhirType(baseType))
|
|
207
212
|
continue;
|
|
208
213
|
// Extract all declared property names from the interface body
|
|
209
214
|
const bodyStart = iface.indexOf('{');
|
|
210
215
|
const body = iface.slice(bodyStart + 1);
|
|
211
216
|
const propNames = [];
|
|
212
217
|
for (const line of body.split('\n')) {
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
215
|
-
propNames.push(
|
|
218
|
+
const propName = line.match(/^\s+(\w+)[?:]/)?.[1];
|
|
219
|
+
if (propName && propName !== 'resourceType')
|
|
220
|
+
propNames.push(propName);
|
|
216
221
|
}
|
|
217
222
|
if (propNames.length === 0)
|
|
218
223
|
continue;
|
|
@@ -266,7 +271,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
266
271
|
logger.debug('[DEBUG FINAL INTERFACES]', {
|
|
267
272
|
interfaceName,
|
|
268
273
|
interfaceCount: interfaces.length,
|
|
269
|
-
interfaceNames: interfaces.map(i => i.split('\n')[0]).filter(n => n.includes('ConditionDeBasis02'))
|
|
274
|
+
interfaceNames: interfaces.map(i => i.split('\n')[0] ?? '').filter(n => n.includes('ConditionDeBasis02'))
|
|
270
275
|
});
|
|
271
276
|
}
|
|
272
277
|
return {
|
|
@@ -122,18 +122,22 @@ export function postProcessExtensions(params) {
|
|
|
122
122
|
}
|
|
123
123
|
idx = findInterfaceIndex(name);
|
|
124
124
|
}
|
|
125
|
-
|
|
125
|
+
// Hoisted once: the block below reads the same entry five times, and
|
|
126
|
+
// indexing it each time reads as possibly-undefined even after the bound
|
|
127
|
+
// check. Reads go through `target`, writes still go to interfaces[idx].
|
|
128
|
+
const target = idx >= 0 ? interfaces[idx] : undefined;
|
|
129
|
+
if (target !== undefined && !target.includes(line.trim())) {
|
|
126
130
|
// Also check if a property with the same name already exists to avoid duplicates
|
|
127
|
-
const propName = line.trim().split(/[?:]/)[0].trim();
|
|
131
|
+
const propName = (line.trim().split(/[?:]/)[0] ?? '').trim();
|
|
128
132
|
const propPattern = new RegExp(`^\\s*${propName}[?:]`, 'm');
|
|
129
|
-
if (
|
|
133
|
+
if (target.match(propPattern)) {
|
|
130
134
|
// For extension properties, REPLACE the existing declaration with the new union
|
|
131
135
|
// This handles both union `(Extension | Type)[]` and plain `Type[]` formats
|
|
132
136
|
if (propName === 'extension' && line.includes('extension?:')) {
|
|
133
137
|
const existingExtPattern = /\s*extension\?\s*:\s*[^;\n]+;/;
|
|
134
|
-
if (existingExtPattern.test(
|
|
138
|
+
if (existingExtPattern.test(target)) {
|
|
135
139
|
const newExtLine = line.trim();
|
|
136
|
-
interfaces[idx] =
|
|
140
|
+
interfaces[idx] = target.replace(existingExtPattern, `\n ${newExtLine}`);
|
|
137
141
|
logger.debug('[DEBUG replaced extension property]', { propName, line: newExtLine });
|
|
138
142
|
return;
|
|
139
143
|
}
|
|
@@ -142,9 +146,9 @@ export function postProcessExtensions(params) {
|
|
|
142
146
|
return;
|
|
143
147
|
}
|
|
144
148
|
if (name === 'USCoreQuestionnaireResponseProfileItem' && line.includes('answer')) {
|
|
145
|
-
logger.debug('[DEBUG appendToInterface Item answer]', { name, line, currentInterface:
|
|
149
|
+
logger.debug('[DEBUG appendToInterface Item answer]', { name, line, currentInterface: target });
|
|
146
150
|
}
|
|
147
|
-
interfaces[idx] =
|
|
151
|
+
interfaces[idx] = target.replace(/}\s*$/, `\n ${line}\n}`);
|
|
148
152
|
}
|
|
149
153
|
};
|
|
150
154
|
// Ensure root interface exists and track its name
|
|
@@ -174,7 +178,9 @@ export function postProcessExtensions(params) {
|
|
|
174
178
|
continue;
|
|
175
179
|
}
|
|
176
180
|
if (segs.length === 1) {
|
|
177
|
-
const seg = segs[0].replace(/\[x\]/g, '');
|
|
181
|
+
const seg = (segs[0] ?? '').replace(/\[x\]/g, '');
|
|
182
|
+
if (!seg)
|
|
183
|
+
continue;
|
|
178
184
|
const nestedName = `${rootName}${capitalize(seg)}`;
|
|
179
185
|
// Create alias types and union
|
|
180
186
|
const aliases = [];
|
|
@@ -204,8 +210,9 @@ export function postProcessExtensions(params) {
|
|
|
204
210
|
const optionalLine = `_${seg}?: ${elementIface};`;
|
|
205
211
|
// Replace any required with optional first
|
|
206
212
|
const idx = findInterfaceIndex(rootName);
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
const existing = idx >= 0 ? interfaces[idx] : undefined;
|
|
214
|
+
if (existing !== undefined) {
|
|
215
|
+
interfaces[idx] = existing.replace(requiredLine, optionalLine);
|
|
209
216
|
}
|
|
210
217
|
appendToInterface(rootName, `_${seg}${opt}: ${elementIface};`);
|
|
211
218
|
}
|
|
@@ -265,6 +272,8 @@ export function postProcessExtensions(params) {
|
|
|
265
272
|
}
|
|
266
273
|
else if (segs.length === 2) {
|
|
267
274
|
const [seg1Raw, seg2Raw] = segs;
|
|
275
|
+
if (!seg1Raw || !seg2Raw)
|
|
276
|
+
continue;
|
|
268
277
|
const seg1 = seg1Raw.replace(/\[x\]/g, '');
|
|
269
278
|
const seg2 = seg2Raw.replace(/\[x\]/g, '');
|
|
270
279
|
// Ensure first-level container exists
|
|
@@ -389,14 +398,15 @@ export function safetyNetRootExtensions(params) {
|
|
|
389
398
|
importManager.addFhirType('Extension');
|
|
390
399
|
const union = ['Extension', ...aliases].join(' | ');
|
|
391
400
|
const idx = findInterfaceIndex(interfaceName);
|
|
392
|
-
|
|
401
|
+
const target = idx >= 0 ? interfaces[idx] : undefined;
|
|
402
|
+
if (target !== undefined) {
|
|
393
403
|
// Replace any existing extension property (union or plain type) with the new union
|
|
394
404
|
const existingExtPattern = /\s*extension\?\s*:\s*[^;\n]+;/;
|
|
395
|
-
if (existingExtPattern.test(
|
|
396
|
-
interfaces[idx] =
|
|
405
|
+
if (existingExtPattern.test(target)) {
|
|
406
|
+
interfaces[idx] = target.replace(existingExtPattern, `\n extension?: (${union})[];`);
|
|
397
407
|
}
|
|
398
408
|
else {
|
|
399
|
-
interfaces[idx] =
|
|
409
|
+
interfaces[idx] = target.replace(/}\s*$/, `\n extension?: (${union})[];\n}`);
|
|
400
410
|
}
|
|
401
411
|
}
|
|
402
412
|
}
|