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
@@ -174,7 +174,19 @@ export function buildForbiddenFieldConstraintEntries(fields) {
174
174
  human: `${concretePath}: max allowed = 0, but found 1`,
175
175
  severity: 'error',
176
176
  },
177
- fieldPath: concreteParts[concreteParts.length - 1],
177
+ // The full path, not the last segment. The expression above is written
178
+ // relative to the resource root, and the emitter wraps an expression in
179
+ // `${fieldPath}.all(...)` unless the expression already navigates from
180
+ // fieldPath. With a bare last segment it never did, so a max=0 on
181
+ // Medication.amount.numerator.value became
182
+ //
183
+ // value.all(amount.numerator.value.exists().not())
184
+ //
185
+ // and Medication has no `value` element: the collection is empty, FHIRPath
186
+ // all() on empty is true, and the constraint could never fire. Two of the
187
+ // three kbv-erp random-parity failures were this, with HL7 reporting the
188
+ // violation we had already written down and then scoped into oblivion.
189
+ fieldPath: concretePath,
178
190
  isChoiceType: false,
179
191
  });
180
192
  });
@@ -262,95 +274,123 @@ function emitNestedCodingPattern(output, fields, pathParts, parentFieldName, pat
262
274
  (f.name || '').replace(/^[^.]+\./, '').split(':')[0] === parentBasePath);
263
275
  if (parentIsSliced)
264
276
  return;
265
- const system = 'system' in pattern && pattern.system ? String(pattern.system) : null;
266
- const code = String(pattern.code);
267
- if (system) {
268
- output.push(`
269
- // Pattern constraint for ${humanRel}: every ${parentPath} element must include a coding matching pattern
277
+ // Same split as the CodeableConcept case: identity here, pinned details below.
278
+ const match = codingIdentityPredicate(pattern);
279
+ const described = codingPatternDescription(pattern, true);
280
+ output.push(`
281
+ // Pattern constraint for ${humanRel}: every ${parentPath} element must include a coding matching ${described}
270
282
  if (resource.${parentPath} && Array.isArray(resource.${parentPath})) {
271
283
  for (const _item of resource.${parentPath}) {
272
- const _hasMatch = _item?.coding?.some((coding) => (coding?.system as string) === "${system}" && (coding?.code as string) === "${code}");
284
+ const _hasMatch = codings(_item).some((coding) => ${match});
273
285
  if (!_hasMatch) {
274
- errors.push("Pattern constraint violation: ${humanRel} requires system='${system}' and code='${code}'");
286
+ errors.push("Pattern constraint violation: ${humanRel} requires ${described}");
275
287
  }
276
288
  }
277
289
  }`);
278
- }
279
- else {
290
+ emitPinnedDetailChecks(output, `resource.${parentPath}`, pattern, parentPath.replace(/[^a-zA-Z0-9_]/g, '_'));
291
+ }
292
+ /**
293
+ * Coding sub-elements a pattern can pin, in the order they read best in a message.
294
+ *
295
+ * `display` is deliberately absent: HL7 does not fail a pattern on a differing
296
+ * display, and enforcing it would make us the stricter outlier.
297
+ */
298
+ const PATTERN_CODING_PARTS = ['system', 'version', 'code'];
299
+ /** The sub-elements this pattern actually pins, in message order. */
300
+ function pinnedParts(coding) {
301
+ return PATTERN_CODING_PARTS.filter(part => coding[part] !== undefined && coding[part] !== null && coding[part] !== '');
302
+ }
303
+ /**
304
+ * Predicate matching only the parts that *identify* which coding the pattern is
305
+ * talking about, so a coding that is clearly the intended one but missing a pinned
306
+ * detail can be reported against that detail.
307
+ */
308
+ function codingIdentityPredicate(coding, receiver = 'coding') {
309
+ const identity = pinnedParts(coding).filter(part => part === 'system' || part === 'code');
310
+ const comparisons = identity.map(part => `(${receiver}?.${part} as string) === "${String(coding[part])}"`);
311
+ return comparisons.length > 0 ? comparisons.join(' && ') : 'true';
312
+ }
313
+ /**
314
+ * Per-sub-element checks in HL7's own wording, for parts the pattern pins beyond
315
+ * system and code (in practice `version`).
316
+ *
317
+ * Reporting these separately is what makes the finding comparable: HL7 locates the
318
+ * intended coding and blames the specific absent sub-element —
319
+ * `Missing element 'version' - required by fixed value assigned in profile`, at
320
+ * location `ExplanationOfBenefit.type.coding.version`. A single message naming the
321
+ * parent element instead classifies as a `type` finding against HL7's `version`
322
+ * one, so the two never line up even though both validators agree the resource is
323
+ * wrong — which is precisely what left carin-bb at 7/15 after the comparison
324
+ * itself was fixed.
325
+ */
326
+ function emitPinnedDetailChecks(output, accessor, coding, varName) {
327
+ const details = pinnedParts(coding).filter(part => part !== 'system' && part !== 'code');
328
+ if (details.length === 0)
329
+ return;
330
+ const identity = codingIdentityPredicate(coding, 'c');
331
+ details.forEach((part, i) => {
332
+ const found = `_${varName}_${part}${i}`;
280
333
  output.push(`
281
- // Pattern constraint for ${humanRel}: every ${parentPath} element must include a coding matching pattern
282
- if (resource.${parentPath} && Array.isArray(resource.${parentPath})) {
283
- for (const _item of resource.${parentPath}) {
284
- const _hasMatch = _item?.coding?.some((coding) => (coding?.code as string) === "${code}");
285
- if (!_hasMatch) {
286
- errors.push("Pattern constraint violation: ${humanRel} requires code='${code}'");
287
- }
334
+ // Pattern on ${accessor} pins ${part}; reported the way HL7 does when it is absent
335
+ {
336
+ const ${found} = codings(${accessor}).find((c) => ${identity});
337
+ if (${found} && (${found}.${part} as string) !== "${String(coding[part])}") {
338
+ errors.push("Missing element '${part}' - required by fixed value assigned in profile");
288
339
  }
289
340
  }`);
290
- }
341
+ });
342
+ }
343
+ /**
344
+ * Human-readable form of the pattern, for the error message.
345
+ *
346
+ * `identityOnly` keeps it to the parts the headline check compares, so the message
347
+ * does not promise a comparison the emitted code no longer makes there.
348
+ */
349
+ function codingPatternDescription(coding, identityOnly = false) {
350
+ return pinnedParts(coding)
351
+ .filter(part => !identityOnly || part === 'system' || part === 'code')
352
+ .map(part => `${part}='${String(coding[part])}'`)
353
+ .join(' and ');
291
354
  }
292
355
  function emitCodeableConceptPattern(output, field, rel, humanRel, pattern, fieldPathMap, _arrayFieldPaths) {
293
356
  const requiredCodings = pattern.coding.filter((c) => c !== null && typeof c === 'object' && 'code' in c);
294
357
  if (requiredCodings.length === 0)
295
358
  return;
296
359
  requiredCodings.forEach((requiredCoding, idx) => {
297
- const system = requiredCoding.system ? String(requiredCoding.system) : null;
298
- const code = String(requiredCoding.code);
360
+ // The headline check asks only whether the *intended* coding is present, so it
361
+ // stays a `type` finding. Sub-elements the pattern additionally pins are
362
+ // reported separately by emitPinnedDetailChecks, which is how HL7 splits it —
363
+ // otherwise both fire and we report a `type` finding HL7 never raises.
364
+ const match = codingIdentityPredicate(requiredCoding);
365
+ const described = codingPatternDescription(requiredCoding, true);
299
366
  const varName = rel.replace(/[^a-zA-Z0-9_]/g, '_') + `_pattern${idx}`;
300
367
  const pathInfo = fieldPathMap.get(rel);
301
368
  const isResourceFieldArray = field.isArray || (field.sliceName && pathInfo?.isNestedInArray !== true) || rel.includes('.');
302
369
  if (isResourceFieldArray) {
303
- if (system) {
304
- output.push(`
305
- // Pattern constraint for ${humanRel}: at least one element must include coding with system="${system}" and code="${code}"
306
- if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
307
- const ${varName}Valid = resource.${rel}.some((item) =>
308
- item?.coding?.some((coding) => (coding?.system as string) === "${system}" && (coding?.code as string) === "${code}")
309
- );
310
- if (!${varName}Valid) {
311
- errors.push("${humanRel} must include at least one element with a coding with system '${system}' and code '${code}'");
312
- }
313
- }`);
314
- }
315
- else {
316
- output.push(`
317
- // Pattern constraint for ${humanRel}: at least one element must include coding with code="${code}"
370
+ output.push(`
371
+ // Pattern constraint for ${humanRel}: at least one element must include a coding matching ${described}
318
372
  if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
319
- const ${varName}Valid = resource.${rel}.some((item) =>
320
- item?.coding?.some((coding) => (coding?.code as string) === "${code}")
373
+ const ${varName}Valid = elements(resource.${rel}).some((item) =>
374
+ codings(item).some((coding) => ${match})
321
375
  );
322
376
  if (!${varName}Valid) {
323
- errors.push("${humanRel} must include at least one element with a coding with code '${code}'");
377
+ errors.push("${humanRel} must include at least one element with a coding with ${described}");
324
378
  }
325
379
  }`);
326
- }
327
380
  }
328
381
  else {
329
- if (system) {
330
- output.push(`
331
- // Pattern constraint for ${humanRel}: must include coding with system="${system}" and code="${code}"
332
- if (resource.${rel}) {
333
- const ${varName}Valid = resource.${rel}.coding?.some((coding) =>
334
- (coding?.system as string) === "${system}" && (coding?.code as string) === "${code}"
335
- );
336
- if (!${varName}Valid) {
337
- errors.push("${humanRel} must include a coding with system '${system}' and code '${code}'");
338
- }
339
- }`);
340
- }
341
- else {
342
- output.push(`
343
- // Pattern constraint for ${humanRel}: must include coding with code="${code}"
382
+ output.push(`
383
+ // Pattern constraint for ${humanRel}: must include a coding matching ${described}
344
384
  if (resource.${rel}) {
345
- const ${varName}Valid = resource.${rel}.coding?.some((coding) =>
346
- (coding?.code as string) === "${code}"
347
- );
385
+ const ${varName}Valid = codings(resource.${rel}).some((coding) => ${match});
348
386
  if (!${varName}Valid) {
349
- errors.push("${humanRel} must include a coding with code '${code}'");
387
+ errors.push("${humanRel} must include a coding with ${described}");
350
388
  }
351
389
  }`);
352
- }
353
390
  }
391
+ // Blame the specific pinned sub-element when the intended coding is present
392
+ // but incomplete, matching how HL7 reports it.
393
+ emitPinnedDetailChecks(output, `resource.${rel}`, requiredCoding, varName);
354
394
  });
355
395
  }
356
396
  /**
@@ -396,8 +436,9 @@ export function buildChoiceTypeNarrowingValidations(fields, profileUrl) {
396
436
  // Choice-type narrowing: ${relPath} allows only: ${allowedTypesStr}
397
437
  {
398
438
  const _ckAllowed = [${allowedKeys.join(', ')}];
399
- for (const _ckKey of Object.keys(_bRes)) {
400
- if (_ckKey.startsWith('${basePropName}') && _ckKey.length > ${baseLen} && _ckKey.charCodeAt(${baseLen}) >= 65 && _ckKey.charCodeAt(${baseLen}) <= 90 && !_ckAllowed.includes(_ckKey) && _bRes[_ckKey] !== undefined) {
439
+ const _ckNode = node(resource);
440
+ for (const _ckKey of Object.keys(_ckNode)) {
441
+ if (_ckKey.startsWith('${basePropName}') && _ckKey.length > ${baseLen} && _ckKey.charCodeAt(${baseLen}) >= 65 && _ckKey.charCodeAt(${baseLen}) <= 90 && !_ckAllowed.includes(_ckKey) && _ckNode[_ckKey] !== undefined) {
401
442
  errors.push("The Profile '${escapedUrl}' definition allows for the type ${allowedTypesStr} but found type " + _ckKey.substring(${baseLen}));
402
443
  }
403
444
  }
@@ -413,7 +454,7 @@ function emitCodingPattern(output, field, rel, humanRel, pattern) {
413
454
  output.push(`
414
455
  // Pattern constraint for ${humanRel}: at least one element must have system="${system}" and code="${code}"
415
456
  if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
416
- const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = resource.${rel}.some((item) =>
457
+ const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = elements(resource.${rel}).some((item) =>
417
458
  (item?.system as string) === "${system}" && (item?.code as string) === "${code}"
418
459
  );
419
460
  if (!${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid) {
@@ -425,7 +466,7 @@ function emitCodingPattern(output, field, rel, humanRel, pattern) {
425
466
  output.push(`
426
467
  // Pattern constraint for ${humanRel}: at least one element must have code="${code}"
427
468
  if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
428
- const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = resource.${rel}.some((item) =>
469
+ const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = elements(resource.${rel}).some((item) =>
429
470
  (item?.code as string) === "${code}"
430
471
  );
431
472
  if (!${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid) {
@@ -0,0 +1,54 @@
1
+ /**
2
+ * FHIRPath expression normalisation shared by the constraint emitters.
3
+ *
4
+ * Each function here exists because fhirpath.js holds the spec more strictly than
5
+ * HL7's Java engine does, or because a FHIR-supplied expression is not valid
6
+ * FHIRPath as written.
7
+ */
8
+ const FHIRPATH_KEYWORDS = new Set([
9
+ 'and', 'as', 'contains', 'day', 'days', 'div', 'false', 'hour', 'hours',
10
+ 'implies', 'in', 'is', 'millisecond', 'milliseconds', 'minute', 'minutes',
11
+ 'mod', 'month', 'months', 'not', 'or', 'second', 'seconds', 'true', 'week',
12
+ 'weeks', 'xor', 'year', 'years',
13
+ ]);
14
+ /**
15
+ * Delimit any keyword segment of a FHIRPath navigation path.
16
+ *
17
+ * Scoping a constraint to its field path used to emit the raw path, so a
18
+ * constraint on Narrative.div produced `text.div.all(htmlChecks())`, which
19
+ * fhirpath.js rejects outright ("mismatched input 'div'") and which threw out of
20
+ * validate() at runtime for every profile carrying txt-1.
21
+ */
22
+ export function quoteFhirPathPath(path) {
23
+ return path
24
+ .split('.')
25
+ .map(segment => (FHIRPATH_KEYWORDS.has(segment) ? `\`${segment}\`` : segment))
26
+ .join('.');
27
+ }
28
+ /**
29
+ * Rewrite `as()` applied to a collection into the equivalent `ofType()`.
30
+ *
31
+ * FHIRPath defines `as` on a singleton and `ofType` as the collection filter, so
32
+ * `descendants().as(canonical)` is a spec violation. HL7's Java engine tolerates
33
+ * it, fhirpath.js rejects it outright ("Expected singleton on left side of 'as'")
34
+ * and the throw escaped validate(), losing every finding for the resource — dom-3
35
+ * is written this way. The two forms select the same items, so normalising keeps
36
+ * the invariant evaluated instead of dropped. `as()` applied per item inside
37
+ * `where()` is already a singleton and is left alone.
38
+ */
39
+ export function normalizeCollectionTypeFilters(expression) {
40
+ return expression.replace(/\b(descendants\(\)|children\(\)|repeat\([^()]*\))\s*\.\s*as\s*\(/g, '$1.ofType(');
41
+ }
42
+ /**
43
+ * The two base FHIR element invariants whose evaluation diverges between
44
+ * fhirpath.js and HL7 once they are applied below the top level:
45
+ * ele-1: hasValue() or (children().count() > id.count())
46
+ * ext-1: extension.exists() != value.exists()
47
+ *
48
+ * They are dropped for nested field paths, and kept at a single level for
49
+ * recursive elements, rather than being walked through the whole recursion.
50
+ */
51
+ export function isBaseElementInvariant(expression) {
52
+ return /^hasValue\(\)\s+or\s+\(children\(\)\.count\(\)\s*>\s*id\.count\(\)\)$/.test(expression)
53
+ || /^extension\.exists\(\)\s*!=\s*value\.exists\(\)$/.test(expression);
54
+ }