babelfhir-ts 1.5.3 → 1.5.5
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.
|
@@ -30,7 +30,7 @@ export function generateBackboneSliceTypes(ctx) {
|
|
|
30
30
|
const { interfaceName, baseResource, newFields, baseFields, interfaces, importManager, inferBackboneType } = ctx;
|
|
31
31
|
const rules = getRules();
|
|
32
32
|
// 1. Find all sliced backbone element arrays
|
|
33
|
-
const sliceGroups = collectSliceGroups(newFields, baseResource);
|
|
33
|
+
const sliceGroups = collectSliceGroups(newFields, baseFields, baseResource);
|
|
34
34
|
if (sliceGroups.length === 0)
|
|
35
35
|
return;
|
|
36
36
|
for (const group of sliceGroups) {
|
|
@@ -70,7 +70,7 @@ export function generateBackboneSliceTypes(ctx) {
|
|
|
70
70
|
return childId.startsWith(sliceElementId + '.');
|
|
71
71
|
});
|
|
72
72
|
// Build interface body lines from slice children and the slice's own constraints
|
|
73
|
-
const { lines: bodyLines, referencedFhirTypes } = buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, backboneType);
|
|
73
|
+
const { lines: bodyLines, referencedFhirTypes } = buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, backboneType, baseFields, parentField.name);
|
|
74
74
|
if (bodyLines.length === 0) {
|
|
75
75
|
// No constrainable properties — skip this slice interface
|
|
76
76
|
log.debug(`No constraints for slice ${sliceName} of ${parentField.name}, skipping`);
|
|
@@ -109,10 +109,18 @@ export function generateBackboneSliceTypes(ctx) {
|
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
111
|
* Collect sliced backbone element groups from the fields.
|
|
112
|
+
*
|
|
113
|
+
* Handles two scenarios:
|
|
114
|
+
* 1. The differential includes the unsliced parent field (e.g. Observation.component) —
|
|
115
|
+
* the parent field carries slicing metadata and baseTypeCode.
|
|
116
|
+
* 2. The differential only contains slice entries (e.g. Observation.component:confidence)
|
|
117
|
+
* without redeclaring the parent — common in downstream profiles. In this case the
|
|
118
|
+
* parent field is looked up from baseFields.
|
|
112
119
|
*/
|
|
113
|
-
function collectSliceGroups(fields, baseResource) {
|
|
120
|
+
function collectSliceGroups(fields, baseFields, baseResource) {
|
|
114
121
|
const groups = [];
|
|
115
122
|
const seenPaths = new Set();
|
|
123
|
+
// Pass 1: parent field present in newFields
|
|
116
124
|
for (const field of fields) {
|
|
117
125
|
if (field.sliceName)
|
|
118
126
|
continue; // Skip slice entries themselves
|
|
@@ -121,52 +129,75 @@ function collectSliceGroups(fields, baseResource) {
|
|
|
121
129
|
const path = field.name;
|
|
122
130
|
if (seenPaths.has(path))
|
|
123
131
|
continue;
|
|
124
|
-
// Check if there are named slices at this path
|
|
125
132
|
const sliceFields = fields.filter(f => f.name === path && f.sliceName);
|
|
126
133
|
if (sliceFields.length === 0)
|
|
127
134
|
continue;
|
|
128
|
-
|
|
129
|
-
// is BackboneElement or Element (inline nested types). Skip complex DataTypes
|
|
130
|
-
// like Identifier, CodeableConcept, ContactPoint, etc. — those have slices
|
|
131
|
-
// handled by the main type mapping and profiled type reference system.
|
|
132
|
-
const baseType = field.baseTypeCode || field.type || '';
|
|
133
|
-
if (baseType !== 'BackboneElement' && baseType !== 'Element')
|
|
135
|
+
if (!isEligibleBackbonePath(field.baseTypeCode || field.type || '', path, baseResource))
|
|
134
136
|
continue;
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
137
|
+
const childFields = gatherChildFields(fields, path);
|
|
138
|
+
const slicingRules = field.slicingRules || 'open';
|
|
139
|
+
seenPaths.add(path);
|
|
140
|
+
groups.push({ parentField: field, sliceFields, childFields, slicingRules });
|
|
141
|
+
}
|
|
142
|
+
// Pass 2: orphaned slice entries whose parent path wasn't found in newFields.
|
|
143
|
+
// The differential may only contain slice entries (e.g. Observation.component:confidence)
|
|
144
|
+
// without the unsliced parent. Resolve the parent from baseFields.
|
|
145
|
+
for (const field of fields) {
|
|
146
|
+
if (!field.sliceName)
|
|
138
147
|
continue;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (fieldName === 'entry')
|
|
148
|
+
const path = field.name;
|
|
149
|
+
if (seenPaths.has(path))
|
|
142
150
|
continue;
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
151
|
+
// Find the parent field in baseFields
|
|
152
|
+
const parentField = baseFields.find(f => f.name === path && !f.sliceName);
|
|
153
|
+
if (!parentField)
|
|
146
154
|
continue;
|
|
147
|
-
if (
|
|
155
|
+
if (!parentField.isArray)
|
|
148
156
|
continue;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
157
|
-
// Determine slicing rules from the parent field's metadata
|
|
158
|
-
// The slicing rules come from the SD's element.slicing.rules
|
|
159
|
-
// We default to 'open' since that's the FHIR default
|
|
160
|
-
const slicingRules = field.slicingRules || 'open';
|
|
157
|
+
if (!isEligibleBackbonePath(parentField.baseTypeCode || parentField.type || '', path, baseResource))
|
|
158
|
+
continue;
|
|
159
|
+
const sliceFields = fields.filter(f => f.name === path && f.sliceName);
|
|
160
|
+
if (sliceFields.length === 0)
|
|
161
|
+
continue;
|
|
162
|
+
const childFields = gatherChildFields(fields, path);
|
|
163
|
+
const slicingRules = parentField.slicingRules || 'open';
|
|
161
164
|
seenPaths.add(path);
|
|
162
|
-
groups.push({ parentField
|
|
165
|
+
groups.push({ parentField, sliceFields, childFields, slicingRules });
|
|
163
166
|
}
|
|
164
167
|
return groups;
|
|
165
168
|
}
|
|
169
|
+
/** Check whether a field path is eligible for backbone slice typing. */
|
|
170
|
+
function isEligibleBackbonePath(baseType, path, baseResource) {
|
|
171
|
+
if (baseType !== 'BackboneElement' && baseType !== 'Element')
|
|
172
|
+
return false;
|
|
173
|
+
const fieldName = path.split('.').pop() || '';
|
|
174
|
+
// Skip extension slices — handled separately by postProcessExtensions
|
|
175
|
+
if (fieldName === 'extension' || fieldName === 'modifierExtension')
|
|
176
|
+
return false;
|
|
177
|
+
// Skip Bundle.entry and similar resource-container slices
|
|
178
|
+
if (fieldName === 'entry')
|
|
179
|
+
return false;
|
|
180
|
+
// Must be a root-level field (direct child of the base resource)
|
|
181
|
+
const parts = path.split('.');
|
|
182
|
+
if (parts.length !== 2)
|
|
183
|
+
return false;
|
|
184
|
+
if (baseResource && parts[0] !== baseResource)
|
|
185
|
+
return false;
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
/** Gather all child fields for any slice of the given parent path. */
|
|
189
|
+
function gatherChildFields(fields, path) {
|
|
190
|
+
return fields.filter(f => {
|
|
191
|
+
if (f.name === path)
|
|
192
|
+
return false;
|
|
193
|
+
const childId = f.elementId || f.name;
|
|
194
|
+
return childId.startsWith(`${path}:`) || (f.name.startsWith(`${path}.`) && f.name !== path);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
166
197
|
/**
|
|
167
198
|
* Build the body lines for a slice-specific interface.
|
|
168
199
|
*/
|
|
169
|
-
function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _backboneType) {
|
|
200
|
+
function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _backboneType, baseFields, parentPath) {
|
|
170
201
|
const lines = [];
|
|
171
202
|
const referencedFhirTypes = [];
|
|
172
203
|
// Add pattern constraint from the slice field itself (discriminator value)
|
|
@@ -180,16 +211,58 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
|
|
|
180
211
|
}
|
|
181
212
|
}
|
|
182
213
|
}
|
|
183
|
-
//
|
|
214
|
+
// Pre-scan: identify choice-type parent elements (value[x]) that have
|
|
215
|
+
// sub-slices (value[x]:valueQuantity, etc.). These parents carry only slicing
|
|
216
|
+
// metadata and should not be emitted as typed properties.
|
|
217
|
+
// Match only direct choice-type slices like "value[x]:valueString" — NOT deeper
|
|
218
|
+
// elements like "value[x].extension:questionnaireDisplay" where the colon is
|
|
219
|
+
// on a different segment.
|
|
220
|
+
const choiceTypeParents = new Set();
|
|
221
|
+
const choiceTypeSlices = [];
|
|
222
|
+
const choiceSlicePattern = /^([^.]+\[x]):([^.]+)$/; // e.g. "value[x]:valueString"
|
|
184
223
|
for (const child of sliceChildren) {
|
|
185
224
|
const childId = child.elementId || child.name;
|
|
186
225
|
const relativePath = childId.substring(sliceElementId.length + 1);
|
|
187
|
-
|
|
188
|
-
if (
|
|
226
|
+
const match = choiceSlicePattern.exec(relativePath);
|
|
227
|
+
if (match) {
|
|
228
|
+
choiceTypeSlices.push(child);
|
|
229
|
+
choiceTypeParents.add(match[1]); // e.g. "value[x]"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Collect deeply nested fixed values for object literal synthesis.
|
|
233
|
+
// e.g. code.coding.code = "confidence" → code: { coding: [{ code: "confidence" }] }
|
|
234
|
+
const nestedFixedByProp = new Map();
|
|
235
|
+
// Process children of the slice
|
|
236
|
+
for (const child of sliceChildren) {
|
|
237
|
+
const childId = child.elementId || child.name;
|
|
238
|
+
const relativePath = childId.substring(sliceElementId.length + 1);
|
|
239
|
+
// Nested children (dots in relative path) — collect fixed values for synthesis
|
|
240
|
+
if (relativePath.includes('.')) {
|
|
241
|
+
if (child.fixedValue !== undefined) {
|
|
242
|
+
const segments = relativePath.split('.');
|
|
243
|
+
const rawPropName = segments[0];
|
|
244
|
+
const propName = rawPropName.replace(/\[x\]$/, '');
|
|
245
|
+
// Skip nested values under:
|
|
246
|
+
// - choice-type elements ([x]) — handled by direct child or sub-slices
|
|
247
|
+
// - slice notation (part:LineItem.name) — deeper-level constraints
|
|
248
|
+
// - extension/modifierExtension — handled by postProcessExtensions
|
|
249
|
+
if (!rawPropName.includes('[x]') && !rawPropName.includes(':')
|
|
250
|
+
&& propName !== 'extension' && propName !== 'modifierExtension') {
|
|
251
|
+
const nestedPath = segments.slice(1).join('.');
|
|
252
|
+
if (!nestedFixedByProp.has(propName))
|
|
253
|
+
nestedFixedByProp.set(propName, new Map());
|
|
254
|
+
nestedFixedByProp.get(propName).set(nestedPath, String(child.fixedValue));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
189
257
|
continue;
|
|
190
|
-
|
|
258
|
+
}
|
|
259
|
+
// Choice-type sub-slices (value[x]:valueXxx) are processed separately below
|
|
191
260
|
if (relativePath.includes(':'))
|
|
192
261
|
continue;
|
|
262
|
+
// Skip choice-type parents that have sub-slices — they carry only slicing
|
|
263
|
+
// metadata and their inherited type (e.g. Quantity) is misleading
|
|
264
|
+
if (choiceTypeParents.has(relativePath))
|
|
265
|
+
continue;
|
|
193
266
|
const propName = relativePath.replace(/\[x\]$/, '');
|
|
194
267
|
if (child.fixedValue !== undefined) {
|
|
195
268
|
// Fixed value constraint
|
|
@@ -239,6 +312,43 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
|
|
|
239
312
|
}
|
|
240
313
|
}
|
|
241
314
|
}
|
|
315
|
+
// Emit choice-type sub-slice properties (e.g. value[x]:valueQuantity → valueQuantity: Quantity)
|
|
316
|
+
for (const child of choiceTypeSlices) {
|
|
317
|
+
if (!child.sliceName || !child.type)
|
|
318
|
+
continue;
|
|
319
|
+
if (child.type === 'BackboneElement' || child.type === 'Element' || child.type === 'any')
|
|
320
|
+
continue;
|
|
321
|
+
const rawType = getRules().mapTypeToTS(child.type);
|
|
322
|
+
const mappedType = sanitizeIdentifier(rawType);
|
|
323
|
+
if (mappedType !== rawType && child.isProfiled)
|
|
324
|
+
continue;
|
|
325
|
+
const optMark = child.isOptional ? '?' : '';
|
|
326
|
+
lines.push(`${child.sliceName}${optMark}: ${mappedType};`);
|
|
327
|
+
if (getRules().isFhirType(mappedType)) {
|
|
328
|
+
referencedFhirTypes.push(mappedType);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Emit synthesized nested object literals from deeply nested fixed values.
|
|
332
|
+
// Skip properties already emitted by the direct child or choice-type loops
|
|
333
|
+
// to prevent TS2717 (duplicate property) and TS2430 (incompatible extends).
|
|
334
|
+
const emittedProps = new Set(lines.map(l => l.match(/^(\w+)[?]?:/)?.[1]).filter(Boolean));
|
|
335
|
+
for (const [propName, paths] of nestedFixedByProp) {
|
|
336
|
+
if (emittedProps.has(propName))
|
|
337
|
+
continue;
|
|
338
|
+
// Check if this property is an array — look in both slice children and
|
|
339
|
+
// base fields. Synthesized literals for array properties need wrapping
|
|
340
|
+
// in [] to avoid TS2430 (incompatible extends).
|
|
341
|
+
const propFullPath = `${parentPath}.${propName}`;
|
|
342
|
+
const isArrayProp = sliceChildren.some(c => {
|
|
343
|
+
const cId = c.elementId || c.name;
|
|
344
|
+
const rel = cId.substring(sliceElementId.length + 1);
|
|
345
|
+
return rel === propName && c.isArray;
|
|
346
|
+
}) || baseFields.some(f => f.name === propFullPath && f.isArray);
|
|
347
|
+
const literal = synthesizeNestedLiteral(paths);
|
|
348
|
+
if (literal) {
|
|
349
|
+
lines.push(`${propName}: ${isArrayProp ? `[${literal}]` : literal};`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
242
352
|
return { lines, referencedFhirTypes };
|
|
243
353
|
}
|
|
244
354
|
/**
|
|
@@ -263,6 +373,39 @@ function formatCodingLiteral(codings) {
|
|
|
263
373
|
return null;
|
|
264
374
|
return `[${entries.join(', ')}]`;
|
|
265
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Synthesize a nested object literal from deeply nested fixed values.
|
|
378
|
+
*
|
|
379
|
+
* Given a map of nested paths → values (e.g. { "coding.code" → "confidence" }),
|
|
380
|
+
* produces an object literal like `{ coding: [{ code: "confidence" }] }`.
|
|
381
|
+
*
|
|
382
|
+
* Special handling: `coding` segments are wrapped in array brackets because
|
|
383
|
+
* `CodeableConcept.coding` is always an array in FHIR.
|
|
384
|
+
*/
|
|
385
|
+
function synthesizeNestedLiteral(paths) {
|
|
386
|
+
const codingFields = new Map();
|
|
387
|
+
const scalarFields = [];
|
|
388
|
+
for (const [path, value] of paths) {
|
|
389
|
+
if (path.startsWith('coding.')) {
|
|
390
|
+
codingFields.set(path.substring('coding.'.length), value);
|
|
391
|
+
}
|
|
392
|
+
else if (!path.includes('.')) {
|
|
393
|
+
scalarFields.push([path, value]);
|
|
394
|
+
}
|
|
395
|
+
// Deeper nesting (3+ levels) is uncommon; skip gracefully
|
|
396
|
+
}
|
|
397
|
+
const parts = [];
|
|
398
|
+
if (codingFields.size > 0) {
|
|
399
|
+
const codingParts = [...codingFields.entries()].map(([k, v]) => `${k}: "${v}"`);
|
|
400
|
+
parts.push(`coding: [{ ${codingParts.join('; ')} }]`);
|
|
401
|
+
}
|
|
402
|
+
for (const [field, value] of scalarFields) {
|
|
403
|
+
parts.push(`${field}: "${value}"`);
|
|
404
|
+
}
|
|
405
|
+
if (parts.length === 0)
|
|
406
|
+
return null;
|
|
407
|
+
return `{ ${parts.join('; ')} }`;
|
|
408
|
+
}
|
|
266
409
|
/**
|
|
267
410
|
* Update the root interface to use the union type for a sliced backbone field.
|
|
268
411
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B/R5 StructureDefinitions (profiles) directly inside package archives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "out/src/main.js",
|