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
|
@@ -1,163 +1,82 @@
|
|
|
1
1
|
import { logger } from '../../../logger.js';
|
|
2
2
|
import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
|
|
3
3
|
import { generateSliceValidations } from './sliceValidatorGenerator.js';
|
|
4
|
+
import { generateClosedSlicingValidations } from './closedSlicingValidation.js';
|
|
4
5
|
import { fhirpathModelSlug } from '../../fhir/versionContext.js';
|
|
5
6
|
import { buildRequiredConstraints, buildMaxCardinalityConstraints, buildForbiddenFieldConstraintEntries, buildPatternValidations, buildChoiceTypeNarrowingValidations, } from './validatorConstraintBuilders.js';
|
|
6
|
-
import { buildNestedRequiredValidations, buildFixedValueValidations, buildProhibitedFieldValidations, buildPrimitiveFormatValidations, buildStructuralTypeValidations, } from './validatorFieldBuilders.js';
|
|
7
|
+
import { buildNestedRequiredValidations, buildFixedValueValidations, buildProhibitedFieldValidations, buildPrimitiveFormatValidations, buildStructuralTypeValidations, buildInlineResourceTypeValidations, INLINE_RESOURCE_SLOTS, } from './validatorFieldBuilders.js';
|
|
7
8
|
import { buildBindingValidations } from './validatorBindingBuilder.js';
|
|
8
|
-
import { generateBundleRefValidation, generateContainedRefValidation, generateExtensionStructuralValidation,
|
|
9
|
+
import { generateBundleRefValidation, generateContainedRefValidation, generateExtensionStructuralValidation, generateAggregationRefValidation, buildReferenceTargetTypeValidation, buildBundleEntryResolution } from './validatorTemplates.js';
|
|
10
|
+
import { HELPER_NAMES } from './validatorRuntime.js';
|
|
11
|
+
import { quoteFhirPathPath, normalizeCollectionTypeFilters, isBaseElementInvariant } from './validatorExpressions.js';
|
|
9
12
|
const log = logger.withTag('validator');
|
|
10
13
|
/**
|
|
11
|
-
*
|
|
12
|
-
* plus the element-probing helpers that every generated validateX() imports.
|
|
14
|
+
* A constraint declared on a slice applies only to that slice's members.
|
|
13
15
|
*
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* still narrow before it can do anything with a value.
|
|
19
|
-
*/
|
|
20
|
-
export function generateValidatorOptionsContent() {
|
|
21
|
-
return `/** Runtime options for generated FHIR validators. */
|
|
22
|
-
export interface ValidatorOptions {
|
|
23
|
-
/** Terminology server URL for memberOf() evaluation (e.g. "https://tx.fhir.org/r4"). */
|
|
24
|
-
terminologyUrl?: string;
|
|
25
|
-
/** FHIR server URL for resolve() evaluation (e.g. "https://hapi.fhir.org/baseR4"). */
|
|
26
|
-
fhirServerUrl?: string;
|
|
27
|
-
/** HTTP headers keyed by server URL — used for auth with terminology or FHIR servers. */
|
|
28
|
-
httpHeaders?: Record<string, string>;
|
|
29
|
-
/** AbortSignal for cancelling long-running async evaluations. */
|
|
30
|
-
signal?: AbortSignal;
|
|
31
|
-
/** Debug tracing callback — invoked for every FHIRPath trace() call. */
|
|
32
|
-
traceFn?: (value: unknown, label: string) => void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Property bag view of a FHIR element. Reads yield \`unknown\`, never \`any\`. */
|
|
36
|
-
export type FhirNode = { readonly [key: string]: unknown };
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* View an arbitrary value as a property bag, for key enumeration
|
|
40
|
-
* (\`Object.entries(node(x))\`) and repeated child reads.
|
|
41
|
-
*
|
|
42
|
-
* Non-objects become \`{}\` so callers never need a null check.
|
|
43
|
-
*/
|
|
44
|
-
export function node(value: unknown): FhirNode {
|
|
45
|
-
return typeof value === 'object' && value !== null ? (value as FhirNode) : {};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Read a (possibly nested) child element off an arbitrary FHIR value.
|
|
50
|
-
*
|
|
51
|
-
* Returns \`undefined\` as soon as the path leaves an object, so it is safe on
|
|
52
|
-
* absent, null, primitive and array-valued intermediates alike. This is the one
|
|
53
|
-
* place the untyped boundary is crossed, replacing a cast at every call site.
|
|
54
|
-
*
|
|
55
|
-
* @example at(resource, 'channel', 'payload', 'extension')
|
|
56
|
-
*/
|
|
57
|
-
export function at(value: unknown, ...path: readonly string[]): unknown {
|
|
58
|
-
let current = value;
|
|
59
|
-
for (const key of path) {
|
|
60
|
-
if (typeof current !== 'object' || current === null) return undefined;
|
|
61
|
-
current = (current as FhirNode)[key];
|
|
62
|
-
}
|
|
63
|
-
return current;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* View an element as an iterable list of children.
|
|
68
|
-
*
|
|
69
|
-
* Exactly equivalent to the \`if (Array.isArray(x)) for (const c of x)\` guard it
|
|
70
|
-
* replaces: non-array values (absent, null, or a scalar) yield no iterations, so
|
|
71
|
-
* substituting this helper does not change which elements get validated.
|
|
72
|
-
*/
|
|
73
|
-
export function arr(value: unknown): readonly unknown[] {
|
|
74
|
-
return Array.isArray(value) ? value : [];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** A coding as read by the emitted slice, pattern and binding checks. */
|
|
78
|
-
export interface CodingShape {
|
|
79
|
-
code?: string;
|
|
80
|
-
system?: string;
|
|
81
|
-
display?: string;
|
|
82
|
-
version?: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* The properties emitted element checks actually read off a FHIR element.
|
|
87
|
-
*
|
|
88
|
-
* Deliberately structural and all-optional: slice discriminators match on
|
|
89
|
-
* \`url\`/\`code\`/\`system\`, pattern checks walk \`coding\`, and choice-type checks
|
|
90
|
-
* look for \`value*\` keys. Typing the element this way keeps every access checked,
|
|
91
|
-
* where a \`(item: any)\` callback parameter would switch checking off entirely.
|
|
92
|
-
*/
|
|
93
|
-
export interface ElementShape {
|
|
94
|
-
/** Codings when the element is itself a CodeableConcept. */
|
|
95
|
-
coding?: CodingShape[];
|
|
96
|
-
/**
|
|
97
|
-
* Everything else reads as \`unknown\`.
|
|
98
|
-
*
|
|
99
|
-
* Discriminator properties are deliberately *not* declared with concrete types:
|
|
100
|
-
* a FHIR element's \`code\` may be a \`code\` primitive on one profile and a
|
|
101
|
-
* \`CodeableConcept\` on another, so committing to \`string\` would break the
|
|
102
|
-
* \`.coding\` reads (and vice versa). \`unknown\` still supports \`===\` comparisons
|
|
103
|
-
* and \`typeof\` narrowing; use \`at()\` / \`codings()\` to go deeper.
|
|
104
|
-
*/
|
|
105
|
-
readonly [key: string]: unknown;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Like {@link arr} but typed for element inspection, so emitted callbacks need no
|
|
110
|
-
* parameter annotation and stay type-checked.
|
|
16
|
+
* The scope is derived from field.name, which carries no slice identity, so every
|
|
17
|
+
* slice's invariants were being applied to the whole base element. US Core pins
|
|
18
|
+
* Practitioner.identifier:NPI to ten digits and :NCSBN to eight, so a conformant
|
|
19
|
+
* NPI necessarily failed the NCSBN rule and we reported an error HL7 does not.
|
|
111
20
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
21
|
+
* Returns a predicate matching the value the slice pins, so the constraint reaches
|
|
22
|
+
* only its own members. Slices discriminated by something other than a scalar
|
|
23
|
+
* (a CodeableConcept's coding array, say) yield undefined and are left alone
|
|
24
|
+
* rather than guessed at.
|
|
114
25
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (Array.isArray(value)) return value as CodingShape[];
|
|
128
|
-
const nested = at(value, 'coding');
|
|
129
|
-
if (Array.isArray(nested)) return nested as CodingShape[];
|
|
130
|
-
// A lone Coding. Elements typed Coding 0..1 are neither an array nor a
|
|
131
|
-
// CodeableConcept — AuditEvent.entity.type in R4 is the common case — and used
|
|
132
|
-
// to fall through to [], so every slice discriminated on one matched nothing.
|
|
133
|
-
// That silently disabled the whole within-slice block: the IHE audit profiles
|
|
134
|
-
// reported "No elements matched required slice" while their max/min child
|
|
135
|
-
// constraints were never evaluated at all.
|
|
136
|
-
if (value !== null && typeof value === 'object') {
|
|
137
|
-
const lone = value as CodingShape;
|
|
138
|
-
if (lone.system !== undefined || lone.code !== undefined) return [lone];
|
|
139
|
-
}
|
|
140
|
-
return [];
|
|
26
|
+
function buildSliceConstraintFilter(field) {
|
|
27
|
+
const elementId = field.elementId || '';
|
|
28
|
+
if (!elementId.split('.').some(segment => segment.includes(':')))
|
|
29
|
+
return undefined;
|
|
30
|
+
const pattern = field.patternConstraint;
|
|
31
|
+
if (!pattern || typeof pattern !== 'object')
|
|
32
|
+
return undefined;
|
|
33
|
+
for (const [key, value] of Object.entries(pattern)) {
|
|
34
|
+
if (typeof value === 'string')
|
|
35
|
+
return `${key} = '${value.replace(/'/g, "\\'")}'`;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
141
38
|
}
|
|
142
|
-
|
|
39
|
+
/** Coded types whose membership a terminology server can decide. */
|
|
40
|
+
const CODED_BINDING_TYPES = new Set(['code', 'Coding', 'CodeableConcept']);
|
|
143
41
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* Cross-profile delegation reaches its target through \`at()\`, which yields
|
|
147
|
-
* \`unknown\`; the callee wants e.g. \`USCorePatient\`. \`T\` is inferred from the
|
|
148
|
-
* parameter position, so the conversion needs no type import and no
|
|
149
|
-
* \`Parameters<typeof validateX>[0]\` — the latter breaks outright in packages that
|
|
150
|
-
* define a FHIR resource named \`Parameters\`, which shadows the utility type, and
|
|
151
|
-
* it rejects array-valued fields whose element type is the delegate's.
|
|
42
|
+
* Scope a binding declared on a slice's child to that slice's members.
|
|
152
43
|
*
|
|
153
|
-
*
|
|
44
|
+
* Task.input is sliced by `value@type` with each slice pinning a different type
|
|
45
|
+
* coding, so an unscoped check would apply one slice's ValueSet to every input —
|
|
46
|
+
* the same mistake that made US Core's NPI fail the NCSBN rule. The predicate is
|
|
47
|
+
* built from the sibling element that carries the pin.
|
|
154
48
|
*/
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
49
|
+
function sliceScopedBindingPath(rel, field, fields) {
|
|
50
|
+
const elementId = field.elementId || '';
|
|
51
|
+
const sliceMatch = /^(.*?:[^.]+)\./.exec(elementId);
|
|
52
|
+
if (!sliceMatch)
|
|
53
|
+
return rel;
|
|
54
|
+
const sliceId = sliceMatch[1];
|
|
55
|
+
const pinned = fields.find(f => {
|
|
56
|
+
if (!f.elementId || !f.elementId.startsWith(sliceId + '.'))
|
|
57
|
+
return false;
|
|
58
|
+
const pattern = f.patternConstraint;
|
|
59
|
+
return !!pattern && Array.isArray(pattern.coding) && pattern.coding.length > 0;
|
|
60
|
+
});
|
|
61
|
+
if (!pinned?.elementId)
|
|
62
|
+
return rel;
|
|
63
|
+
const discriminatorProp = pinned.elementId.substring(sliceId.length + 1);
|
|
64
|
+
if (!discriminatorProp || discriminatorProp.includes('.'))
|
|
65
|
+
return rel;
|
|
66
|
+
const coding = pinned.patternConstraint.coding[0];
|
|
67
|
+
const system = typeof coding.system === 'string' ? coding.system : undefined;
|
|
68
|
+
const code = typeof coding.code === 'string' ? coding.code : undefined;
|
|
69
|
+
if (!code)
|
|
70
|
+
return rel;
|
|
71
|
+
const terms = [system ? `system='${system}'` : '', `code='${code}'`].filter(Boolean).join(' and ');
|
|
72
|
+
const segments = rel.split('.');
|
|
73
|
+
// rel is <base>.<leaf>; the where() goes on the repeating base element.
|
|
74
|
+
if (segments.length < 2)
|
|
75
|
+
return rel;
|
|
76
|
+
const leaf = segments.pop();
|
|
77
|
+
return `${segments.join('.')}.where(${discriminatorProp}.coding.where(${terms}).exists()).${leaf}`;
|
|
159
78
|
}
|
|
160
|
-
export function generateValidateProfileFunction(interfaceName, fields, valueSets, profileUrl, baseResourceType, profileUrlToName, extensionMetadata, baseDefinition, profileUrlToType) {
|
|
79
|
+
export function generateValidateProfileFunction(interfaceName, fields, valueSets, profileUrl, baseResourceType, profileUrlToName, extensionMetadata, baseDefinition, profileUrlToType, datatypeProfilePins) {
|
|
161
80
|
const valueSetImports = new Map();
|
|
162
81
|
const validatorImports = new Map();
|
|
163
82
|
// ── Collect field-level constraints ──────────────────────────────────────
|
|
@@ -176,9 +95,10 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
176
95
|
else {
|
|
177
96
|
fieldPath = rawName.replace(/^[^.]+\./, "").replace(/\[x\]/g, "");
|
|
178
97
|
}
|
|
98
|
+
const sliceFilter = buildSliceConstraintFilter(field);
|
|
179
99
|
field.constraints.forEach((constraint) => {
|
|
180
100
|
if (constraint && constraint.expression) {
|
|
181
|
-
constraintsWithContext.push({ constraint, fieldPath, isChoiceType });
|
|
101
|
+
constraintsWithContext.push({ constraint, fieldPath, isChoiceType, sliceFilter });
|
|
182
102
|
}
|
|
183
103
|
});
|
|
184
104
|
});
|
|
@@ -268,7 +188,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
268
188
|
constraintsWithContext.push(...buildRequiredConstraints(fields, arrayFieldPaths, fieldPathMap, fieldParentMap), ...buildMaxCardinalityConstraints(fields, arrayFieldPaths, fieldParentMap), ...buildForbiddenFieldConstraintEntries(fields));
|
|
269
189
|
const patternValidations = buildPatternValidations(fields, arrayFieldPaths, fieldPathMap);
|
|
270
190
|
const nestedRequiredValidations = buildNestedRequiredValidations(fields, arrayFieldPaths);
|
|
271
|
-
const fixedValueValidations = buildFixedValueValidations(fields, arrayFieldPaths);
|
|
191
|
+
const fixedValueValidations = buildFixedValueValidations(fields, arrayFieldPaths, profileUrl, baseResourceType);
|
|
272
192
|
const prohibitedFieldValidations = buildProhibitedFieldValidations(fields, arrayFieldPaths, fieldParentMap, baseResourceType, interfaceName);
|
|
273
193
|
const bindingValidations = buildBindingValidations(fields, fieldParentMap, arrayFieldPaths, getValueSetForBinding, valueSetImports);
|
|
274
194
|
// ── Synthesize memberOf() constraints for code-type non-enumerable bindings ─
|
|
@@ -284,7 +204,10 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
284
204
|
if (f.sliceName)
|
|
285
205
|
return false;
|
|
286
206
|
const type = f.type || f.baseTypeCode;
|
|
287
|
-
|
|
207
|
+
// Coding and CodeableConcept are decidable by the server too; restricting this
|
|
208
|
+
// to `code` left a required binding on a CodeableConcept unchecked entirely,
|
|
209
|
+
// and the system-presence fallback passes anything that carries a system.
|
|
210
|
+
if (!type || !CODED_BINDING_TYPES.has(type))
|
|
288
211
|
return false;
|
|
289
212
|
const vsInfo = getValueSetForBinding(f.binding.uri);
|
|
290
213
|
// Only emit when VS is unresolved or systemOnly (no enumerated codes)
|
|
@@ -292,9 +215,16 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
292
215
|
})
|
|
293
216
|
.forEach(f => {
|
|
294
217
|
const relFull = f.name || '';
|
|
295
|
-
const
|
|
218
|
+
const type = f.type || f.baseTypeCode || '';
|
|
219
|
+
let fieldPath = relFull.replace(/^[^.]+\./, '');
|
|
220
|
+
// A choice element's JSON property carries the type suffix, so stripping
|
|
221
|
+
// "[x]" alone would name a property that does not exist.
|
|
222
|
+
fieldPath = fieldPath.includes('[x]')
|
|
223
|
+
? fieldPath.replace(/\[x\]/, type.charAt(0).toUpperCase() + type.slice(1))
|
|
224
|
+
: fieldPath;
|
|
296
225
|
if (!fieldPath)
|
|
297
226
|
return;
|
|
227
|
+
fieldPath = sliceScopedBindingPath(fieldPath, f, fields);
|
|
298
228
|
const vsUri = (f.binding.uri || '').split('|')[0];
|
|
299
229
|
const vsDisplayName = vsUri.split('/').pop() || vsUri;
|
|
300
230
|
constraintsWithContext.push({
|
|
@@ -310,12 +240,18 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
310
240
|
const sliceValidations = generateSliceValidations({
|
|
311
241
|
fields, fieldPathMap, arrayFieldPaths, getValueSetForBinding,
|
|
312
242
|
valueSetImports, profileUrl, profileName: interfaceName, baseResourceType,
|
|
313
|
-
profileUrlToName, validatorImports, extensionMetadata, profileUrlToType,
|
|
243
|
+
profileUrlToName, validatorImports, extensionMetadata, profileUrlToType, datatypeProfilePins,
|
|
314
244
|
});
|
|
245
|
+
const closedSlicingValidations = generateClosedSlicingValidations(fields, profileUrl, baseResourceType);
|
|
315
246
|
const primitiveFormatValidations = buildPrimitiveFormatValidations(fields);
|
|
316
|
-
const structuralTypeValidations =
|
|
247
|
+
const structuralTypeValidations = [
|
|
248
|
+
...buildStructuralTypeValidations(fields, arrayFieldPaths),
|
|
249
|
+
...buildInlineResourceTypeValidations(fields, profileUrl),
|
|
250
|
+
];
|
|
317
251
|
const choiceNarrowingValidations = buildChoiceTypeNarrowingValidations(fields, profileUrl);
|
|
318
252
|
const bundleRefValidation = generateBundleRefValidation(baseResourceType);
|
|
253
|
+
const referenceTargetTypeValidation = buildReferenceTargetTypeValidation(fields, profileUrlToType);
|
|
254
|
+
const bundleEntryResolution = buildBundleEntryResolution(baseResourceType);
|
|
319
255
|
const containedRefValidation = generateContainedRefValidation(baseResourceType);
|
|
320
256
|
const extensionStructuralValidation = generateExtensionStructuralValidation();
|
|
321
257
|
// ── Bundled aggregation reference checks ─────────────────────────────────
|
|
@@ -329,8 +265,19 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
329
265
|
// Skip slices — base field already covers them
|
|
330
266
|
if (field.sliceName)
|
|
331
267
|
continue;
|
|
332
|
-
const
|
|
333
|
-
|
|
268
|
+
const aggregations = new Set((field.typeOptions ?? [])
|
|
269
|
+
.filter(t => t.code === 'Reference')
|
|
270
|
+
.flatMap(t => t.aggregation ?? []));
|
|
271
|
+
let kind;
|
|
272
|
+
if (aggregations.has('bundled')) {
|
|
273
|
+
kind = 'bundled';
|
|
274
|
+
}
|
|
275
|
+
else if (aggregations.has('contained') && !aggregations.has('referenced')) {
|
|
276
|
+
// Only restrict when a contained reference is the sole option — a field
|
|
277
|
+
// that also permits 'referenced' accepts an ordinary relative reference.
|
|
278
|
+
kind = 'contained';
|
|
279
|
+
}
|
|
280
|
+
if (!kind)
|
|
334
281
|
continue;
|
|
335
282
|
// Strip resource-type prefix (e.g. "Condition.encounter" → "encounter")
|
|
336
283
|
const dotIdx = field.name.indexOf('.');
|
|
@@ -342,10 +289,10 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
342
289
|
const partial = field.name.substring(0, dotIdx) + '.' + segments.slice(0, i + 1).join('.');
|
|
343
290
|
return fieldArrayMap.get(partial) ?? false;
|
|
344
291
|
});
|
|
345
|
-
bundledPaths.push({ segments, isArray });
|
|
292
|
+
bundledPaths.push({ segments, isArray, kind });
|
|
346
293
|
}
|
|
347
294
|
}
|
|
348
|
-
const bundledAggregationValidation =
|
|
295
|
+
const bundledAggregationValidation = generateAggregationRefValidation(bundledPaths);
|
|
349
296
|
// ── Profiled datatype field delegation ──────────────────────────────────
|
|
350
297
|
// When a non-sliced field references a profiled datatype (e.g., Bundle.signature
|
|
351
298
|
// with profile cdex-signature), delegate validation to the generated validator.
|
|
@@ -372,12 +319,25 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
372
319
|
if (!delegateName || delegateName === interfaceName)
|
|
373
320
|
continue;
|
|
374
321
|
validatorImports.set(delegateName, `./${delegateName}.js`);
|
|
322
|
+
// An inline-resource slot narrowed to a concrete resource type is not a
|
|
323
|
+
// datatype: the value carries a resourceType, and handing an untyped or
|
|
324
|
+
// wrong-typed blob to a resource profile's validator reports that profile's
|
|
325
|
+
// required fields against something that is not one. HL7 rejects the
|
|
326
|
+
// resource as untyped and never descends, so it reports none of them.
|
|
327
|
+
// field.type is often the profile alias (dtr-questionnaire-adapt) rather than
|
|
328
|
+
// a resource type name, so resolve the type the profile targets.
|
|
329
|
+
const isResourceTypeName = (name) => !!name && /^[A-Z][A-Za-z]+$/.test(name);
|
|
330
|
+
const expectedResourceType = [profileUrlToType?.get(pUrl), field.baseTypeCode, fieldType]
|
|
331
|
+
.find(isResourceTypeName);
|
|
332
|
+
const inlineResourceGuard = INLINE_RESOURCE_SLOTS.has(relPath) && expectedResourceType
|
|
333
|
+
? `\n if (at(_dtEl, 'resourceType') !== '${expectedResourceType}') continue;`
|
|
334
|
+
: '';
|
|
375
335
|
if (field.isArray) {
|
|
376
336
|
datatypeProfileDelegations.push(`
|
|
377
337
|
// Profiled datatype delegation: ${relPath} → ${delegateName}
|
|
378
338
|
if (resource.${relPath} && Array.isArray(resource.${relPath})) {
|
|
379
|
-
for (const _dtEl of resource.${relPath}) {
|
|
380
|
-
const _sub = await validate${delegateName}(asResource(_dtEl),
|
|
339
|
+
for (const _dtEl of resource.${relPath}) {${inlineResourceGuard}
|
|
340
|
+
const _sub = await validate${delegateName}(asResource(_dtEl), _delegateOptions);
|
|
381
341
|
errors.push(..._sub.errors);
|
|
382
342
|
warnings.push(..._sub.warnings);
|
|
383
343
|
}
|
|
@@ -387,7 +347,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
387
347
|
datatypeProfileDelegations.push(`
|
|
388
348
|
// Profiled datatype delegation: ${relPath} → ${delegateName}
|
|
389
349
|
if (resource.${relPath}) {
|
|
390
|
-
const _sub = await validate${delegateName}(asResource(resource.${relPath}),
|
|
350
|
+
const _sub = await validate${delegateName}(asResource(resource.${relPath}), _delegateOptions);
|
|
391
351
|
errors.push(..._sub.errors);
|
|
392
352
|
warnings.push(..._sub.warnings);
|
|
393
353
|
}`);
|
|
@@ -397,7 +357,13 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
397
357
|
}
|
|
398
358
|
}
|
|
399
359
|
// ── Deduplicate & filter constraints ────────────────────────────────────
|
|
400
|
-
const uniqueConstraints = Array.from(new Map(
|
|
360
|
+
const uniqueConstraints = Array.from(new Map(
|
|
361
|
+
// The slice filter is part of the identity: two slices of the same element can
|
|
362
|
+
// carry the same expression with different pinned values, and collapsing them
|
|
363
|
+
// would keep only one slice's rule.
|
|
364
|
+
constraintsWithContext.map((item) => [
|
|
365
|
+
`${item.constraint.expression}|${item.fieldPath}|${item.sliceFilter ?? ''}`, item,
|
|
366
|
+
])).values());
|
|
401
367
|
const filteredConstraints = uniqueConstraints.filter(c => {
|
|
402
368
|
const expr = c.constraint.expression || "";
|
|
403
369
|
const fieldPath = c.fieldPath || "";
|
|
@@ -408,6 +374,16 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
408
374
|
// Filter $this on choice-type fields — fhirpath scoping differs from HL7.
|
|
409
375
|
if (c.isChoiceType && /\$this\b/.test(expr))
|
|
410
376
|
return false;
|
|
377
|
+
// txt-1/txt-2 (htmlChecks) are not filtered: htmlChecks is supplied through
|
|
378
|
+
// fhirpath's userInvocationTable, and as() over a collection is normalised to
|
|
379
|
+
// ofType() below.
|
|
380
|
+
//
|
|
381
|
+
// dom-3 is implemented natively in generateContainedRefValidation instead.
|
|
382
|
+
// Its FHIRPath form builds the fragment as '#'+id, so an absent id makes the
|
|
383
|
+
// predicate empty and the invariant passes where HL7 reports the resource as
|
|
384
|
+
// unreferenced. Keeping both would double-report the ones that do fire.
|
|
385
|
+
if (/'#'\s*\+\s*id\b/.test(expr))
|
|
386
|
+
return false;
|
|
411
387
|
// For nested fields (depth > 1), filter only base FHIR invariants whose
|
|
412
388
|
// evaluation diverges between fhirpath.js and HL7:
|
|
413
389
|
// ele-1: hasValue() or (children().count() > id.count())
|
|
@@ -415,19 +391,28 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
415
391
|
// Profile-specific constraints at nested depth are allowed through —
|
|
416
392
|
// the .all() wrapping handles scoping correctly for these.
|
|
417
393
|
if (fieldPath && fieldPath.split('.').length > 1) {
|
|
418
|
-
|
|
419
|
-
const isExt1 = /^extension\.exists\(\)\s*!=\s*value\.exists\(\)$/.test(expr);
|
|
420
|
-
if (isEle1 || isExt1)
|
|
394
|
+
if (isBaseElementInvariant(expr))
|
|
421
395
|
return false;
|
|
422
396
|
}
|
|
423
397
|
return true;
|
|
424
398
|
});
|
|
399
|
+
// An invariant on a recursive BackboneElement (Composition.section,
|
|
400
|
+
// Questionnaire.item, …) applies to every occurrence, not only the top-level
|
|
401
|
+
// ones. `<path>.all(…)` visits one level, so a nested element that violates the
|
|
402
|
+
// invariant goes unreported while HL7 flags it — cmp-1 on a depth-2
|
|
403
|
+
// Composition.section is the case that surfaced this. repeat() walks the whole
|
|
404
|
+
// recursion, which is the idiom FHIR's own invariants use.
|
|
405
|
+
const relativeFieldNames = new Set(fields.map(f => (f.name || '').replace(/^[^.]+\./, '')).filter(Boolean));
|
|
406
|
+
const isRecursiveElement = (path) => {
|
|
407
|
+
const lastSegment = path.split('.').pop() || '';
|
|
408
|
+
return !!lastSegment && relativeFieldNames.has(`${path}.${lastSegment}`);
|
|
409
|
+
};
|
|
425
410
|
// ── Generate FHIRPath evaluation code ───────────────────────────────────
|
|
426
411
|
const validationLogic = filteredConstraints
|
|
427
412
|
.map((item, index) => {
|
|
428
413
|
const constraint = item.constraint;
|
|
429
414
|
const fieldPath = item.fieldPath;
|
|
430
|
-
let escapedExpression = (constraint.expression || '')
|
|
415
|
+
let escapedExpression = normalizeCollectionTypeFilters(constraint.expression || '')
|
|
431
416
|
.replace(/\s*\n\s*/g, ' ')
|
|
432
417
|
.replace(/\\/g, '\\\\')
|
|
433
418
|
.replace(/"/g, '\\"');
|
|
@@ -455,13 +440,19 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
455
440
|
if (!expressionRefersToField) {
|
|
456
441
|
const resourceTypePattern = /^[A-Z][a-zA-Z]+\./;
|
|
457
442
|
if (!resourceTypePattern.test(escapedExpression)) {
|
|
458
|
-
|
|
443
|
+
const walkRecursion = isRecursiveElement(fieldPath)
|
|
444
|
+
&& !isBaseElementInvariant(constraint.expression || '');
|
|
445
|
+
const navigation = item.sliceFilter
|
|
446
|
+
? `${quoteFhirPathPath(fieldPath)}.where(${item.sliceFilter})`
|
|
447
|
+
: quoteFhirPathPath(fieldPath);
|
|
448
|
+
const scope = walkRecursion ? `repeat(${navigation})` : navigation;
|
|
449
|
+
escapedExpression = `${scope}.all(${escapedExpression})`;
|
|
459
450
|
}
|
|
460
451
|
}
|
|
461
452
|
}
|
|
462
453
|
const needsTerminology = /\bmemberOf\b/.test(constraint.expression || '');
|
|
463
454
|
let evaluateBlock = `
|
|
464
|
-
const result${index} = await fhirpath.evaluate(resource, "${escapedExpression}", { resource }, fhirpath_model, fhirpathOptions);
|
|
455
|
+
const result${index} = await fhirpath.evaluate(resource, "${escapedExpression}", { resource, rootResource: resource }, fhirpath_model, fhirpathOptions);
|
|
465
456
|
if (!result${index}.every(Boolean)) {
|
|
466
457
|
${constraint.severity === "error" ? "errors" : "warnings"}.push("Constraint violation: ${escapedHuman}");
|
|
467
458
|
}`;
|
|
@@ -490,7 +481,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
490
481
|
})
|
|
491
482
|
.join("\n");
|
|
492
483
|
// ── Assemble final output ───────────────────────────────────────────────
|
|
493
|
-
const hasConstraints = filteredConstraints.length > 0 || sliceValidations.length > 0
|
|
484
|
+
const hasConstraints = filteredConstraints.length > 0 || sliceValidations.length > 0 || closedSlicingValidations.length > 0
|
|
494
485
|
|| patternValidations.length > 0 || fixedValueValidations.length > 0
|
|
495
486
|
|| bindingValidations.length > 0 || prohibitedFieldValidations.length > 0
|
|
496
487
|
|| bundleRefValidation.length > 0 || primitiveFormatValidations.length > 0
|
|
@@ -510,14 +501,22 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
510
501
|
? `\n // Non-DomainResource: text is not a valid property\n if (at(resource, 'text') !== undefined) {\n errors.push("Unrecognized property 'text'");\n }`
|
|
511
502
|
: '';
|
|
512
503
|
if (!hasConstraints) {
|
|
504
|
+
const body = `${bundleEntryResolution}${referenceTargetTypeValidation}${structuralTypeValidations.join('')}${nonDomainTextCheck}${extensionStructuralValidation}${containedRefValidation}${bundledAggregationValidation}`;
|
|
505
|
+
// This path skips the shared helper import, so pull in exactly the helpers the
|
|
506
|
+
// body actually calls — importing all of them would trip no-unused-vars, and
|
|
507
|
+
// importing none breaks bodies that use at()/arr() (aggregation, text checks).
|
|
508
|
+
const usedHelpers = HELPER_NAMES.filter(name => new RegExp(`\\b${name}\\(`).test(body));
|
|
509
|
+
const helperImport = usedHelpers.length > 0
|
|
510
|
+
? `import { ${usedHelpers.join(', ')} } from './ValidatorOptions.js';\n`
|
|
511
|
+
: '';
|
|
513
512
|
return {
|
|
514
513
|
code: `import type { ValidatorOptions } from './ValidatorOptions.js';
|
|
515
|
-
|
|
514
|
+
${helperImport}
|
|
516
515
|
export async function validate${interfaceName}(resource: ${interfaceName}, options?: ValidatorOptions): Promise<{ errors: string[]; warnings: string[] }> {
|
|
517
516
|
const errors: string[] = [];
|
|
518
517
|
const warnings: string[] = [];
|
|
519
518
|
void options;
|
|
520
|
-
${
|
|
519
|
+
${body}
|
|
521
520
|
return { errors, warnings };
|
|
522
521
|
}`,
|
|
523
522
|
valueSetImports
|
|
@@ -533,9 +532,11 @@ ${structuralTypeValidations.length > 0 ? `\n // eslint-disable-next-line @types
|
|
|
533
532
|
// `at`/`arr` are value imports: they replace the `as any` element probing that
|
|
534
533
|
// would otherwise appear throughout the emitted body.
|
|
535
534
|
const optionsImport = 'import type { ValidatorOptions } from \'./ValidatorOptions.js\';\n'
|
|
536
|
-
+
|
|
535
|
+
+ `import { ${HELPER_NAMES.join(', ')}, fhirpathUserFunctions } from './ValidatorOptions.js';\n\n`;
|
|
537
536
|
const fhirpathOptionsLines = [
|
|
538
537
|
' preciseMath: true,',
|
|
538
|
+
// Supplies htmlChecks(), which fhirpath.js does not implement.
|
|
539
|
+
' userInvocationTable: fhirpathUserFunctions,',
|
|
539
540
|
' traceFn: options?.traceFn ?? (() => {}),',
|
|
540
541
|
' ...(options?.signal ? { signal: options.signal } : {}),',
|
|
541
542
|
' ...(options?.httpHeaders ? { httpHeaders: options.httpHeaders } : {}),',
|
|
@@ -562,7 +563,7 @@ ${structuralTypeValidations.length > 0 ? `\n // eslint-disable-next-line @types
|
|
|
562
563
|
const errors: string[] = [];
|
|
563
564
|
const warnings: string[] = [];
|
|
564
565
|
${fhirpathOptionsBlock}
|
|
565
|
-
${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${
|
|
566
|
+
${bundleEntryResolution}${referenceTargetTypeValidation}${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${structuralTypeValidations.join('')}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${choiceNarrowingValidations.join('')}${sliceValidations.join('')}${closedSlicingValidations.join('')}${datatypeProfileDelegations.join('')}${nonDomainTextCheck}${extensionStructuralValidation}${containedRefValidation}${bundledAggregationValidation}${bundleRefValidation}
|
|
566
567
|
return { errors, warnings };
|
|
567
568
|
}`,
|
|
568
569
|
valueSetImports
|