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
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
? `
|
|
48
|
-
: `
|
|
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
|
|
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
|
-
|
|
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 =>
|
|
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
|
|
99
|
-
: `(item
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
233
|
-
const exportMatches = content.matchAll(/export\s+(
|
|
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.
|
|
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
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
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,
|