babelfhir-ts 1.5.19 → 1.5.21
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/cli/updateCommand.js +19 -12
- package/out/src/generator/core/constants.js +19 -0
- package/out/src/generator/core/extensionSdReader.js +99 -0
- package/out/src/generator/core/utils.js +97 -3
- package/out/src/generator/emitters/class/classTemplate.js +8 -2
- package/out/src/generator/emitters/class/sliceElementDefaults.js +5 -3
- package/out/src/generator/emitters/client/clientGenerator.js +22 -6
- package/out/src/generator/emitters/client/smartAuthGenerator.js +4 -1
- package/out/src/generator/emitters/interface/backboneSliceTyping.js +15 -4
- package/out/src/generator/emitters/interface/importManager.js +28 -2
- package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +21 -16
- package/out/src/generator/emitters/interface/interfaceFieldUtils.js +16 -0
- package/out/src/generator/emitters/interface/postProcessExtensions.js +1 -0
- package/out/src/generator/emitters/validator/closedSlicingValidation.js +146 -0
- package/out/src/generator/emitters/validator/sliceDelegation.js +450 -0
- package/out/src/generator/emitters/validator/sliceExtensionValidation.js +200 -35
- package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +130 -299
- package/out/src/generator/emitters/validator/sliceValidatorUtils.js +89 -16
- package/out/src/generator/emitters/validator/subExtensionScope.js +100 -0
- package/out/src/generator/emitters/validator/validatorBindingBuilder.js +113 -237
- package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +13 -4
- package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +104 -63
- package/out/src/generator/emitters/validator/validatorExpressions.js +54 -0
- package/out/src/generator/emitters/validator/validatorFieldBuilders.js +492 -46
- package/out/src/generator/emitters/validator/validatorGenerator.js +170 -169
- package/out/src/generator/emitters/validator/validatorRuntime.js +279 -0
- package/out/src/generator/emitters/validator/validatorTemplates.js +186 -54
- package/out/src/generator/emitters/zod/zodRefinementBuilder.js +29 -19
- package/out/src/generator/emitters/zod/zodSchemaGenerator.js +16 -1
- package/out/src/generator/index.js +21 -11
- package/out/src/generator/parser/packageParser.js +1 -8
- package/out/src/generator/parser/sdParser.js +1 -0
- package/out/src/generator/sdProcessor.js +303 -30
- package/out/src/generator/sdProcessorHelpers.js +1 -1
- package/out/src/generator/sdProcessorRequiredFields.js +120 -0
- package/out/src/main.js +37 -11
- package/package.json +3 -1
- package/parity-matrix.json +18 -18
|
@@ -123,32 +123,96 @@ export function isArrayPath(relPath, fieldPathMap, arrayFieldPaths) {
|
|
|
123
123
|
return false;
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
126
|
+
* Resolve a slice that pins a *coded child* rather than itself.
|
|
127
|
+
*
|
|
128
|
+
* patternIdentifier of `{type: {coding: [...]}}` discriminates on
|
|
129
|
+
* identifier.type, not on the Identifier. That matches neither the
|
|
130
|
+
* patternCodeableConcept shape (`coding` at the top level) nor patternCoding
|
|
131
|
+
* (`system`/`code` at the top level), so such a slice fell through to the child's
|
|
132
|
+
* *binding* instead: cdex pins tracking-id, and we compared against the whole
|
|
133
|
+
* v2-0203 identifier-type ValueSet, so a conformant identifier never matched and
|
|
134
|
+
* the required slice was reported missing.
|
|
135
|
+
*
|
|
136
|
+
* Returns a synthetic child field carrying the nested pattern, so the existing
|
|
137
|
+
* child-pattern machinery discriminates on the pinned coding.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveNestedPatternChild(slice) {
|
|
140
|
+
const pattern = slice.patternConstraint;
|
|
141
|
+
if (!pattern || typeof pattern !== 'object')
|
|
142
|
+
return undefined;
|
|
143
|
+
const top = pattern;
|
|
144
|
+
// Already handled by the patternCodeableConcept / patternCoding branches.
|
|
145
|
+
if ('coding' in top || 'system' in top || 'code' in top)
|
|
146
|
+
return undefined;
|
|
147
|
+
for (const [childProp, value] of Object.entries(top)) {
|
|
148
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
149
|
+
continue;
|
|
150
|
+
const coding = value.coding;
|
|
151
|
+
if (!Array.isArray(coding) || coding.length === 0)
|
|
152
|
+
continue;
|
|
153
|
+
return {
|
|
154
|
+
...slice,
|
|
155
|
+
name: `${slice.name}.${childProp}`,
|
|
156
|
+
elementId: `${slice.elementId || slice.name}.${childProp}`,
|
|
157
|
+
type: 'CodeableConcept',
|
|
158
|
+
baseTypeCode: 'CodeableConcept',
|
|
159
|
+
patternConstraint: value,
|
|
160
|
+
// The pinned coding is the discriminator; the child's own binding is a
|
|
161
|
+
// wider allowed set and must not stand in for it.
|
|
162
|
+
binding: undefined,
|
|
163
|
+
sliceName: undefined,
|
|
164
|
+
isArray: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Collect descendants of a slice that have finite max cardinality constraints.
|
|
127
171
|
* Used by slice validators to emit within-slice max checks (e.g., max=0 forbidden fields).
|
|
172
|
+
*
|
|
173
|
+
* Nested paths (e.g. entity:patient.what.reference) are included only when every
|
|
174
|
+
* intermediate segment is a single-valued, non-choice element. Traversing an array or
|
|
175
|
+
* choice-typed intermediate would require per-item descent that `at()` does not do, so
|
|
176
|
+
* those are skipped rather than risk a false positive.
|
|
128
177
|
*/
|
|
129
178
|
export function collectWithinSliceMaxChildren(slice, fields) {
|
|
130
179
|
const sliceElemId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
|
|
131
180
|
const sliceFullId = slice.sliceName && !sliceElemId.includes(':')
|
|
132
181
|
? `${sliceElemId}:${slice.sliceName}`
|
|
133
182
|
: sliceElemId;
|
|
183
|
+
/** True when the element at `relPath` inside the slice is safe to descend through. */
|
|
184
|
+
const isTraversableIntermediate = (relPath) => {
|
|
185
|
+
if (relPath.endsWith('[x]'))
|
|
186
|
+
return false;
|
|
187
|
+
const target = `${sliceFullId}.${relPath}`;
|
|
188
|
+
const match = fields.find(f => (f.elementId || f.name).replace(/^[^.]+\./, '') === target);
|
|
189
|
+
if (!match)
|
|
190
|
+
return false;
|
|
191
|
+
if (match.isArray)
|
|
192
|
+
return false;
|
|
193
|
+
return typeof match.max !== 'number' || match.max <= 1;
|
|
194
|
+
};
|
|
134
195
|
const result = [];
|
|
135
196
|
for (const f of fields) {
|
|
136
197
|
const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
|
|
137
198
|
if (!fId.startsWith(sliceFullId + '.'))
|
|
138
199
|
continue;
|
|
139
200
|
const relChild = fId.substring(sliceFullId.length + 1);
|
|
140
|
-
if (relChild.includes('.'))
|
|
141
|
-
continue; // only direct children
|
|
142
201
|
if (relChild.includes(':'))
|
|
143
202
|
continue; // skip sub-slices (e.g., part:MemberPatient) — handled by dedicated slice validators
|
|
144
203
|
if (typeof f.max !== 'number' || f.max >= Infinity)
|
|
145
204
|
continue;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
205
|
+
const segments = relChild.split('.');
|
|
206
|
+
// Every segment above the leaf must be safely traversable.
|
|
207
|
+
const intermediatesOk = segments
|
|
208
|
+
.slice(0, -1)
|
|
209
|
+
.every((_, i) => isTraversableIntermediate(segments.slice(0, i + 1).join('.')));
|
|
210
|
+
if (!intermediatesOk)
|
|
211
|
+
continue;
|
|
212
|
+
const leaf = segments[segments.length - 1];
|
|
213
|
+
const isChoiceType = leaf.endsWith('[x]');
|
|
214
|
+
const propPath = [...segments.slice(0, -1), isChoiceType ? leaf.replace(/\[x\]$/, '') : leaf];
|
|
215
|
+
result.push({ propPath, fhirPath: relChild, maxVal: f.max, isArray: !!f.isArray, isChoiceType });
|
|
152
216
|
}
|
|
153
217
|
return result;
|
|
154
218
|
}
|
|
@@ -158,13 +222,22 @@ export function collectWithinSliceMaxChildren(slice, fields) {
|
|
|
158
222
|
*/
|
|
159
223
|
export function generateWithinSliceMaxChecks(children, elementsVarName, resType, relPath, sliceLabel) {
|
|
160
224
|
let code = '';
|
|
161
|
-
for (const {
|
|
225
|
+
for (const { propPath, fhirPath, maxVal, isArray, isChoiceType } of children) {
|
|
226
|
+
const errorPath = `${resType}.${relPath}:${sliceLabel}.${fhirPath}`;
|
|
227
|
+
// Address the leaf via at()'s variadic path so nested elements
|
|
228
|
+
// (e.g. entity:patient.what.reference) resolve from the slice element.
|
|
229
|
+
const accessArgs = propPath.map(p => `'${p}'`).join(', ');
|
|
162
230
|
if (maxVal === 0 && isChoiceType) {
|
|
163
231
|
// max=0 on choice type (e.g., value[x]): check no key starting with prefix exists
|
|
232
|
+
const parentPath = propPath.slice(0, -1);
|
|
233
|
+
const parentExpr = parentPath.length
|
|
234
|
+
? `at(_wsElem, ${parentPath.map(p => `'${p}'`).join(', ')})`
|
|
235
|
+
: '_wsElem';
|
|
236
|
+
const prefix = propPath[propPath.length - 1];
|
|
164
237
|
code += `
|
|
165
238
|
for (const _wsElem of ${elementsVarName}) {
|
|
166
|
-
if (Object.keys(node(
|
|
167
|
-
errors.push("${
|
|
239
|
+
if (Object.keys(node(${parentExpr})).some(k => k.startsWith("${prefix}"))) {
|
|
240
|
+
errors.push("${errorPath}: max allowed = 0, but found 1");
|
|
168
241
|
}
|
|
169
242
|
}`;
|
|
170
243
|
}
|
|
@@ -172,9 +245,9 @@ export function generateWithinSliceMaxChecks(children, elementsVarName, resType,
|
|
|
172
245
|
// max=0: field must not exist at all — handles both scalar and array
|
|
173
246
|
code += `
|
|
174
247
|
for (const _wsElem of ${elementsVarName}) {
|
|
175
|
-
const _wsVal = at(_wsElem,
|
|
248
|
+
const _wsVal = at(_wsElem, ${accessArgs});
|
|
176
249
|
if (_wsVal !== undefined && _wsVal !== null) {
|
|
177
|
-
errors.push("${
|
|
250
|
+
errors.push("${errorPath}: max allowed = 0, but found " + (Array.isArray(_wsVal) ? _wsVal.length : 1));
|
|
178
251
|
}
|
|
179
252
|
}`;
|
|
180
253
|
}
|
|
@@ -182,9 +255,9 @@ export function generateWithinSliceMaxChecks(children, elementsVarName, resType,
|
|
|
182
255
|
// array field: check count doesn't exceed max
|
|
183
256
|
code += `
|
|
184
257
|
for (const _wsElem of ${elementsVarName}) {
|
|
185
|
-
const _wsArr = at(_wsElem,
|
|
258
|
+
const _wsArr = at(_wsElem, ${accessArgs});
|
|
186
259
|
if (Array.isArray(_wsArr) && _wsArr.length > ${maxVal}) {
|
|
187
|
-
errors.push("${
|
|
260
|
+
errors.push("${errorPath}: max allowed = ${maxVal}, but found " + _wsArr.length);
|
|
188
261
|
}
|
|
189
262
|
}`;
|
|
190
263
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Addressing a sub-extension slice — an extension slice nested inside another.
|
|
3
|
+
*
|
|
4
|
+
* Split out of sliceExtensionValidation.ts to keep it under the 650 LOC gate, and
|
|
5
|
+
* because this is a self-contained concern: turning a slice's element id into the
|
|
6
|
+
* runtime walk that reaches the elements *containing* it.
|
|
7
|
+
*/
|
|
8
|
+
import { stripVersionFromCanonicalUrl } from '../../core/utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* The url an *instance* of this extension slice carries.
|
|
11
|
+
*
|
|
12
|
+
* `detectExtensionUrlSlice` answers a different question — which canonical names the
|
|
13
|
+
* slice — and prefers `type.profile`, which may be version-pinned (`…|4.0.0`). An
|
|
14
|
+
* instance never writes the version, so matching on that form finds nothing. The
|
|
15
|
+
* pinned `url` child is what the instance actually carries; fall back to the profile
|
|
16
|
+
* canonical with the version stripped.
|
|
17
|
+
*/
|
|
18
|
+
export function instanceUrlForSlice(slice, fields) {
|
|
19
|
+
const sliceElementId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
|
|
20
|
+
const sliceFullId = slice.sliceName && !sliceElementId.includes(':')
|
|
21
|
+
? `${sliceElementId}:${slice.sliceName}`
|
|
22
|
+
: sliceElementId;
|
|
23
|
+
for (const f of fields) {
|
|
24
|
+
const childId = (f.elementId || f.name).replace(/^[^.]+\./, '');
|
|
25
|
+
if (childId === sliceFullId + '.url' && typeof f.fixedValue === 'string')
|
|
26
|
+
return f.fixedValue;
|
|
27
|
+
}
|
|
28
|
+
return slice.profileUrls?.length ? stripVersionFromCanonicalUrl(slice.profileUrls[0]) : undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the walk down to the elements that *contain* a sub-extension slice.
|
|
32
|
+
*
|
|
33
|
+
* Only for a slice nested inside another extension slice
|
|
34
|
+
* (`item.extension:contextExpression.extension:label`). The generic path builds one
|
|
35
|
+
* `at(_el, 'extension', 'extension')` hop, which returns undefined the moment it
|
|
36
|
+
* meets the `extension` array — so the check read nothing and its `length < min`
|
|
37
|
+
* assertion fired on every parent element instead. It also ignored the parent slice
|
|
38
|
+
* entirely, applying contextExpression's requirements to every extension on the item.
|
|
39
|
+
*
|
|
40
|
+
* Returns null when the slice is not of that shape, leaving the existing paths alone.
|
|
41
|
+
*/
|
|
42
|
+
export function buildSubExtensionScope(slice, fields) {
|
|
43
|
+
const elemId = (slice.elementId || '').replace(/^[^.]+\./, '');
|
|
44
|
+
if (!elemId)
|
|
45
|
+
return null;
|
|
46
|
+
const segments = elemId.split('.');
|
|
47
|
+
// The slice itself is the last segment; everything before it is the container walk.
|
|
48
|
+
const containerSegments = segments.slice(0, -1);
|
|
49
|
+
const slicedAncestors = containerSegments.filter(s => {
|
|
50
|
+
const [prop, sliceName] = s.split(':');
|
|
51
|
+
return sliceName && (prop === 'extension' || prop === 'modifierExtension');
|
|
52
|
+
});
|
|
53
|
+
if (slicedAncestors.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
const hops = [];
|
|
56
|
+
for (let i = 0; i < containerSegments.length; i++) {
|
|
57
|
+
const [prop, sliceName] = containerSegments[i].split(':');
|
|
58
|
+
let url;
|
|
59
|
+
if (sliceName) {
|
|
60
|
+
const ancestorId = containerSegments.slice(0, i + 1).join('.');
|
|
61
|
+
const ancestor = fields.find(f => (f.elementId || '').replace(/^[^.]+\./, '') === ancestorId);
|
|
62
|
+
// Without a resolvable url the hop cannot be scoped, and an unscoped walk is
|
|
63
|
+
// what this function exists to avoid — bail rather than guess.
|
|
64
|
+
if (!ancestor)
|
|
65
|
+
return null;
|
|
66
|
+
url = instanceUrlForSlice(ancestor, fields);
|
|
67
|
+
if (!url)
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
hops.push({ prop, url });
|
|
71
|
+
}
|
|
72
|
+
return hops;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Render a scope walk as nested for-loops, innermost body supplied by the caller.
|
|
76
|
+
*
|
|
77
|
+
* Unfiltered hops go through `elementsAt`, which drops absent and null intermediates
|
|
78
|
+
* and flattens arrays. Reading a single element into a one-item list instead would
|
|
79
|
+
* carry `undefined` into the body, where a `length < min` assertion fires on it.
|
|
80
|
+
*/
|
|
81
|
+
export function renderScopeHops(hops, body) {
|
|
82
|
+
let code = '';
|
|
83
|
+
let indent = ' ';
|
|
84
|
+
let current = 'resource';
|
|
85
|
+
hops.forEach((hop, i) => {
|
|
86
|
+
const next = `_scope${i}`;
|
|
87
|
+
const list = hop.url
|
|
88
|
+
? `elements(at(${current}, '${hop.prop}')).filter((e) => e.url === "${hop.url}")`
|
|
89
|
+
: `elementsAt(${current}, ['${hop.prop}'])`;
|
|
90
|
+
code += `\n${indent}for (const ${next} of ${list}) {`;
|
|
91
|
+
indent += ' ';
|
|
92
|
+
current = next;
|
|
93
|
+
});
|
|
94
|
+
code += body(current, indent);
|
|
95
|
+
for (let i = hops.length - 1; i >= 0; i--) {
|
|
96
|
+
indent = indent.slice(2);
|
|
97
|
+
code += `\n${indent}}`;
|
|
98
|
+
}
|
|
99
|
+
return code;
|
|
100
|
+
}
|