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
@@ -40,16 +40,19 @@ export function buildRefinements(allFields, rootPrefix, valueSets) {
40
40
  for (const coding of requiredCodings) {
41
41
  const system = coding.system ? String(coding.system) : undefined;
42
42
  const code = String(coding.code);
43
- const accessor = field.isArray
44
- ? `(d as any).${rel}`
45
- : `[(d as any).${rel}]`;
43
+ // arr() flattens the array case and [] wraps the scalar case, so both
44
+ // reduce to "some member carries a matching coding". codings() reads the
45
+ // CodeableConcept's .coding and yields typed CodingShapes.
46
+ const members = field.isArray
47
+ ? `arr(at(d, '${rel}'))`
48
+ : `[at(d, '${rel}')]`;
46
49
  const matchExpr = system
47
- ? `(c as any).system === "${system}" && (c as any).code === "${code}"`
48
- : `(c as any).code === "${code}"`;
50
+ ? `c.system === "${system}" && c.code === "${code}"`
51
+ : `c.code === "${code}"`;
49
52
  const msg = system
50
53
  ? `${rel} must include coding ${system}|${code}`
51
54
  : `${rel} must include coding ${code}`;
52
- refinements.push(`.refine(d => !(d as any).${rel} || ${accessor}.flat().some((item: any) => item?.coding?.some((c: any) => ${matchExpr})), { message: "${escStr(msg)}" })`);
55
+ refinements.push(`.refine(d => !at(d, '${rel}') || ${members}.some((item) => codings(item).some((c) => ${matchExpr})), { message: "${escStr(msg)}" })`);
53
56
  }
54
57
  }
55
58
  // patternCoding
@@ -57,10 +60,11 @@ export function buildRefinements(allFields, rootPrefix, valueSets) {
57
60
  const system = String(pattern.system);
58
61
  const code = String(pattern.code);
59
62
  if (field.isArray) {
60
- refinements.push(`.refine(d => !(d as any).${rel} || (d as any).${rel}.some((c: any) => c.system === "${system}" && c.code === "${code}"), { message: "${escStr(rel)} must include coding ${system}|${code}" })`);
63
+ // An array of Codings passes straight through codings() as CodingShapes.
64
+ refinements.push(`.refine(d => !at(d, '${rel}') || codings(at(d, '${rel}')).some((c) => c.system === "${system}" && c.code === "${code}"), { message: "${escStr(rel)} must include coding ${system}|${code}" })`);
61
65
  }
62
66
  else {
63
- refinements.push(`.refine(d => !(d as any).${rel} || ((d as any).${rel}.system === "${system}" && (d as any).${rel}.code === "${code}"), { message: "${escStr(rel)} must match coding ${system}|${code}" })`);
67
+ refinements.push(`.refine(d => { const _c = coding(at(d, '${rel}')); return !at(d, '${rel}') || (_c.system === "${system}" && _c.code === "${code}"); }, { message: "${escStr(rel)} must match coding ${system}|${code}" })`);
64
68
  }
65
69
  }
66
70
  }
@@ -95,23 +99,26 @@ export function buildRefinements(allFields, rootPrefix, valueSets) {
95
99
  const checks = codings
96
100
  .map(c => {
97
101
  return c.system
98
- ? `(item as any)?.coding?.some((cd: any) => cd.system === "${c.system}" && cd.code === "${c.code}")`
99
- : `(item as any)?.coding?.some((cd: any) => cd.code === "${c.code}")`;
102
+ ? `codings(item).some((cd) => cd.system === "${c.system}" && cd.code === "${c.code}")`
103
+ : `codings(item).some((cd) => cd.code === "${c.code}")`;
100
104
  })
101
105
  .join(' || ');
102
- refinements.push(`.refine(d => !Array.isArray((d as any).${relPath}) || (d as any).${relPath}.filter((item: any) => ${checks}).length >= ${min}, { message: "Required slice '${escStr(relPath)}:${escStr(sliceLabel)}' not satisfied", when: () => true })`);
106
+ refinements.push(`.refine(d => !Array.isArray(at(d, '${relPath}')) || arr(at(d, '${relPath}')).filter((item) => ${checks}).length >= ${min}, { message: "Required slice '${escStr(relPath)}:${escStr(sliceLabel)}' not satisfied", when: () => true })`);
103
107
  }
104
108
  }
105
109
  // patternCoding discriminator
106
110
  if (slice.patternConstraint && typeof slice.patternConstraint === 'object' && 'system' in slice.patternConstraint && 'code' in slice.patternConstraint && !('coding' in slice.patternConstraint)) {
107
111
  const pat = slice.patternConstraint;
108
- refinements.push(`.refine(d => !Array.isArray((d as any).${relPath}) || (d as any).${relPath}.filter((c: any) => c.system === "${pat.system}" && c.code === "${pat.code}").length >= ${min}, { message: "Required slice '${escStr(relPath)}:${escStr(sliceLabel)}' not satisfied", when: () => true })`);
112
+ refinements.push(`.refine(d => !Array.isArray(at(d, '${relPath}')) || codings(at(d, '${relPath}')).filter((c) => c.system === "${pat.system}" && c.code === "${pat.code}").length >= ${min}, { message: "Required slice '${escStr(relPath)}:${escStr(sliceLabel)}' not satisfied", when: () => true })`);
109
113
  }
110
114
  // Binding codes discriminator
111
115
  if (!slice.patternConstraint && slice.binding?.codes && slice.binding.codes.length > 0) {
112
116
  const codes = slice.binding.codes.map(c => JSON.stringify(c.code));
113
117
  const codesSet = `[${codes.join(', ')}]`;
114
- refinements.push(`.refine(d => !Array.isArray((d as any).${relPath}) || (d as any).${relPath}.filter((item: any) => ${codesSet}.includes(item?.code ?? item)).length >= ${min}, { message: "Required slice '${escStr(relPath)}:${escStr(sliceLabel)}' not satisfied", when: () => true })`);
118
+ refinements.push(
119
+ // A binding-discriminated slice member is either a coded element or a
120
+ // bare code string, so fall back to the member itself.
121
+ `.refine(d => !Array.isArray(at(d, '${relPath}')) || arr(at(d, '${relPath}')).filter((item) => { const _code = coding(item).code ?? item; return typeof _code === 'string' && ${codesSet}.includes(_code); }).length >= ${min}, { message: "Required slice '${escStr(relPath)}:${escStr(sliceLabel)}' not satisfied", when: () => true })`);
115
122
  }
116
123
  }
117
124
  }
@@ -173,10 +180,10 @@ export function buildRefinements(allFields, rootPrefix, valueSets) {
173
180
  if (min <= 0)
174
181
  continue;
175
182
  if (field.isArray) {
176
- refinements.push(`.refine(d => (d as any).meta?.${subField}?.length >= ${min}, { message: "meta.${subField} must be present", when: () => true })`);
183
+ refinements.push(`.refine(d => arr(at(d, 'meta', '${subField}')).length >= ${min}, { message: "meta.${subField} must be present", when: () => true })`);
177
184
  }
178
185
  else {
179
- refinements.push(`.refine(d => (d as any).meta?.${subField} !== undefined, { message: "meta.${subField} must be present", when: () => true })`);
186
+ refinements.push(`.refine(d => at(d, 'meta', '${subField}') !== undefined, { message: "meta.${subField} must be present", when: () => true })`);
180
187
  }
181
188
  }
182
189
  // 5. Required ValueSet bindings on CodeableConcept / Coding fields
@@ -223,23 +230,26 @@ export function buildRefinements(allFields, rootPrefix, valueSets) {
223
230
  continue;
224
231
  const codesLiteral = `[${codes.map(c => JSON.stringify(c)).join(', ')}]`;
225
232
  const msgBase = `None of the codings provided are in the value set '${escStr(vsDisplayName)}' (${escStr(field.binding.uri || '')})`;
233
+ // `allowed` is a string[], so each candidate code is narrowed to a string
234
+ // before the lookup rather than asserted through an `any`.
235
+ const isAllowed = (codeExpr) => `${codeExpr} !== undefined && ${codesLiteral}.includes(${codeExpr})`;
226
236
  if (type === 'CodeableConcept') {
227
237
  if (field.isArray) {
228
238
  // Array of CodeableConcept: each entry must have at least one valid coding
229
- refinements.push(`.refine(d => { const arr = (d as any).${rel}; if (!Array.isArray(arr)) return true; const allowed = ${codesLiteral}; return arr.every((cc: any) => cc?.coding?.some((c: any) => allowed.includes(c?.code))); }, { message: "${escStr(msgBase)}", when: () => true })`);
239
+ refinements.push(`.refine(d => { const _value = at(d, '${rel}'); if (!Array.isArray(_value)) return true; return arr(_value).every((cc) => codings(cc).some((c) => ${isAllowed('c.code')})); }, { message: "${escStr(msgBase)}", when: () => true })`);
230
240
  }
231
241
  else {
232
242
  // Single CodeableConcept: coding must include at least one valid code
233
- refinements.push(`.refine(d => { const cc = (d as any).${rel}; if (!cc) return true; const allowed = ${codesLiteral}; return cc.coding?.some((c: any) => allowed.includes(c?.code)); }, { message: "${escStr(msgBase)}", when: () => true })`);
243
+ refinements.push(`.refine(d => { const _value = at(d, '${rel}'); if (!_value) return true; return codings(_value).some((c) => ${isAllowed('c.code')}); }, { message: "${escStr(msgBase)}", when: () => true })`);
234
244
  }
235
245
  }
236
246
  else {
237
247
  // Coding field
238
248
  if (field.isArray) {
239
- refinements.push(`.refine(d => { const arr = (d as any).${rel}; if (!Array.isArray(arr)) return true; const allowed = ${codesLiteral}; return arr.every((c: any) => allowed.includes(c?.code)); }, { message: "${escStr(msgBase)}", when: () => true })`);
249
+ refinements.push(`.refine(d => { const _value = at(d, '${rel}'); if (!Array.isArray(_value)) return true; return codings(_value).every((c) => ${isAllowed('c.code')}); }, { message: "${escStr(msgBase)}", when: () => true })`);
240
250
  }
241
251
  else {
242
- refinements.push(`.refine(d => { const c = (d as any).${rel}; if (!c) return true; const allowed = ${codesLiteral}; return allowed.includes(c.code); }, { message: "${escStr(msgBase)}", when: () => true })`);
252
+ refinements.push(`.refine(d => { const _value = at(d, '${rel}'); if (!_value) return true; const _c = coding(_value); return ${isAllowed('_c.code')}; }, { message: "${escStr(msgBase)}", when: () => true })`);
243
253
  }
244
254
  }
245
255
  }
@@ -11,6 +11,8 @@ import { getZodPackageTypes } from './zodTypes.js';
11
11
  import { mergeFields } from '../../core/utils.js';
12
12
  import { buildRefinements } from './zodRefinementBuilder.js';
13
13
  /** Map FHIR primitive types to Zod schema calls */
14
+ /** Runtime helpers a refinement may reference, imported from ValidatorOptions. */
15
+ const ZOD_REFINEMENT_HELPERS = ['at', 'arr', 'coding', 'codings'];
14
16
  const ZOD_PRIMITIVE_MAP = {
15
17
  any: 'z.string()',
16
18
  string: 'z.string()',
@@ -262,6 +264,19 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
262
264
  valueSetImports.set(name, importPath);
263
265
  }
264
266
  }
267
+ // Refinements read dynamic property paths through the same runtime helpers the
268
+ // generated validators use, rather than casting the parsed object to `any`.
269
+ // ValidatorOptions.ts is written into every output directory, so the relative
270
+ // import always resolves. Only the helpers actually referenced are imported.
271
+ // The required-choice[x] refinements are appended further below and always
272
+ // probe their variants with at(), so they count toward the helper set here.
273
+ const refinementCode = refinementResult.refinements.join('\n')
274
+ + (requiredChoiceVariants.length > 0 ? '\nat(' : '');
275
+ const usedHelpers = ZOD_REFINEMENT_HELPERS.filter(helper => new RegExp(`\\b${helper}\\(`).test(refinementCode));
276
+ if (usedHelpers.length > 0) {
277
+ const headerIndex = lines.indexOf(`import { z } from "zod";`);
278
+ lines.splice(headerIndex + 1, 0, `import { ${usedHelpers.join(', ')} } from "./ValidatorOptions.js";`);
279
+ }
265
280
  lines.push('');
266
281
  // Emit schema
267
282
  const schemaName = `${interfaceName}Schema`;
@@ -285,7 +300,7 @@ export function generateZodSchema(interfaceName, newFields, baseResource, baseFi
285
300
  // Uses `when: () => true` so the refinement fires even when base schema has errors
286
301
  // (e.g., missing required fields on an empty resource).
287
302
  for (const { basePropName, variantProps } of requiredChoiceVariants) {
288
- const checks = variantProps.map(p => `(d as any).${p} !== undefined`).join(' || ');
303
+ const checks = variantProps.map(p => `at(d, '${p}') !== undefined`).join(' || ');
289
304
  lines.push(`.refine(d => ${checks}, { message: "${basePropName} must be present", when: () => true })`);
290
305
  }
291
306
  // Close with semicolon
@@ -223,16 +223,18 @@ async function generateIndexFile(dir) {
223
223
  const files = fs.readdirSync(dir, { withFileTypes: true });
224
224
  const exports = [];
225
225
  const exportedNames = new Set();
226
- // First pass: collect all files and their exported names
226
+ // First pass: collect all files and their exported names, keeping the
227
+ // declaration kind — `verbatimModuleSyntax` needs a re-exported type to go
228
+ // through `export type { … }`, so the barrel has to know which is which.
227
229
  const fileExports = new Map();
228
230
  for (const file of files) {
229
231
  if (file.isFile() && file.name.endsWith('.ts') && !file.name.endsWith('.d.ts') && file.name !== 'index.ts' && file.name !== 'ValidatorOptions.ts') {
230
232
  const filePath = path.join(dir, file.name);
231
233
  const content = fs.readFileSync(filePath, 'utf-8');
232
- const names = new Set();
233
- const exportMatches = content.matchAll(/export\s+(?:interface|class|type|const|function)\s+(\w+)/g);
234
+ const names = new Map();
235
+ const exportMatches = content.matchAll(/export\s+(interface|class|type|const|function)\s+(\w+)/g);
234
236
  for (const match of exportMatches) {
235
- names.add(match[1]);
237
+ names.set(match[2], match[1] === 'interface' || match[1] === 'type' ? 'type' : 'value');
236
238
  }
237
239
  fileExports.set(file.name, names);
238
240
  }
@@ -241,11 +243,11 @@ async function generateIndexFile(dir) {
241
243
  for (const file of files) {
242
244
  if (file.isFile() && file.name.endsWith('.ts') && !file.name.endsWith('.d.ts') && file.name !== 'index.ts' && file.name !== 'ValidatorOptions.ts') {
243
245
  const baseName = file.name.replace('.ts', '');
244
- const fileNames = fileExports.get(file.name) || new Set();
246
+ const fileNames = fileExports.get(file.name) || new Map();
245
247
  // Find which names from this file conflict with already-exported names
246
248
  const conflicting = new Set();
247
249
  const unique = new Set();
248
- for (const name of fileNames) {
250
+ for (const name of fileNames.keys()) {
249
251
  if (exportedNames.has(name)) {
250
252
  conflicting.add(name);
251
253
  }
@@ -254,17 +256,25 @@ async function generateIndexFile(dir) {
254
256
  }
255
257
  }
256
258
  if (conflicting.size === 0) {
257
- // No conflicts — use simple wildcard export
259
+ // No conflicts — use simple wildcard export. `export *` carries types
260
+ // without needing the `type` modifier.
258
261
  exports.push(`export * from './${baseName}.js';`);
259
262
  }
260
263
  else if (unique.size > 0) {
261
- // Has conflicts but also has unique names — use selective named export
262
- const names = [...unique].sort().join(', ');
263
- exports.push(`export { ${names} } from './${baseName}.js';`);
264
+ // Has conflicts but also has unique names — use selective named exports,
265
+ // split by kind so re-exported types carry the `type` modifier.
266
+ const typeNames = [...unique].filter(n => fileNames.get(n) === 'type').sort();
267
+ const valueNames = [...unique].filter(n => fileNames.get(n) !== 'type').sort();
268
+ if (valueNames.length > 0) {
269
+ exports.push(`export { ${valueNames.join(', ')} } from './${baseName}.js';`);
270
+ }
271
+ if (typeNames.length > 0) {
272
+ exports.push(`export type { ${typeNames.join(', ')} } from './${baseName}.js';`);
273
+ }
264
274
  }
265
275
  // If all names conflict (unique.size === 0), skip entirely
266
276
  // Track all names from this file as exported
267
- for (const name of fileNames) {
277
+ for (const name of fileNames.keys()) {
268
278
  exportedNames.add(name);
269
279
  }
270
280
  }
@@ -7,14 +7,7 @@ import { logger } from "../../logger.js";
7
7
  import { getFhirPackagesCacheDir, ensureCacheDir } from "../core/cacheConfig.js";
8
8
  import { downloadFile } from '../core/utils.js';
9
9
  import { versionSlug } from '../fhir/versionContext.js';
10
- /**
11
- * FHIR package registries in priority order.
12
- * packages.fhir.org is the canonical source; packages.simplifier.net is the fallback.
13
- */
14
- const FHIR_REGISTRIES = [
15
- 'https://packages.fhir.org',
16
- 'https://packages.simplifier.net',
17
- ];
10
+ import { FHIR_REGISTRIES } from '../core/constants.js';
18
11
  const log = logger.withTag('package');
19
12
  /**
20
13
  * Load curated sample codes for large external CodeSystems and core ValueSet→system mappings
@@ -442,6 +442,7 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
442
442
  type,
443
443
  baseTypeCode, // Raw FHIR type code (e.g., "HumanName" even when type is "humanname-de-basis")
444
444
  sliceName: element.sliceName,
445
+ slicingRules: element.slicing?.rules,
445
446
  profileUrls: (element.type || []).flatMap(t => t.profile || []),
446
447
  binding: bindingMeta,
447
448
  isProfiled: hasProfile,