babelfhir-ts 1.5.21 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/out/src/generator/core/constants.js +2 -1
- package/out/src/generator/core/extensionSdReader.js +4 -1
- package/out/src/generator/core/utils.js +91 -9
- package/out/src/generator/emitters/class/classGenerator.js +14 -8
- package/out/src/generator/emitters/class/classGeneratorHelpers.js +22 -9
- 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 +21 -6
- 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/interfaceFieldProcessor.js +15 -12
- package/out/src/generator/emitters/interface/interfaceFieldUtils.js +6 -3
- package/out/src/generator/emitters/interface/interfaceGenerator.js +14 -9
- package/out/src/generator/emitters/interface/postProcessExtensions.js +32 -16
- package/out/src/generator/emitters/interface/processFlatField.js +9 -7
- package/out/src/generator/emitters/interface/processNestedField.js +9 -10
- package/out/src/generator/emitters/namingsystem/namingSystemGenerator.js +136 -0
- package/out/src/generator/emitters/prefab/prefabEmitter.js +2 -15
- package/out/src/generator/emitters/prefab/prefabRenderer.js +2 -19
- package/out/src/generator/emitters/prefab/prefabTypeMapper.js +2 -0
- package/out/src/generator/emitters/validator/closedSlicingValidation.js +5 -2
- package/out/src/generator/emitters/validator/fhirpathStubInstaller.js +7 -1
- package/out/src/generator/emitters/validator/sliceBackboneValidation.js +27 -12
- 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 +41 -41
- package/out/src/generator/emitters/validator/sliceValidatorUtils.js +8 -3
- package/out/src/generator/emitters/validator/subExtensionScope.js +10 -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/validatorExpressions.js +44 -0
- package/out/src/generator/emitters/validator/validatorFieldBuilders.js +34 -17
- package/out/src/generator/emitters/validator/validatorGenerator.js +43 -47
- package/out/src/generator/emitters/validator/validatorRuntime.js +13 -2
- package/out/src/generator/emitters/validator/validatorTemplates.js +4 -1
- package/out/src/generator/emitters/valueset/valueSetGenerator.js +6 -10
- package/out/src/generator/emitters/zod/zodRefinementBuilder.js +48 -8
- package/out/src/generator/emitters/zod/zodSchemaGenerator.js +51 -24
- package/out/src/generator/fhir/corePackageResolver.js +2 -3
- package/out/src/generator/fhir/corePackageResolver.ts +2 -3
- package/out/src/generator/generationHelpers.js +59 -7
- package/out/src/generator/index.js +94 -49
- package/out/src/generator/parser/packageManager.js +4 -4
- package/out/src/generator/parser/packageParser.js +116 -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 +6 -3
- package/out/src/generator/sdProcessor.js +30 -21
- package/out/src/generator/sdProcessorRequiredFields.js +3 -2
- package/out/src/main.js +5 -0
- package/package.json +13 -10
- package/parity-matrix.json +6 -0
|
@@ -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) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
|
|
1
|
+
import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl, stripSliceSuffix } from '../../core/utils.js';
|
|
2
2
|
import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
|
|
3
3
|
import { logger } from '../../../logger.js';
|
|
4
4
|
import { getRules } from '../../fhir/versionContext.js';
|
|
@@ -45,8 +45,8 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
45
45
|
}
|
|
46
46
|
// Register external profiles for FHIR datatype profiles (like SimpleQuantity) early
|
|
47
47
|
// This must happen before any returns to ensure the profile is registered
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const profileUrl = field.isProfiled ? field.profileUrls?.[0] : undefined;
|
|
49
|
+
if (profileUrl) {
|
|
50
50
|
// Use profileUrlToName to get the actual interface name, fallback to URL-derived name
|
|
51
51
|
// Strip version suffix from canonical URL (e.g., |5.3.0-ballot-tc1) before extracting name
|
|
52
52
|
const strippedUrl = stripVersionFromCanonicalUrl(profileUrl);
|
|
@@ -61,13 +61,9 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
const fieldParts = field.name.split(".");
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
const colonIndex = fieldName.indexOf(':');
|
|
68
|
-
if (colonIndex !== -1) {
|
|
69
|
-
fieldName = fieldName.substring(0, colonIndex);
|
|
70
|
-
}
|
|
64
|
+
// Slice notation is dropped for the property name ("extension:Stadtteil"
|
|
65
|
+
// -> "extension"); field.sliceName keeps it for semantic purposes.
|
|
66
|
+
let fieldName = stripSliceSuffix(fieldParts.at(-1) ?? field.name);
|
|
71
67
|
const isRootInterface = parentInterfaceName === interfaceName;
|
|
72
68
|
const isNested = isRootInterface ? fieldParts.length > 1 : fieldParts.length >= 1;
|
|
73
69
|
// When generating a slice-specific interface, only include direct children
|
|
@@ -126,7 +122,7 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
126
122
|
// (2 segments: Identifier.system) and skip nested paths (4+ segments: Identifier.assigner.identifier.system)
|
|
127
123
|
if (isNested && fieldParts.length >= 2) {
|
|
128
124
|
const baseTypeName = fieldParts[0]; // e.g., "Identifier" from "Identifier.assigner.identifier.system"
|
|
129
|
-
const isExtendingBaseType = baseResource && baseTypeName
|
|
125
|
+
const isExtendingBaseType = !!baseResource && baseTypeName?.toLowerCase() === baseResource.toLowerCase();
|
|
130
126
|
// Check if this field will create a nested interface (has children in fields)
|
|
131
127
|
const hasChildren = fields.some((f) => f.name.startsWith(field.name + '.'));
|
|
132
128
|
const shouldKeepForNested = field.mustSupport && hasChildren;
|
|
@@ -173,7 +169,8 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
173
169
|
}
|
|
174
170
|
else if (!isLogicalModel) {
|
|
175
171
|
// For other profiles, use the original logic
|
|
176
|
-
|
|
172
|
+
const parentSegment = fieldParts.at(-2);
|
|
173
|
+
if (!parentSegment || !parentInterfaceName.toLowerCase().includes(parentSegment.toLowerCase())) {
|
|
177
174
|
return; // Skip nested fields that don't match the parent interface name
|
|
178
175
|
}
|
|
179
176
|
}
|
|
@@ -529,7 +526,13 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
529
526
|
deduped.push(line); // JSDoc or other non-field lines
|
|
530
527
|
continue;
|
|
531
528
|
}
|
|
529
|
+
// Group 1 is present whenever the regex matched, but the type does not
|
|
530
|
+
// say so; a line without it is passed through rather than deduped.
|
|
532
531
|
const name = fieldMatch[1];
|
|
532
|
+
if (!name) {
|
|
533
|
+
deduped.push(line);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
533
536
|
if (seenFieldNames.has(name)) {
|
|
534
537
|
continue; // Skip duplicate — keep the first (more specific) version
|
|
535
538
|
}
|
|
@@ -26,11 +26,14 @@ export function canonicalToTypeName(name, profileUrlToName) {
|
|
|
26
26
|
matches.add(generatedName);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
if (matches.size === 1)
|
|
30
|
-
|
|
29
|
+
if (matches.size === 1) {
|
|
30
|
+
const [only] = [...matches];
|
|
31
|
+
if (only)
|
|
32
|
+
return only;
|
|
33
|
+
}
|
|
31
34
|
}
|
|
32
35
|
// if already a valid identifier with capitalization, return as-is
|
|
33
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(t) &&
|
|
36
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(t) && /^[A-Z]/.test(t))
|
|
34
37
|
return t;
|
|
35
38
|
const parts = t.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
36
39
|
let pascal = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
@@ -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
|
|
@@ -300,9 +309,15 @@ export function postProcessExtensions(params) {
|
|
|
300
309
|
});
|
|
301
310
|
const baseFld2 = baseFieldByLastSegment.get(seg2);
|
|
302
311
|
let extendT2 = actualField2?.type || baseFld2?.type;
|
|
303
|
-
//
|
|
312
|
+
// A generic BackboneElement carries none of the base backbone's own properties, so
|
|
313
|
+
// resolve it against the level-1 backbone already resolved above
|
|
314
|
+
// (QuestionnaireResponseItem + 'answer' -> QuestionnaireResponseItemAnswer).
|
|
304
315
|
if (!extendT2 || extendT2 === 'any' || extendT2 === 'BackboneElement') {
|
|
305
|
-
|
|
316
|
+
const inferred2 = extendT1 ? inferBackboneType(extendT1, seg2) : undefined;
|
|
317
|
+
if (inferred2) {
|
|
318
|
+
extendT2 = inferred2;
|
|
319
|
+
}
|
|
320
|
+
else if (baseResource === 'Encounter' && seg1 === 'hospitalization' && seg2 === 'dischargeDisposition') {
|
|
306
321
|
extendT2 = 'CodeableConcept';
|
|
307
322
|
}
|
|
308
323
|
}
|
|
@@ -383,14 +398,15 @@ export function safetyNetRootExtensions(params) {
|
|
|
383
398
|
importManager.addFhirType('Extension');
|
|
384
399
|
const union = ['Extension', ...aliases].join(' | ');
|
|
385
400
|
const idx = findInterfaceIndex(interfaceName);
|
|
386
|
-
|
|
401
|
+
const target = idx >= 0 ? interfaces[idx] : undefined;
|
|
402
|
+
if (target !== undefined) {
|
|
387
403
|
// Replace any existing extension property (union or plain type) with the new union
|
|
388
404
|
const existingExtPattern = /\s*extension\?\s*:\s*[^;\n]+;/;
|
|
389
|
-
if (existingExtPattern.test(
|
|
390
|
-
interfaces[idx] =
|
|
405
|
+
if (existingExtPattern.test(target)) {
|
|
406
|
+
interfaces[idx] = target.replace(existingExtPattern, `\n extension?: (${union})[];`);
|
|
391
407
|
}
|
|
392
408
|
else {
|
|
393
|
-
interfaces[idx] =
|
|
409
|
+
interfaces[idx] = target.replace(/}\s*$/, `\n extension?: (${union})[];\n}`);
|
|
394
410
|
}
|
|
395
411
|
}
|
|
396
412
|
}
|
|
@@ -366,8 +366,9 @@ export function handleFlatField(args) {
|
|
|
366
366
|
}
|
|
367
367
|
else {
|
|
368
368
|
const idxEl = findInterfaceIndex(elementIface);
|
|
369
|
-
|
|
370
|
-
|
|
369
|
+
const ifaceEl = idxEl >= 0 ? interfaces[idxEl] : undefined;
|
|
370
|
+
if (ifaceEl !== undefined && !ifaceEl.includes('extension?: (')) {
|
|
371
|
+
interfaces[idxEl] = ifaceEl.replace(/\n}\s*$/, `\n ${extLine}\n}`);
|
|
371
372
|
}
|
|
372
373
|
}
|
|
373
374
|
// Add sidecar property _fieldName?: ElementIface to the current parent's interface lines
|
|
@@ -391,8 +392,8 @@ export function handleFlatField(args) {
|
|
|
391
392
|
// If a required variant was already written elsewhere, replace it with optional
|
|
392
393
|
if (writtenLines.has(sidecarLineRequired)) {
|
|
393
394
|
// replace in existing interfaces
|
|
394
|
-
for (
|
|
395
|
-
interfaces[i] =
|
|
395
|
+
for (const [i, iface] of interfaces.entries()) {
|
|
396
|
+
interfaces[i] = iface.replace(sidecarLineRequired, sidecarLineOptional);
|
|
396
397
|
}
|
|
397
398
|
writtenLines.delete(sidecarLineRequired);
|
|
398
399
|
}
|
|
@@ -414,8 +415,9 @@ export function handleFlatField(args) {
|
|
|
414
415
|
const nestedDefined = interfaces.some(i => i.startsWith(`export interface ${nestedInterfaceName}`));
|
|
415
416
|
if (nestedDefined) {
|
|
416
417
|
const idx = findInterfaceIndex(nestedInterfaceName);
|
|
417
|
-
|
|
418
|
-
|
|
418
|
+
const iface = idx >= 0 ? interfaces[idx] : undefined;
|
|
419
|
+
if (iface !== undefined && !iface.includes('extension?: (')) {
|
|
420
|
+
interfaces[idx] = iface.replace(/\n}\s*$/, `\n ${extLine}\n}`);
|
|
419
421
|
}
|
|
420
422
|
}
|
|
421
423
|
else {
|
|
@@ -463,7 +465,7 @@ export function handleFlatField(args) {
|
|
|
463
465
|
fieldType = applyBindingConstraint(fieldType, field, fields);
|
|
464
466
|
// Apply ValueSet binding constraints for primitive code fields (generates union literal type)
|
|
465
467
|
if (isPrimitiveCodeField && hasNarrowingBinding && field.binding?.codes) {
|
|
466
|
-
const vsUri = field.binding.uri
|
|
468
|
+
const vsUri = field.binding.uri ? stripVersionFromCanonicalUrl(field.binding.uri) : undefined;
|
|
467
469
|
const vs = vsUri && valueSets ? (valueSets.get(vsUri) || valueSets.get(field.binding.uri)) : undefined;
|
|
468
470
|
if (vs && vs.isSmall && vs.concepts.length > 0) {
|
|
469
471
|
// Import the ValueSet Code type from the generated valuesets directory
|
|
@@ -169,18 +169,17 @@ export function handleNestedField(args) {
|
|
|
169
169
|
if (nestedDefined) {
|
|
170
170
|
// Append a line to the nested interface's content
|
|
171
171
|
const idx = findInterfaceIndex(nestedInterfaceName);
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
const iface = idx >= 0 ? interfaces[idx] : undefined;
|
|
173
|
+
if (iface !== undefined && !iface.includes('extension?: (')) {
|
|
174
|
+
interfaces[idx] = iface.replace(/\n}\s*$/, `\n ${extLine}\n}`);
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
else {
|
|
177
178
|
// No nested interface existed; create a minimal one extending the base/inferred type to host the extension union
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
parentT = inferred;
|
|
183
|
-
}
|
|
179
|
+
// Reuse the type resolved above rather than recomputing it: the local
|
|
180
|
+
// recomputation used to stop at a generic BackboneElement, which drops the
|
|
181
|
+
// base backbone's own properties (e.g. QuestionnaireResponseItem.linkId).
|
|
182
|
+
let parentT = baseFieldType;
|
|
184
183
|
if (parentT && !isPrimitiveType(parentT)) {
|
|
185
184
|
parentT = canonicalToTypeName(parentT);
|
|
186
185
|
const sanitized = sanitizeIdentifier(parentT);
|
|
@@ -394,8 +393,8 @@ export function handleNestedField(args) {
|
|
|
394
393
|
}
|
|
395
394
|
// Register external profiles for any field with profile URLs, regardless of nesting
|
|
396
395
|
// This handles FHIR datatype profiles like SimpleQuantity that aren't in @types/fhir
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
const [profileUrl] = field.profileUrls ?? [];
|
|
397
|
+
if (field.isProfiled && profileUrl && !isFhirType(resolvedType)) {
|
|
399
398
|
// Strip version suffix from canonical URL before extracting name
|
|
400
399
|
const strippedUrl = stripVersionFromCanonicalUrl(profileUrl);
|
|
401
400
|
// Use helper to determine base type - extension fields get 'Extension' as base type
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NamingSystem Generator
|
|
3
|
+
*
|
|
4
|
+
* Emits a `namingsystems/` module mirroring `valuesets/`, so an IG's declared
|
|
5
|
+
* identifier systems are consumable as typed constants instead of retyped as
|
|
6
|
+
* string literals in every downstream repo.
|
|
7
|
+
*/
|
|
8
|
+
import { pascalCaseIdentifier } from '../../core/utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize a NamingSystem name for use as a TypeScript identifier.
|
|
11
|
+
*/
|
|
12
|
+
export function sanitizeNamingSystemName(name) {
|
|
13
|
+
return pascalCaseIdentifier(name, {
|
|
14
|
+
stripPrefix: /^NamingSystem[-_]?/i,
|
|
15
|
+
digitPrefix: 'NS',
|
|
16
|
+
fallback: 'UnnamedNamingSystem',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Assign a unique identifier to every NamingSystem that will be emitted.
|
|
21
|
+
*
|
|
22
|
+
* Single source of truth for NamingSystem naming, shared by the file writer and
|
|
23
|
+
* the registry — the same arrangement `assignValueSetNames` uses, and for the
|
|
24
|
+
* same reason: two independent deduplication passes drift, and the registry
|
|
25
|
+
* then imports a file that was never written (TS2307).
|
|
26
|
+
*
|
|
27
|
+
* A NamingSystem has no canonical URL in R4, so identity here is the preferred
|
|
28
|
+
* uniqueId value rather than a `url` element.
|
|
29
|
+
*/
|
|
30
|
+
export function assignNamingSystemNames(namingSystems) {
|
|
31
|
+
const takenNames = new Set();
|
|
32
|
+
const seenIdentities = new Set();
|
|
33
|
+
const assigned = [];
|
|
34
|
+
for (const namingSystem of namingSystems) {
|
|
35
|
+
const identity = preferredUniqueId(namingSystem).value;
|
|
36
|
+
if (seenIdentities.has(identity))
|
|
37
|
+
continue;
|
|
38
|
+
seenIdentities.add(identity);
|
|
39
|
+
const base = sanitizeNamingSystemName(namingSystem.name);
|
|
40
|
+
let sanitized = base;
|
|
41
|
+
let suffix = 2;
|
|
42
|
+
while (takenNames.has(sanitized)) {
|
|
43
|
+
sanitized = `${base}${suffix}`;
|
|
44
|
+
suffix++;
|
|
45
|
+
}
|
|
46
|
+
takenNames.add(sanitized);
|
|
47
|
+
assigned.push({ namingSystem, sanitized });
|
|
48
|
+
}
|
|
49
|
+
return assigned;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The uniqueId a consumer means when they say "the system".
|
|
53
|
+
*
|
|
54
|
+
* `preferred: true` wins, then the first `uri` entry (what a profile fixes on
|
|
55
|
+
* `Identifier.system`), then the first entry at all.
|
|
56
|
+
*/
|
|
57
|
+
export function preferredUniqueId(namingSystem) {
|
|
58
|
+
return namingSystem.uniqueIds.find(u => u.preferred)
|
|
59
|
+
?? namingSystem.uniqueIds.find(u => u.type === 'uri')
|
|
60
|
+
?? namingSystem.uniqueIds[0];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generate TypeScript content for a NamingSystem.
|
|
64
|
+
*/
|
|
65
|
+
export function generateNamingSystemTypeScript(namingSystem, options) {
|
|
66
|
+
const name = options?.nameOverride ?? sanitizeNamingSystemName(namingSystem.name);
|
|
67
|
+
const filename = `NamingSystem-${name}.ts`;
|
|
68
|
+
const preferred = preferredUniqueId(namingSystem);
|
|
69
|
+
let content = `/**\n * NamingSystem: ${namingSystem.title || namingSystem.name}\n`;
|
|
70
|
+
if (namingSystem.description) {
|
|
71
|
+
content += ` * ${namingSystem.description.replace(/\*\//g, '*\\/').split('\n').join('\n * ')}\n`;
|
|
72
|
+
}
|
|
73
|
+
if (namingSystem.kind)
|
|
74
|
+
content += ` * Kind: ${namingSystem.kind}\n`;
|
|
75
|
+
content += ` */\n\n`;
|
|
76
|
+
content += `export const ${name}UniqueIds = [\n`;
|
|
77
|
+
for (const uid of namingSystem.uniqueIds) {
|
|
78
|
+
const preferredPart = uid.preferred !== undefined ? `, preferred: ${uid.preferred}` : '';
|
|
79
|
+
const commentPart = uid.comment ? `, comment: ${JSON.stringify(uid.comment)}` : '';
|
|
80
|
+
content += ` { type: ${JSON.stringify(uid.type)}, value: ${JSON.stringify(uid.value)}${preferredPart}${commentPart} },\n`;
|
|
81
|
+
}
|
|
82
|
+
content += `] as const;\n\n`;
|
|
83
|
+
content += `/**\n * The identifier system this NamingSystem defines.\n`;
|
|
84
|
+
content += ` * Use in place of a hand-written literal on \`Identifier.system\`.\n */\n`;
|
|
85
|
+
content += `export const ${name}System = ${JSON.stringify(preferred.value)};\n\n`;
|
|
86
|
+
const valueUnion = [...new Set(namingSystem.uniqueIds.map(u => JSON.stringify(u.value)))].join(' | ');
|
|
87
|
+
content += `/** Union of every uniqueId value this NamingSystem declares */\n`;
|
|
88
|
+
content += `export type ${name}Value = ${valueUnion};\n\n`;
|
|
89
|
+
content += `/** Type representing a uniqueId entry of this NamingSystem */\n`;
|
|
90
|
+
content += `export type ${name}UniqueId = typeof ${name}UniqueIds[number];\n\n`;
|
|
91
|
+
content += `/**\n * Check whether a value is one of this NamingSystem's declared identifiers\n */\n`;
|
|
92
|
+
content += `export function isValid${name}Value(value: string): value is ${name}Value {\n`;
|
|
93
|
+
content += ` return ${name}UniqueIds.some(u => u.value === value);\n`;
|
|
94
|
+
content += `}\n\n`;
|
|
95
|
+
content += `/**\n * Get this NamingSystem's identifier for a given uniqueId type (oid, uuid, uri, other)\n */\n`;
|
|
96
|
+
content += `export function get${name}Value(type: string): string | undefined {\n`;
|
|
97
|
+
content += ` return ${name}UniqueIds.find(u => u.type === type)?.value;\n`;
|
|
98
|
+
content += `}\n`;
|
|
99
|
+
return { filename, content };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generate a registry file that exports all NamingSystems.
|
|
103
|
+
*/
|
|
104
|
+
export function generateNamingSystemRegistry(namingSystems) {
|
|
105
|
+
let content = `/**\n * NamingSystem Registry\n * Central registry of all identifier systems declared by this package\n */\n\n`;
|
|
106
|
+
// Names come from the shared assignment pass so every import below refers to
|
|
107
|
+
// a file the writer actually emitted (see assignNamingSystemNames).
|
|
108
|
+
const sorted = assignNamingSystemNames(namingSystems)
|
|
109
|
+
.map(({ namingSystem, sanitized }) => ({ sanitized, system: preferredUniqueId(namingSystem).value }))
|
|
110
|
+
.sort((a, b) => a.sanitized.localeCompare(b.sanitized));
|
|
111
|
+
for (const { sanitized } of sorted) {
|
|
112
|
+
content += `import * as ${sanitized} from './NamingSystem-${sanitized}.js';\n`;
|
|
113
|
+
}
|
|
114
|
+
content += '\n';
|
|
115
|
+
content += `export const NamingSystemRegistry = {\n`;
|
|
116
|
+
for (const { sanitized } of sorted) {
|
|
117
|
+
content += ` ${sanitized},\n`;
|
|
118
|
+
}
|
|
119
|
+
content += `} as const;\n\n`;
|
|
120
|
+
content += `export type NamingSystemName = keyof typeof NamingSystemRegistry;\n\n`;
|
|
121
|
+
content += `export function getNamingSystem(name: NamingSystemName) {\n`;
|
|
122
|
+
content += ` return NamingSystemRegistry[name];\n`;
|
|
123
|
+
content += `}\n\n`;
|
|
124
|
+
content += `/** Map from identifier system value to registry name */\n`;
|
|
125
|
+
content += `export const NamingSystemUrlMap: Record<string, NamingSystemName> = {\n`;
|
|
126
|
+
for (const { sanitized, system } of sorted) {
|
|
127
|
+
content += ` ${JSON.stringify(system)}: '${sanitized}',\n`;
|
|
128
|
+
}
|
|
129
|
+
content += `};\n\n`;
|
|
130
|
+
content += `/**\n * Look up the registry entry that declares an identifier system\n * @param system - An identifier system value (e.g. from \`Identifier.system\`)\n */\n`;
|
|
131
|
+
content += `export function getNamingSystemBySystem(system: string) {\n`;
|
|
132
|
+
content += ` const name = NamingSystemUrlMap[system];\n`;
|
|
133
|
+
content += ` return name ? NamingSystemRegistry[name] : undefined;\n`;
|
|
134
|
+
content += `}\n`;
|
|
135
|
+
return content;
|
|
136
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { lowerFirstPreservingAcronym } from '../../core/utils.js';
|
|
11
12
|
import { generatePrefabRenderer } from './prefabRenderer.js';
|
|
12
13
|
import { FHIR_FORMAT_TS, DEFAULT_STYLES_TS, REGISTER_FHIR_PIPES_TS, buildPrefabIndexTs, } from './prefabRuntimeAssets.js';
|
|
13
14
|
import { logger } from '../../../logger.js';
|
|
@@ -88,21 +89,7 @@ export function resetPrefabState() {
|
|
|
88
89
|
emittedProfiles.clear();
|
|
89
90
|
}
|
|
90
91
|
// ── MCP View Registry generator ──────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
* Lower-case the first character(s) of a name, preserving acronym runs.
|
|
93
|
-
* Mirrors the same logic in prefabRenderer.ts.
|
|
94
|
-
*/
|
|
95
|
-
function lowerFirst(name) {
|
|
96
|
-
if (!name)
|
|
97
|
-
return name;
|
|
98
|
-
let i = 0;
|
|
99
|
-
while (i < name.length && name[i] >= 'A' && name[i] <= 'Z')
|
|
100
|
-
i++;
|
|
101
|
-
if (i <= 1)
|
|
102
|
-
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
103
|
-
const acronymEnd = i === name.length ? i : i - 1;
|
|
104
|
-
return name.slice(0, acronymEnd).toLowerCase() + name.slice(acronymEnd);
|
|
105
|
-
}
|
|
92
|
+
const lowerFirst = lowerFirstPreservingAcronym;
|
|
106
93
|
/**
|
|
107
94
|
* Build the `mcpViews.ts` source — a `FhirViewRegistry` that maps FHIR
|
|
108
95
|
* resource types to their generated prefab table/detail components.
|
|
@@ -11,26 +11,9 @@
|
|
|
11
11
|
* The emitted code depends on `@maxhealth.tech/prefab` and the sibling
|
|
12
12
|
* `fhirFormat.ts` / `styles.ts` files produced in the same output folder.
|
|
13
13
|
*/
|
|
14
|
+
import { lowerFirstPreservingAcronym } from '../../core/utils.js';
|
|
14
15
|
import { describeField, dedupeDescriptors, pickTableDescriptors, findTitleDescriptor, } from './prefabTypeMapper.js';
|
|
15
|
-
|
|
16
|
-
* camelCase first char lowercased, but preserve leading acronym runs.
|
|
17
|
-
* "CHCorePatient" → "chCorePatient" (acronym lowered wholesale)
|
|
18
|
-
* "Patient" → "patient"
|
|
19
|
-
* "HumanName" → "humanName"
|
|
20
|
-
*/
|
|
21
|
-
function lowerFirst(name) {
|
|
22
|
-
if (!name)
|
|
23
|
-
return name;
|
|
24
|
-
// Count leading uppercase letters (the acronym run).
|
|
25
|
-
let i = 0;
|
|
26
|
-
while (i < name.length && name[i] >= 'A' && name[i] <= 'Z')
|
|
27
|
-
i++;
|
|
28
|
-
if (i <= 1)
|
|
29
|
-
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
30
|
-
// Preserve boundary: last uppercase stays uppercase if it starts the next word.
|
|
31
|
-
const acronymEnd = i === name.length ? i : i - 1;
|
|
32
|
-
return name.slice(0, acronymEnd).toLowerCase() + name.slice(acronymEnd);
|
|
33
|
-
}
|
|
16
|
+
const lowerFirst = lowerFirstPreservingAcronym;
|
|
34
17
|
/** Emit a JS expression that extracts the display value for a descriptor from `r`. */
|
|
35
18
|
function rowAccessor(d) {
|
|
36
19
|
// Descriptors can pre-bake a bespoke accessor (e.g. synthesized per-system
|
|
@@ -100,6 +100,8 @@ export function describeField(field) {
|
|
|
100
100
|
if (segments.length !== 2)
|
|
101
101
|
return [];
|
|
102
102
|
const bareName = segments[1];
|
|
103
|
+
if (!bareName)
|
|
104
|
+
return [];
|
|
103
105
|
// Skip extension plumbing — surfaced separately once sliced-extension rendering lands.
|
|
104
106
|
if (bareName === 'extension' || bareName === 'modifierExtension')
|
|
105
107
|
return [];
|
|
@@ -34,7 +34,8 @@ function instanceUrl(slice, fields) {
|
|
|
34
34
|
if (childId === sliceFullId + '.url' && typeof f.fixedValue === 'string')
|
|
35
35
|
return f.fixedValue;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
const firstProfile = slice.profileUrls?.[0];
|
|
38
|
+
return firstProfile ? stripVersionFromCanonicalUrl(firstProfile) : undefined;
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
41
|
* The property name a choice slice occupies.
|
|
@@ -87,7 +88,9 @@ export function generateClosedSlicingValidations(fields, profileUrl, baseResourc
|
|
|
87
88
|
if (elemId.includes(':'))
|
|
88
89
|
continue;
|
|
89
90
|
const segments = elemId.split('.');
|
|
90
|
-
const leaf = segments
|
|
91
|
+
const leaf = segments.at(-1);
|
|
92
|
+
if (!leaf)
|
|
93
|
+
continue;
|
|
91
94
|
const kind = classify(leaf);
|
|
92
95
|
if (!kind)
|
|
93
96
|
continue;
|
|
@@ -27,13 +27,19 @@ export function installFhirpathStub(outputDir) {
|
|
|
27
27
|
types: 'index.d.ts',
|
|
28
28
|
}, null, 2));
|
|
29
29
|
// Minimal type declaration — only what the generated validators import
|
|
30
|
+
// `compile` is declared alongside `evaluate`: the emitted Zod schemas compile
|
|
31
|
+
// each invariant once at module load rather than re-parsing it per parse.
|
|
30
32
|
fs.writeFileSync(path.join(pkgDir, 'index.d.ts'), `declare const fhirpath: {
|
|
31
33
|
evaluate(resource: any, expression: string, context?: any, model?: any, options?: any): any[];
|
|
34
|
+
compile(expression: string, model?: any, options?: any): (resource: any, context?: any) => any[];
|
|
32
35
|
};
|
|
33
36
|
export default fhirpath;
|
|
34
37
|
`);
|
|
35
38
|
// Minimal JS stub (default export so ESM import doesn't crash if somehow reached at runtime)
|
|
36
|
-
fs.writeFileSync(path.join(pkgDir, 'index.js'), `export default {
|
|
39
|
+
fs.writeFileSync(path.join(pkgDir, 'index.js'), `export default {
|
|
40
|
+
evaluate() { throw new Error("fhirpath stub — install the real package"); },
|
|
41
|
+
compile() { throw new Error("fhirpath stub — install the real package"); },
|
|
42
|
+
};\n`);
|
|
37
43
|
// Stub fhir-context/<version>/index.js for each supported FHIR version
|
|
38
44
|
for (const ver of FHIR_CONTEXT_VERSIONS) {
|
|
39
45
|
const ctxDir = path.join(pkgDir, 'fhir-context', ver);
|