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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime module every generated package emits as ValidatorOptions.ts.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from the emitter that orchestrates validate() so the emitted source
|
|
5
|
+
* can be read as source, rather than as a template buried in control flow.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Runtime helpers every generated validator may import from ValidatorOptions.
|
|
9
|
+
* Single source of truth for the emitted import list.
|
|
10
|
+
*/
|
|
11
|
+
export const HELPER_NAMES = ['at', 'arr', 'node', 'elements', 'elementsAt', 'coding', 'codings', 'asResource', 'refsAt'];
|
|
12
|
+
/**
|
|
13
|
+
* Returns the content of ValidatorOptions.ts — the shared runtime options type
|
|
14
|
+
* plus the element-probing helpers that every generated validateX() imports.
|
|
15
|
+
*
|
|
16
|
+
* The helpers exist so validators can read arbitrary child elements off an
|
|
17
|
+
* open-world FHIR resource *without* widening to `any`. `any` would disable
|
|
18
|
+
* checking for every downstream access, which contradicts the type safety this
|
|
19
|
+
* generator promises; `at()`/`arr()` return `unknown`, so the emitted code must
|
|
20
|
+
* still narrow before it can do anything with a value.
|
|
21
|
+
*/
|
|
22
|
+
export function generateValidatorOptionsContent() {
|
|
23
|
+
return `/** Runtime options for generated FHIR validators. */
|
|
24
|
+
export interface ValidatorOptions {
|
|
25
|
+
/** Terminology server URL for memberOf() evaluation (e.g. "https://tx.fhir.org/r4"). */
|
|
26
|
+
terminologyUrl?: string;
|
|
27
|
+
/** FHIR server URL for resolve() evaluation (e.g. "https://hapi.fhir.org/baseR4"). */
|
|
28
|
+
fhirServerUrl?: string;
|
|
29
|
+
/** HTTP headers keyed by server URL — used for auth with terminology or FHIR servers. */
|
|
30
|
+
httpHeaders?: Record<string, string>;
|
|
31
|
+
/** AbortSignal for cancelling long-running async evaluations. */
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
/** Debug tracing callback — invoked for every FHIRPath trace() call. */
|
|
34
|
+
traceFn?: (value: unknown, label: string) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Resolution map for references, keyed by both fullUrl and \`Type/id\`, valued by the
|
|
37
|
+
* resourceType each key resolves to.
|
|
38
|
+
*
|
|
39
|
+
* A Bundle validator populates this for the resources it contains and passes it to
|
|
40
|
+
* the validators it delegates to, which is what lets a profile check its own
|
|
41
|
+
* reference target types. Absent it, target types are not checked at all — matching
|
|
42
|
+
* the reference validator, which reports a wrong target only when it can resolve
|
|
43
|
+
* the reference, and stays silent on a dangling one even when the type is spelled
|
|
44
|
+
* out in the reference string.
|
|
45
|
+
*/
|
|
46
|
+
bundleEntries?: Readonly<Record<string, string>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Property bag view of a FHIR element. Reads yield \`unknown\`, never \`any\`. */
|
|
50
|
+
export type FhirNode = { readonly [key: string]: unknown };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* View an arbitrary value as a property bag, for key enumeration
|
|
54
|
+
* (\`Object.entries(node(x))\`) and repeated child reads.
|
|
55
|
+
*
|
|
56
|
+
* Non-objects become \`{}\` so callers never need a null check.
|
|
57
|
+
*/
|
|
58
|
+
export function node(value: unknown): FhirNode {
|
|
59
|
+
return typeof value === 'object' && value !== null ? (value as FhirNode) : {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Read a (possibly nested) child element off an arbitrary FHIR value.
|
|
64
|
+
*
|
|
65
|
+
* Returns \`undefined\` as soon as the path leaves an object, so it is safe on
|
|
66
|
+
* absent, null, primitive and array-valued intermediates alike. This is the one
|
|
67
|
+
* place the untyped boundary is crossed, replacing a cast at every call site.
|
|
68
|
+
*
|
|
69
|
+
* @example at(resource, 'channel', 'payload', 'extension')
|
|
70
|
+
*/
|
|
71
|
+
export function at(value: unknown, ...path: readonly string[]): unknown {
|
|
72
|
+
let current = value;
|
|
73
|
+
for (const key of path) {
|
|
74
|
+
if (typeof current !== 'object' || current === null) return undefined;
|
|
75
|
+
current = (current as FhirNode)[key];
|
|
76
|
+
}
|
|
77
|
+
return current;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* View an element as an iterable list of children.
|
|
82
|
+
*
|
|
83
|
+
* Exactly equivalent to the \`if (Array.isArray(x)) for (const c of x)\` guard it
|
|
84
|
+
* replaces: non-array values (absent, null, or a scalar) yield no iterations, so
|
|
85
|
+
* substituting this helper does not change which elements get validated.
|
|
86
|
+
*/
|
|
87
|
+
export function arr(value: unknown): readonly unknown[] {
|
|
88
|
+
return Array.isArray(value) ? value : [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A coding as read by the emitted slice, pattern and binding checks. */
|
|
92
|
+
export interface CodingShape {
|
|
93
|
+
code?: string;
|
|
94
|
+
system?: string;
|
|
95
|
+
display?: string;
|
|
96
|
+
version?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The properties emitted element checks actually read off a FHIR element.
|
|
101
|
+
*
|
|
102
|
+
* Deliberately structural and all-optional: slice discriminators match on
|
|
103
|
+
* \`url\`/\`code\`/\`system\`, pattern checks walk \`coding\`, and choice-type checks
|
|
104
|
+
* look for \`value*\` keys. Typing the element this way keeps every access checked,
|
|
105
|
+
* where a \`(item: any)\` callback parameter would switch checking off entirely.
|
|
106
|
+
*/
|
|
107
|
+
export interface ElementShape {
|
|
108
|
+
/** Codings when the element is itself a CodeableConcept. */
|
|
109
|
+
coding?: CodingShape[];
|
|
110
|
+
/**
|
|
111
|
+
* Everything else reads as \`unknown\`.
|
|
112
|
+
*
|
|
113
|
+
* Discriminator properties are deliberately *not* declared with concrete types:
|
|
114
|
+
* a FHIR element's \`code\` may be a \`code\` primitive on one profile and a
|
|
115
|
+
* \`CodeableConcept\` on another, so committing to \`string\` would break the
|
|
116
|
+
* \`.coding\` reads (and vice versa). \`unknown\` still supports \`===\` comparisons
|
|
117
|
+
* and \`typeof\` narrowing; use \`at()\` / \`codings()\` to go deeper.
|
|
118
|
+
*/
|
|
119
|
+
readonly [key: string]: unknown;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Like {@link arr} but typed for element inspection, so emitted callbacks need no
|
|
124
|
+
* parameter annotation and stay type-checked.
|
|
125
|
+
*
|
|
126
|
+
* Non-arrays yield no iterations, matching the \`Array.isArray\` guard this
|
|
127
|
+
* replaces.
|
|
128
|
+
*/
|
|
129
|
+
export function elements(value: unknown): readonly ElementShape[] {
|
|
130
|
+
return Array.isArray(value) ? (value as ElementShape[]) : [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Collect every element sitting at a property path, flattening arrays at each hop.
|
|
135
|
+
*
|
|
136
|
+
* One path therefore covers both single and repeating elements
|
|
137
|
+
* (Composition.author is 1..*, Composition.subject 0..1) and any mix of the two,
|
|
138
|
+
* which is what \`at()\` cannot do — it stops the moment the path meets an array.
|
|
139
|
+
* Absent and null intermediates simply contribute nothing.
|
|
140
|
+
*/
|
|
141
|
+
export function elementsAt(value: unknown, path: readonly string[]): unknown[] {
|
|
142
|
+
let current: unknown[] = [value];
|
|
143
|
+
for (const key of path) {
|
|
144
|
+
const next: unknown[] = [];
|
|
145
|
+
for (const item of current) {
|
|
146
|
+
const child = at(item, key);
|
|
147
|
+
if (Array.isArray(child)) next.push(...child);
|
|
148
|
+
else if (child !== undefined && child !== null) next.push(child);
|
|
149
|
+
}
|
|
150
|
+
current = next;
|
|
151
|
+
}
|
|
152
|
+
return current;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Collect the literal references found at a property path.
|
|
157
|
+
*
|
|
158
|
+
* Only string \`reference\` values are returned; a Reference carrying just an
|
|
159
|
+
* identifier names no target.
|
|
160
|
+
*/
|
|
161
|
+
export function refsAt(value: unknown, path: readonly string[]): string[] {
|
|
162
|
+
const found: string[] = [];
|
|
163
|
+
for (const item of elementsAt(value, path)) {
|
|
164
|
+
const reference = at(item, 'reference');
|
|
165
|
+
if (typeof reference === 'string') found.push(reference);
|
|
166
|
+
}
|
|
167
|
+
return found;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read the coded properties an element carries *itself*, as opposed to
|
|
172
|
+
* {@link codings} which walks into a CodeableConcept's \`.coding\`.
|
|
173
|
+
*
|
|
174
|
+
* This is the shape binding checks need for elements typed \`Coding\` or
|
|
175
|
+
* \`Quantity\`: both expose \`code\`/\`system\` directly. Values that are not strings
|
|
176
|
+
* are dropped rather than asserted through, so the result is genuinely
|
|
177
|
+
* \`string | undefined\` and callers can concatenate and compare it without a cast.
|
|
178
|
+
* A malformed non-string code is reported by the structural type checks instead.
|
|
179
|
+
*/
|
|
180
|
+
export function coding(value: unknown): CodingShape {
|
|
181
|
+
const source = node(value);
|
|
182
|
+
const result: CodingShape = {};
|
|
183
|
+
if (typeof source.code === 'string') result.code = source.code;
|
|
184
|
+
if (typeof source.system === 'string') result.system = source.system;
|
|
185
|
+
if (typeof source.display === 'string') result.display = source.display;
|
|
186
|
+
if (typeof source.version === 'string') result.version = source.version;
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* View a value as a list of codings.
|
|
192
|
+
*
|
|
193
|
+
* Accepts either a CodeableConcept (reads \`.coding\`) or an already-bare array of
|
|
194
|
+
* Codings, which is what slice discriminators need since the same discriminator
|
|
195
|
+
* path can be typed either way across profiles. Anything else yields \`[]\`.
|
|
196
|
+
*/
|
|
197
|
+
export function codings(value: unknown): readonly CodingShape[] {
|
|
198
|
+
if (Array.isArray(value)) return value as CodingShape[];
|
|
199
|
+
const nested = at(value, 'coding');
|
|
200
|
+
if (Array.isArray(nested)) return nested as CodingShape[];
|
|
201
|
+
// A lone Coding. Elements typed Coding 0..1 are neither an array nor a
|
|
202
|
+
// CodeableConcept — AuditEvent.entity.type in R4 is the common case — and used
|
|
203
|
+
// to fall through to [], so every slice discriminated on one matched nothing.
|
|
204
|
+
// That silently disabled the whole within-slice block: the IHE audit profiles
|
|
205
|
+
// reported "No elements matched required slice" while their max/min child
|
|
206
|
+
// constraints were never evaluated at all.
|
|
207
|
+
if (value !== null && typeof value === 'object') {
|
|
208
|
+
const lone = value as CodingShape;
|
|
209
|
+
if (lone.system !== undefined || lone.code !== undefined) return [lone];
|
|
210
|
+
}
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Hand an untyped element to a validator expecting a concrete profile type.
|
|
216
|
+
*
|
|
217
|
+
* Cross-profile delegation reaches its target through \`at()\`, which yields
|
|
218
|
+
* \`unknown\`; the callee wants e.g. \`USCorePatient\`. \`T\` is inferred from the
|
|
219
|
+
* parameter position, so the conversion needs no type import and no
|
|
220
|
+
* \`Parameters<typeof validateX>[0]\` — the latter breaks outright in packages that
|
|
221
|
+
* define a FHIR resource named \`Parameters\`, which shadows the utility type, and
|
|
222
|
+
* it rejects array-valued fields whose element type is the delegate's.
|
|
223
|
+
*
|
|
224
|
+
* Widening happens here once rather than at each call site.
|
|
225
|
+
*/
|
|
226
|
+
export function asResource<T>(value: unknown): T {
|
|
227
|
+
return value as T;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The XHTML element subset FHIR permits in Narrative.div (R4 Narrative).
|
|
232
|
+
* Anything outside this set — script, style, object, embed, iframe, form,
|
|
233
|
+
* input — is what txt-1 exists to reject.
|
|
234
|
+
*/
|
|
235
|
+
const NARRATIVE_ELEMENTS = new Set([
|
|
236
|
+
'a', 'abbr', 'acronym', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption',
|
|
237
|
+
'cite', 'code', 'col', 'colgroup', 'dd', 'dfn', 'div', 'dl', 'dt', 'em',
|
|
238
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'li', 'ol', 'p',
|
|
239
|
+
'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table',
|
|
240
|
+
'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var',
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
/** Void elements that carry meaning without text content. */
|
|
244
|
+
const NARRATIVE_VOID_ELEMENTS = /<\\s*(?:img|br|hr)\\b/i;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* txt-1 / txt-2: the narrative must hold some non-whitespace content and use
|
|
248
|
+
* only the permitted XHTML subset, with no event-handler attributes.
|
|
249
|
+
*
|
|
250
|
+
* fhirpath.js does not implement \`htmlChecks()\`, and an unimplemented function
|
|
251
|
+
* throws out of the whole evaluation, so every profile carrying txt-1 lost all of
|
|
252
|
+
* its findings. Supplying the function is what keeps the invariant evaluated.
|
|
253
|
+
*/
|
|
254
|
+
export function narrativeHtmlChecks(values: readonly unknown[]): boolean {
|
|
255
|
+
for (const value of values) {
|
|
256
|
+
if (typeof value !== 'string') continue;
|
|
257
|
+
|
|
258
|
+
// Only the permitted elements, opening or closing.
|
|
259
|
+
for (const match of value.matchAll(/<\\s*\\/?\\s*([a-zA-Z][a-zA-Z0-9]*)/g)) {
|
|
260
|
+
if (!NARRATIVE_ELEMENTS.has(match[1].toLowerCase())) return false;
|
|
261
|
+
}
|
|
262
|
+
// No inline event handlers (onclick, onload, …).
|
|
263
|
+
if (/\\son[a-zA-Z]+\\s*=/.test(value)) return false;
|
|
264
|
+
// Some non-whitespace content, unless a void element carries it.
|
|
265
|
+
const text = value.replace(/<[^>]*>/g, '').replace(/&[a-zA-Z#0-9]+;/g, ' ');
|
|
266
|
+
if (text.trim().length === 0 && !NARRATIVE_VOID_ELEMENTS.test(value)) return false;
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* FHIRPath functions fhirpath.js lacks, supplied via its userInvocationTable.
|
|
273
|
+
* Passed as \`userInvocationTable\` in the evaluation options.
|
|
274
|
+
*/
|
|
275
|
+
export const fhirpathUserFunctions = {
|
|
276
|
+
htmlChecks: { fn: narrativeHtmlChecks, arity: { 0: [] } },
|
|
277
|
+
};
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
@@ -1,3 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit the reference target-type check for one profile.
|
|
3
|
+
*
|
|
4
|
+
* Driven by a table rather than a block per element: us-core alone declares ~480
|
|
5
|
+
* reference elements with restricted targets, and inlining a check each would bloat
|
|
6
|
+
* the emitted file for no gain.
|
|
7
|
+
*
|
|
8
|
+
* The check consults options.bundleEntries and does nothing without it, which is what
|
|
9
|
+
* matches the reference validator: it reports a wrong target only when the reference
|
|
10
|
+
* resolves, and stays silent on a dangling `Patient/id-x` even though the type is
|
|
11
|
+
* spelled out there. Parsing the type out of the string instead reports the cases it
|
|
12
|
+
* ignores and misses the `urn:uuid:` ones it reports.
|
|
13
|
+
*/
|
|
14
|
+
export function buildReferenceTargetTypeValidation(fields, profileUrlToType) {
|
|
15
|
+
const resolveTargetType = (url) => {
|
|
16
|
+
const bare = url.split('|')[0];
|
|
17
|
+
const lastSegment = bare.split('/').pop() || '';
|
|
18
|
+
if (/^[A-Z][A-Za-z]+$/.test(lastSegment))
|
|
19
|
+
return lastSegment;
|
|
20
|
+
return profileUrlToType?.get(bare) ?? profileUrlToType?.get(url);
|
|
21
|
+
};
|
|
22
|
+
const rows = [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
for (const field of fields) {
|
|
25
|
+
if (field.sliceName)
|
|
26
|
+
continue;
|
|
27
|
+
const rel = (field.name || '').replace(/^[^.]+\./, '');
|
|
28
|
+
if (!rel || rel.includes('[x]') || seen.has(rel))
|
|
29
|
+
continue;
|
|
30
|
+
const targets = (field.typeOptions ?? [])
|
|
31
|
+
.filter(option => option.code === 'Reference')
|
|
32
|
+
.flatMap(option => option.targetProfileUrls ?? []);
|
|
33
|
+
if (targets.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
const resolved = targets.map(resolveTargetType);
|
|
36
|
+
// An unresolved target, or one permitting any resource, leaves the allowed set
|
|
37
|
+
// unknown — nothing can be rejected against it.
|
|
38
|
+
if (resolved.some(type => !type))
|
|
39
|
+
continue;
|
|
40
|
+
const allowed = [...new Set(resolved)];
|
|
41
|
+
if (allowed.includes('Resource') || allowed.includes('DomainResource'))
|
|
42
|
+
continue;
|
|
43
|
+
seen.add(rel);
|
|
44
|
+
rows.push(` { path: ${JSON.stringify(rel.split('.'))}, allowed: ${JSON.stringify(allowed)} },`);
|
|
45
|
+
}
|
|
46
|
+
if (rows.length === 0)
|
|
47
|
+
return '';
|
|
48
|
+
return `
|
|
49
|
+
// Reference target types, checked only where a reference resolves
|
|
50
|
+
if (options?.bundleEntries) {
|
|
51
|
+
const _rtTargets: ReadonlyArray<{ path: string[]; allowed: string[] }> = [
|
|
52
|
+
${rows.join('\n')}
|
|
53
|
+
];
|
|
54
|
+
for (const _rt of _rtTargets) {
|
|
55
|
+
for (const _rtRef of refsAt(resource, _rt.path)) {
|
|
56
|
+
const _rtFound = options.bundleEntries[_rtRef];
|
|
57
|
+
if (_rtFound !== undefined && !_rt.allowed.includes(_rtFound)) {
|
|
58
|
+
errors.push("Invalid Resource target type. Found " + _rtFound + ", but expected one of ([" + _rt.allowed.join(', ') + "])");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Emit the resolution map a Bundle passes to the validators it delegates to.
|
|
67
|
+
*
|
|
68
|
+
* Keyed by fullUrl and by \`Type/id\`, since a reference may use either form. Only
|
|
69
|
+
* Bundles can populate it, so only Bundles emit it.
|
|
70
|
+
*/
|
|
71
|
+
export function buildBundleEntryResolution(baseResourceType) {
|
|
72
|
+
if (baseResourceType !== 'Bundle') {
|
|
73
|
+
// Outside a Bundle there is nothing to resolve against, so delegates inherit the
|
|
74
|
+
// caller's options unchanged — including any map the caller was itself given.
|
|
75
|
+
return `
|
|
76
|
+
const _delegateOptions = options;
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
return `
|
|
80
|
+
// Resolution map for delegated validators: fullUrl and Type/id → resourceType
|
|
81
|
+
const _bundleEntries: Record<string, string> = {};
|
|
82
|
+
for (const _be of arr(at(resource, 'entry'))) {
|
|
83
|
+
const _beType = at(_be, 'resource', 'resourceType');
|
|
84
|
+
if (typeof _beType !== 'string') continue;
|
|
85
|
+
const _beFullUrl = at(_be, 'fullUrl');
|
|
86
|
+
if (typeof _beFullUrl === 'string') _bundleEntries[_beFullUrl] = _beType;
|
|
87
|
+
const _beId = at(_be, 'resource', 'id');
|
|
88
|
+
if (typeof _beId === 'string') _bundleEntries[_beType + '/' + _beId] = _beType;
|
|
89
|
+
}
|
|
90
|
+
const _delegateOptions = { ...options, bundleEntries: _bundleEntries };
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
1
93
|
/**
|
|
2
94
|
* Generate Bundle reference resolution validation code.
|
|
3
95
|
* Only applies to Bundle profiles — verifies all references within entries are resolvable.
|
|
@@ -93,6 +185,45 @@ export function generateContainedRefValidation(baseResourceType) {
|
|
|
93
185
|
}
|
|
94
186
|
};
|
|
95
187
|
_checkContainedRef(_res);
|
|
188
|
+
|
|
189
|
+
// dom-3: every contained resource must be referenced from the containing
|
|
190
|
+
// resource. Evaluated here rather than through its FHIRPath form: that
|
|
191
|
+
// expression builds the fragment as '#'+id, so an absent id makes the whole
|
|
192
|
+
// predicate empty and the invariant silently passes, where HL7 reports the
|
|
193
|
+
// resource as unreferenced (its message names it 'null').
|
|
194
|
+
if (Array.isArray(_res.contained) && _res.contained.length > 0) {
|
|
195
|
+
const _fragments = new Set<string>();
|
|
196
|
+
const _collectFragments = (obj: unknown): void => {
|
|
197
|
+
if (!obj || typeof obj !== 'object') return;
|
|
198
|
+
if (Array.isArray(obj)) { obj.forEach(_collectFragments); return; }
|
|
199
|
+
for (const [_k, _v] of Object.entries(obj as Record<string, unknown>)) {
|
|
200
|
+
if (_k === 'reference' && typeof _v === 'string' && _v.startsWith('#')) {
|
|
201
|
+
_fragments.add(_v.substring(1));
|
|
202
|
+
} else {
|
|
203
|
+
_collectFragments(_v);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
_collectFragments(_res);
|
|
208
|
+
// The invariant is satisfied either way round, so a contained resource that
|
|
209
|
+
// points back at its container with the bare '#' reference also passes.
|
|
210
|
+
const _refersToContainer = (obj: unknown): boolean => {
|
|
211
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
212
|
+
if (Array.isArray(obj)) return obj.some(_refersToContainer);
|
|
213
|
+
for (const [_k, _v] of Object.entries(obj as Record<string, unknown>)) {
|
|
214
|
+
if (_k === 'reference' && _v === '#') return true;
|
|
215
|
+
if (_refersToContainer(_v)) return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
};
|
|
219
|
+
for (const _c of _res.contained as Array<Record<string, unknown>>) {
|
|
220
|
+
const _cid = _c && typeof _c.id === 'string' ? _c.id : undefined;
|
|
221
|
+
if (_refersToContainer(_c)) continue;
|
|
222
|
+
if (_cid === undefined || !_fragments.has(_cid)) {
|
|
223
|
+
errors.push("The contained resource '" + (_cid ?? 'null') + "' is not referenced to from elsewhere in the containing resource nor does it refer to the containing resource (dom-3)");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
96
227
|
}
|
|
97
228
|
`;
|
|
98
229
|
}
|
|
@@ -125,6 +256,10 @@ export function generateExtensionStructuralValidation() {
|
|
|
125
256
|
const e = ext as Record<string, unknown>;
|
|
126
257
|
if (typeof e.url !== 'string' || !e.url) {
|
|
127
258
|
errors.push('Extension.url is required in order to identify, use and validate the extension');
|
|
259
|
+
} else if (e.url.includes('|')) {
|
|
260
|
+
// Extension.url identifies the definition and is not version-pinned;
|
|
261
|
+
// a '|version' suffix belongs on a canonical reference, not here.
|
|
262
|
+
errors.push('The extension URL must not contain a version');
|
|
128
263
|
}
|
|
129
264
|
// ext-1: Must have either extensions or value[x], not both
|
|
130
265
|
const hasNestedExt = Array.isArray(e.extension) && e.extension.length > 0;
|
|
@@ -147,69 +282,66 @@ export function generateExtensionStructuralValidation() {
|
|
|
147
282
|
}
|
|
148
283
|
`;
|
|
149
284
|
}
|
|
285
|
+
/** Per-aggregation error text and the predicate that marks a reference unresolvable. */
|
|
286
|
+
const AGGREGATION_RULES = {
|
|
287
|
+
// A bundled reference resolves only inside a Bundle, so a relative
|
|
288
|
+
// ResourceType/id reference is unresolvable in standalone validation.
|
|
289
|
+
bundled: {
|
|
290
|
+
message: 'Bundle reference not found: ',
|
|
291
|
+
violates: (ref) => `/^[A-Za-z]+\\//.test(${ref})`,
|
|
292
|
+
},
|
|
293
|
+
// A contained reference must be a local #fragment. Fragments that do not
|
|
294
|
+
// resolve are already reported by generateContainedRefValidation, so this
|
|
295
|
+
// only catches references that are not contained at all.
|
|
296
|
+
contained: {
|
|
297
|
+
message: 'Contained reference not found: ',
|
|
298
|
+
violates: (ref) => `!${ref}.startsWith('#')`,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
150
301
|
/**
|
|
151
|
-
* Generate validation for reference fields
|
|
302
|
+
* Generate validation for reference fields whose profile restricts aggregation.
|
|
152
303
|
*
|
|
153
|
-
* The HL7 validator enforces that
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* be reported as an error to match HL7 parity.
|
|
304
|
+
* The HL7 validator enforces that such references resolve within the bundle or
|
|
305
|
+
* resource; when validating a standalone resource, a reference of the wrong form
|
|
306
|
+
* is unresolvable and must be reported to match HL7 parity.
|
|
157
307
|
*
|
|
158
|
-
* Bundle profiles handle
|
|
308
|
+
* Bundle profiles handle the bundled case via {@link generateBundleRefValidation}.
|
|
159
309
|
*/
|
|
160
|
-
export function
|
|
310
|
+
export function generateAggregationRefValidation(paths) {
|
|
161
311
|
if (paths.length === 0)
|
|
162
312
|
return '';
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
checks.push(`
|
|
180
|
-
if (_res.${f} && ${_checkRef('_res.' + f)}) {
|
|
181
|
-
errors.push('Bundle reference not found: ' + _res.${f}.reference);
|
|
182
|
-
}`);
|
|
183
|
-
}
|
|
313
|
+
/**
|
|
314
|
+
* Emit the descent into `segments`, iterating any array level, then check the
|
|
315
|
+
* leaf reference. Uses at()/arr() so every hop stays typed as unknown.
|
|
316
|
+
* Consecutive single-valued segments collapse into one variadic at() call.
|
|
317
|
+
*/
|
|
318
|
+
const walk = (path, index, expr, pending, indent, id) => {
|
|
319
|
+
const { segments, isArray, kind } = path;
|
|
320
|
+
const asArgs = (parts) => parts.map(p => `, '${p}'`).join('');
|
|
321
|
+
if (index === segments.length) {
|
|
322
|
+
const { message, violates } = AGGREGATION_RULES[kind];
|
|
323
|
+
const refVar = `_aggRef${id}`;
|
|
324
|
+
return `
|
|
325
|
+
${indent}const ${refVar} = at(${expr}${asArgs([...pending, 'reference'])});
|
|
326
|
+
${indent}if (typeof ${refVar} === 'string' && ${violates(refVar)}) {
|
|
327
|
+
${indent} errors.push('${message}' + ${refVar});
|
|
328
|
+
${indent}}`;
|
|
184
329
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
for (const _item of _res.${parent}) {
|
|
192
|
-
if (_item?.${child} && ${_checkRef('_item.' + child)}) {
|
|
193
|
-
errors.push('Bundle reference not found: ' + _item.${child}.reference);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}`);
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
checks.push(`
|
|
200
|
-
if (_res.${parent}?.${child} && ${_checkRef('_res.' + parent + '.' + child)}) {
|
|
201
|
-
errors.push('Bundle reference not found: ' + _res.${parent}.${child}.reference);
|
|
202
|
-
}`);
|
|
203
|
-
}
|
|
330
|
+
const segment = segments[index];
|
|
331
|
+
if (isArray[index]) {
|
|
332
|
+
const itemVar = `_aggItem${id}${index}`;
|
|
333
|
+
return `
|
|
334
|
+
${indent}for (const ${itemVar} of arr(at(${expr}${asArgs([...pending, segment])}))) {${walk(path, index + 1, itemVar, [], indent + ' ', `${id}${index}`)}
|
|
335
|
+
${indent}}`;
|
|
204
336
|
}
|
|
205
|
-
|
|
206
|
-
}
|
|
337
|
+
return walk(path, index + 1, expr, [...pending, segment], indent, `${id}${index}`);
|
|
338
|
+
};
|
|
339
|
+
const checks = paths.map((path, i) => walk(path, 0, 'resource', [], ' ', String(i)));
|
|
207
340
|
return `
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const _res = resource as unknown as Record<string, any>;${checks.join('')}
|
|
341
|
+
// Restrictive reference aggregation: a reference on a field limited to
|
|
342
|
+
// bundled/contained must resolve within the bundle/resource. For standalone
|
|
343
|
+
// validation, a reference of the wrong form is an error.
|
|
344
|
+
{${checks.join('')}
|
|
213
345
|
}
|
|
214
346
|
`;
|
|
215
347
|
}
|