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.
Files changed (38) hide show
  1. package/out/src/cli/updateCommand.js +19 -12
  2. package/out/src/generator/core/constants.js +19 -0
  3. package/out/src/generator/core/extensionSdReader.js +99 -0
  4. package/out/src/generator/core/utils.js +97 -3
  5. package/out/src/generator/emitters/class/classTemplate.js +8 -2
  6. package/out/src/generator/emitters/class/sliceElementDefaults.js +5 -3
  7. package/out/src/generator/emitters/client/clientGenerator.js +22 -6
  8. package/out/src/generator/emitters/client/smartAuthGenerator.js +4 -1
  9. package/out/src/generator/emitters/interface/backboneSliceTyping.js +15 -4
  10. package/out/src/generator/emitters/interface/importManager.js +28 -2
  11. package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +21 -16
  12. package/out/src/generator/emitters/interface/interfaceFieldUtils.js +16 -0
  13. package/out/src/generator/emitters/interface/postProcessExtensions.js +1 -0
  14. package/out/src/generator/emitters/validator/closedSlicingValidation.js +146 -0
  15. package/out/src/generator/emitters/validator/sliceDelegation.js +450 -0
  16. package/out/src/generator/emitters/validator/sliceExtensionValidation.js +200 -35
  17. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +130 -299
  18. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +89 -16
  19. package/out/src/generator/emitters/validator/subExtensionScope.js +100 -0
  20. package/out/src/generator/emitters/validator/validatorBindingBuilder.js +113 -237
  21. package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +13 -4
  22. package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +104 -63
  23. package/out/src/generator/emitters/validator/validatorExpressions.js +54 -0
  24. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +492 -46
  25. package/out/src/generator/emitters/validator/validatorGenerator.js +170 -169
  26. package/out/src/generator/emitters/validator/validatorRuntime.js +279 -0
  27. package/out/src/generator/emitters/validator/validatorTemplates.js +186 -54
  28. package/out/src/generator/emitters/zod/zodRefinementBuilder.js +29 -19
  29. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +16 -1
  30. package/out/src/generator/index.js +21 -11
  31. package/out/src/generator/parser/packageParser.js +1 -8
  32. package/out/src/generator/parser/sdParser.js +1 -0
  33. package/out/src/generator/sdProcessor.js +303 -30
  34. package/out/src/generator/sdProcessorHelpers.js +1 -1
  35. package/out/src/generator/sdProcessorRequiredFields.js +120 -0
  36. package/out/src/main.js +37 -11
  37. package/package.json +3 -1
  38. 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
- * Collect direct children of a slice that have finite max cardinality constraints.
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
- // Strip FHIR slice discriminator (e.g. "extension:time" → "extension") for JS property access
147
- let jsProp = relChild.includes(':') ? relChild.split(':')[0] : relChild;
148
- const isChoiceType = jsProp.endsWith('[x]');
149
- if (isChoiceType)
150
- jsProp = jsProp.replace(/\[x\]$/, '');
151
- result.push({ childProp: jsProp, fhirPath: relChild, maxVal: f.max, isArray: !!f.isArray, isChoiceType });
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 { childProp, fhirPath, maxVal, isArray, isChoiceType } of children) {
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(_wsElem)).some(k => k.startsWith("${childProp}"))) {
167
- errors.push("${resType}.${relPath}:${sliceLabel}.${fhirPath}: max allowed = 0, but found 1");
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, '${childProp}');
248
+ const _wsVal = at(_wsElem, ${accessArgs});
176
249
  if (_wsVal !== undefined && _wsVal !== null) {
177
- errors.push("${resType}.${relPath}:${sliceLabel}.${fhirPath}: max allowed = 0, but found " + (Array.isArray(_wsVal) ? _wsVal.length : 1));
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, '${childProp}');
258
+ const _wsArr = at(_wsElem, ${accessArgs});
186
259
  if (Array.isArray(_wsArr) && _wsArr.length > ${maxVal}) {
187
- errors.push("${resType}.${relPath}:${sliceLabel}.${fhirPath}: max allowed = ${maxVal}, but found " + _wsArr.length);
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
+ }