babelfhir-ts 1.6.11 → 1.6.12

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.
@@ -317,11 +317,16 @@ export function generateEnrichResourceEnd() {
317
317
  }
318
318
  }
319
319
 
320
+ // No display on the RxNorm fallback. 'Amoxicillin 250mg' was paired with
321
+ // 1049502, which RxNorm defines as an oxycodone product, and the HL7 validator
322
+ // answered "Wrong Display Name ... Valid display is one of 5 choices". A Coding
323
+ // without a display is conformant, and an unverifiable display is a liability:
324
+ // when the binding does resolve, the display comes from the expansion instead.
320
325
  // Medication — isik-med-1: code.exists() or ingredient.exists()
321
326
  if (/^Medication$/i.test(baseResource)) {
322
327
  if (!('code' in base) && !('ingredient' in base)) {
323
- const resolved = resolveBindingCode('code', '${TS.RXNORM}', '1049502', 'Amoxicillin 250mg');
324
- base.code = resolved || { coding: [{ system: '${TS.RXNORM}', code: '1049502', display: 'Amoxicillin 250mg' }] };
328
+ const resolved = resolveBindingCode('code', '${TS.RXNORM}', '1049502');
329
+ base.code = resolved || { coding: [{ system: '${TS.RXNORM}', code: '1049502' }] };
325
330
  }
326
331
  }
327
332
 
@@ -330,8 +335,8 @@ export function generateEnrichResourceEnd() {
330
335
  if (!('status' in base)) base.status = 'active';
331
336
  if (!('intent' in base)) base.intent = 'order';
332
337
  if (!('medicationCodeableConcept' in base) && !('medicationReference' in base)) {
333
- const resolved = resolveBindingCode('medicationCodeableConcept', '${TS.RXNORM}', '1049502', 'Amoxicillin 250mg');
334
- base.medicationCodeableConcept = resolved || { coding: [{ system: '${TS.RXNORM}', code: '1049502', display: 'Amoxicillin 250mg' }] };
338
+ const resolved = resolveBindingCode('medicationCodeableConcept', '${TS.RXNORM}', '1049502');
339
+ base.medicationCodeableConcept = resolved || { coding: [{ system: '${TS.RXNORM}', code: '1049502' }] };
335
340
  }
336
341
  if (!('subject' in base)) base.subject = { reference: 'Patient/' + randomId() };
337
342
  // requester required when intent='order' (us-core-21 constraint)
@@ -342,8 +347,8 @@ export function generateEnrichResourceEnd() {
342
347
  if (/MedicationDispense/i.test(baseResource)) {
343
348
  if (!('status' in base)) base.status = 'completed';
344
349
  if (!('medicationCodeableConcept' in base) && !('medicationReference' in base)) {
345
- const resolved = resolveBindingCode('medicationCodeableConcept', '${TS.RXNORM}', '1049502', 'Amoxicillin 250mg');
346
- base.medicationCodeableConcept = resolved || { coding: [{ system: '${TS.RXNORM}', code: '1049502', display: 'Amoxicillin 250mg' }] };
350
+ const resolved = resolveBindingCode('medicationCodeableConcept', '${TS.RXNORM}', '1049502');
351
+ base.medicationCodeableConcept = resolved || { coding: [{ system: '${TS.RXNORM}', code: '1049502' }] };
347
352
  }
348
353
  if (!('subject' in base)) base.subject = { reference: 'Patient/' + randomId() };
349
354
  if (!('performer' in base)) base.performer = [{ actor: { reference: 'Practitioner/' + randomId() } }];
@@ -4,6 +4,7 @@
4
4
  * Extracted helpers live in sliceElementDefaults.ts.
5
5
  */
6
6
  import { logger } from '../../../logger.js';
7
+ import { ctx } from '../../fhir/versionContext.js';
7
8
  import { capitalise, deriveIdentifierConfig } from './classHeuristics.js';
8
9
  import { TS, EXAMPLE_ID_SYSTEM, RESOURCE_SKELETONS, compositionSkeleton, bundleEntry, matchSkeletonBySliceName, } from '../../core/constants.js';
9
10
  import { getDefaultValueForType, getTypePlaceholder, objectToCodeExpression, } from './sliceElementDefaults.js';
@@ -260,9 +261,14 @@ export function generateSliceElement(slice, elementType) {
260
261
  }
261
262
  const STRUCTURAL_BACKBONE_FIELDS = new Set(['extension', 'modifierExtension', 'part', 'contains']);
262
263
  const rawType = child.type;
263
- const effectiveType = (!rawType || rawType === 'any') && STRUCTURAL_BACKBONE_FIELDS.has(childPath)
264
+ // A child whose own type the differential does not restate still has one in
265
+ // the base datatype. Falling back to 'string' gave Identifier.system, a uri,
266
+ // the literal "placeholder", and the HL7 validator rightly answered
267
+ // "Identifier.system must be an absolute reference, not a local reference".
268
+ const declaredType = rawType || ctx().dataTypes[elementType]?.[childPath]?.type;
269
+ const effectiveType = (!declaredType || declaredType === 'any') && STRUCTURAL_BACKBONE_FIELDS.has(childPath)
264
270
  ? 'BackboneElement'
265
- : (rawType || 'string');
271
+ : (declaredType || 'string');
266
272
  const placeholder = getTypePlaceholder(effectiveType);
267
273
  if (placeholder !== undefined) {
268
274
  if (!child.isRequired && (placeholder === '{ }' || placeholder === '{}')) {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Invariants declared on an Extension's own StructureDefinition, evaluated against
3
+ * each matched extension in the resource that carries it.
4
+ *
5
+ * The extension profile's own validator already emits these, but that one runs only
6
+ * when an Extension is validated standalone. A resource embedding the extension
7
+ * checked that the slice was present and that its value[x] was permitted, and never
8
+ * evaluated the extension's constraints. KBV_EX_ERP_Multiple_Prescription is the
9
+ * clear case: "if the flag is true a period must be given" is an invariant on the
10
+ * extension SD, and the HL7 validator reports it on the containing MedicationRequest
11
+ * where we reported nothing.
12
+ *
13
+ * Split out of sliceExtensionValidation.ts to keep that file under the 650 LOC gate.
14
+ */
15
+ /**
16
+ * Emit the invariant checks for one matched-extension list.
17
+ *
18
+ * Returns '' when the extension declares nothing worth checking, so callers can
19
+ * concatenate unconditionally.
20
+ */
21
+ export function generateExtensionInvariantChecks(matchedArrayVar, extensionUrl, extMeta, ctx, indent) {
22
+ const constraints = extMeta?.constraints;
23
+ if (!constraints || constraints.length === 0)
24
+ return '';
25
+ // The fhirpath import is gated on the profile's own constraint list, so a profile
26
+ // that declares none would reference an undeclared `fhirpath` binding here.
27
+ if (ctx?.fhirpathNeeded)
28
+ ctx.fhirpathNeeded.value = true;
29
+ const lines = [];
30
+ lines.push(`${indent}// Invariants declared on extension ${extensionUrl}`);
31
+ lines.push(`${indent}for (const _invExt of ${matchedArrayVar}) {`);
32
+ for (const c of constraints) {
33
+ // Evaluated against the extension element rather than the resource: the
34
+ // expressions are written against Extension, exactly as the extension's own
35
+ // validator evaluates them.
36
+ lines.push(`${indent} const _inv${c.key.replace(/[^A-Za-z0-9]/g, '_')} = await fhirpath.evaluate(`
37
+ + `node(_invExt), ${JSON.stringify(c.expression)}, `
38
+ + `{ resource, rootResource: resource }, fhirpath_model, fhirpathOptions);`);
39
+ lines.push(`${indent} if (!_inv${c.key.replace(/[^A-Za-z0-9]/g, '_')}.every(Boolean)) {`);
40
+ lines.push(`${indent} errors.push(${JSON.stringify(`Constraint violation: ${c.human}`)});`);
41
+ lines.push(`${indent} }`);
42
+ }
43
+ lines.push(`${indent}}`);
44
+ return '\n' + lines.join('\n');
45
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Extension and ValueSet slice validation strategies.
3
3
  */
4
+ import { generateExtensionInvariantChecks } from './extensionInvariants.js';
4
5
  import { safeAccessor, safeList, sliceElseBranch, detectNestedArray, findArrayAncestor, isArrayPath, FHIR_PRIMITIVE_STRING_TYPES } from './sliceValidatorUtils.js';
5
6
  import { stripVersionFromCanonicalUrl, stripSliceSuffix } from '../../core/utils.js';
6
7
  import { buildSubExtensionScope, renderScopeHops } from './subExtensionScope.js';
@@ -517,7 +518,7 @@ ${indent} if (_forbidden) {
517
518
  ${indent} errors.push("Extension.value[x]: max allowed = 0, but found 1 (${forbidUrl})");
518
519
  ${indent} errors.push("The Extension '${extensionUrl}' definition allows for the types [] but found type " + _forbidden.slice(5));
519
520
  ${indent} }${generateSubExtensionChecks(extMeta, ctx, indent + ' ')}
520
- ${indent}}`;
521
+ ${indent}}${generateExtensionInvariantChecks(matchedArrayVar, extensionUrl, extMeta, ctx, indent)}`;
521
522
  }
522
523
  const lines = [];
523
524
  const allowedProps = extMeta.valueTypes.map(t => `"value${valuePropertySuffix(t)}"`);
@@ -575,5 +576,6 @@ ${indent}}`;
575
576
  }
576
577
  }
577
578
  lines.push(`${indent}}`);
578
- return '\n' + lines.join('\n');
579
+ return '\n' + lines.join('\n')
580
+ + generateExtensionInvariantChecks(matchedArrayVar, extensionUrl, extMeta, ctx, indent);
579
581
  }
@@ -216,10 +216,15 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
216
216
  isChoiceType: false,
217
217
  });
218
218
  });
219
+ // Slice code can need fhirpath on its own: an extension's invariants are evaluated
220
+ // against each matched extension, and the profile carrying it need not declare a
221
+ // constraint of its own.
222
+ const fhirpathNeeded = { value: false };
219
223
  const sliceValidations = generateSliceValidations({
220
224
  fields, fieldPathMap, arrayFieldPaths, getValueSetForBinding,
221
225
  valueSetImports, profileUrl, profileName: interfaceName, baseResourceType,
222
226
  profileUrlToName, validatorImports, extensionMetadata, profileUrlToType, datatypeProfilePins,
227
+ fhirpathNeeded,
223
228
  });
224
229
  const closedSlicingValidations = generateClosedSlicingValidations(fields, profileUrl, baseResourceType, profileUrlToType);
225
230
  const primitiveFormatValidations = buildPrimitiveFormatValidations(fields);
@@ -491,7 +496,7 @@ ${body}
491
496
  const expr = c.constraint.expression || '';
492
497
  return /\bmemberOf\b/.test(expr) || /\bresolve\b/.test(expr);
493
498
  });
494
- const fhirpathImport = filteredConstraints.length > 0
499
+ const fhirpathImport = filteredConstraints.length > 0 || fhirpathNeeded.value
495
500
  ? `import fhirpath from "fhirpath";\nimport fhirpath_model from "fhirpath/fhir-context/${fhirpathModelSlug()}/index.js";\n`
496
501
  : '';
497
502
  // `at`/`arr` are value imports: they replace the `as any` element probing that
@@ -21,6 +21,7 @@ import { ctx as versionCtx, fhirModuleSpecifier } from './fhir/versionContext.js
21
21
  import { registerProfile, addDiagnostic } from './core/sdDiagnostics.js';
22
22
  import { resetFetchFailureTracking, getFetchFailureCount, getFetchFailureWarning, recordFetchFailure, resolveComplexTypeChildType, ensureRandomSupportFile, ensureValidatorOptionsFile, } from './sdProcessorHelpers.js';
23
23
  import { extractAndDeduplicateRequiredFields, resolveChoiceSegments } from './sdProcessorRequiredFields.js';
24
+ import { isBaseElementInvariant } from './emitters/validator/validatorExpressions.js';
24
25
  // Re-export public API consumed by index.ts / generationHelpers.ts
25
26
  export { resetFetchFailureTracking, getFetchFailureCount, getFetchFailureWarning };
26
27
  const log = logger.withTag('generator');
@@ -194,6 +195,52 @@ async function fillSliceProfileUrlsFromBaseProfiles(fields, baseDefinition) {
194
195
  * DTRStdQuestionnaire's contextExpression emitted no validation at all.
195
196
  * Callers must strip before reading; use `lookupExtensionMetadata`.
196
197
  */
198
+ /**
199
+ * Invariants on an Extension SD, minus the ones the emitted validator handles
200
+ * elsewhere or evaluates differently from HL7.
201
+ *
202
+ * ext-1 is already checked structurally by the extension slice code, and ele-1
203
+ * diverges between fhirpath.js and HL7, so both are dropped here for the same
204
+ * reasons validatorGenerator drops them from a profile's own constraint list.
205
+ */
206
+ function readExtensionConstraints(extSd) {
207
+ const els = extensionElements(extSd);
208
+ if (!els)
209
+ return undefined;
210
+ const out = [];
211
+ const seen = new Set();
212
+ for (const el of els) {
213
+ // Only invariants on the Extension root apply to the extension as a whole.
214
+ const path = el.path;
215
+ if (path !== 'Extension')
216
+ continue;
217
+ for (const c of (el.constraint ?? [])) {
218
+ const { key, human, expression, severity } = c;
219
+ if (!key || !human || !expression)
220
+ continue;
221
+ if (severity && severity !== 'error')
222
+ continue;
223
+ // By key first, because these are restated with local variations that defeat
224
+ // matching on the expression: the subscriptions-backport extensions write ele-1
225
+ // as `hasValue() or (children().count() > id.count()) or $this is Parameters`,
226
+ // and isBaseElementInvariant anchors its pattern, so the suffix slipped through
227
+ // and every backport extension reported ele-1 on its Subscription.
228
+ if (key === 'ele-1' || key === 'ext-1')
229
+ continue;
230
+ if (isBaseElementInvariant(expression))
231
+ continue;
232
+ if (/text\.div/.test(expression))
233
+ continue;
234
+ if (/\$[A-Z]/.test(expression))
235
+ continue;
236
+ if (seen.has(key))
237
+ continue;
238
+ seen.add(key);
239
+ out.push({ key, human, expression });
240
+ }
241
+ }
242
+ return out.length > 0 ? out : undefined;
243
+ }
197
244
  async function resolveExtensionMetadata(fields) {
198
245
  const extUrls = new Set();
199
246
  for (const f of fields) {
@@ -220,6 +267,10 @@ async function resolveExtensionMetadata(fields) {
220
267
  const valueEl = els.find(el => el.path === 'Extension.value[x]' || el.id === 'Extension.value[x]');
221
268
  const subExtensions = collectSubExtensionSlices(els);
222
269
  const subExtensionMin = readSubExtensionMin(els);
270
+ // Read before the branching below: a complex extension returns early, and its
271
+ // invariants are exactly the ones worth carrying (Mehrfachverordnung's
272
+ // "if the flag is true a period is required" lives on such an SD).
273
+ const constraints = readExtensionConstraints(extSd);
223
274
  // A complex extension carries sub-extensions instead of a value, and forbids a
224
275
  // value outright (ext-1: extension.exists() != value.exists()). Recorded as an
225
276
  // empty type list — different from no entry at all, which would mean the SD
@@ -235,6 +286,7 @@ async function resolveExtensionMetadata(fields) {
235
286
  valueTypes: [],
236
287
  subExtensionMin,
237
288
  subExtensions: subExtensions.length ? subExtensions : undefined,
289
+ constraints,
238
290
  });
239
291
  return;
240
292
  }
@@ -243,7 +295,7 @@ async function resolveExtensionMetadata(fields) {
243
295
  const types = (valueEl.type || []).map((t) => t.code).filter(Boolean);
244
296
  if (types.length === 0)
245
297
  return;
246
- const entry = { valueTypes: types };
298
+ const entry = { valueTypes: types, constraints };
247
299
  // Check binding on value[x] first, then fall back to typed slices
248
300
  // (e.g., Extension.valueCode may carry the binding instead of Extension.value[x])
249
301
  let binding = readBinding(valueEl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.6.11",
3
+ "version": "1.6.12",
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",