babelfhir-ts 1.3.4 β 1.3.6
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/README.md +1 -1
- package/out/src/generator/emitters/class/sliceElementGenerator.js +6 -1
- package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +110 -10
- package/out/src/generator/emitters/validator/validatorBindingBuilder.js +53 -3
- package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +58 -1
- package/out/src/generator/emitters/validator/validatorGenerator.js +34 -7
- package/out/src/generator/emitters/validator/validatorTemplates.js +8 -9
- package/out/src/generator/fhir/r4/base/well-known-system-codes.json +413 -2
- package/out/src/generator/parser/packageParser.js +34 -7
- package/out/src/generator/parser/sdParser.js +1 -1
- package/out/src/generator/parser/sliceAggregator.js +10 -2
- package/out/src/generator/parser/vsParser.js +10 -13
- package/out/src/generator/sdProcessor.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,7 +160,7 @@ The second pipeline validates using the [official HL7 FHIR Validator](https://co
|
|
|
160
160
|

|
|
161
161
|

|
|
162
162
|
|
|
163
|
-
> Terminology validation requires a tx server. The pipeline uses `--tx-server https://tx.fhir.org/r4` during generation to expand ValueSets and produce valid codes.
|
|
163
|
+
> Terminology validation requires a tx server. The pipeline uses `--tx-server https://tx.fhir.org/{r4|r5}` (matching the package's FHIR version) during generation to expand ValueSets and produce valid codes.
|
|
164
164
|
|
|
165
165
|
π **[Full Report](https://max-health-inc.github.io/BabelFHIR-TS/)**
|
|
166
166
|
<!-- HL7-PARITY-BADGES:END -->
|
|
@@ -158,9 +158,14 @@ export function generateSliceElement(slice, elementType) {
|
|
|
158
158
|
log.debug(`Resource child - sliceName: ${sliceName}, profileUrls: ${JSON.stringify(profileUrls)}, profileUrl: ${profileUrl}`);
|
|
159
159
|
const metaProfile = profileUrl ? `, meta: { profile: ['${profileUrl}'] }` : '';
|
|
160
160
|
let resourceType = 'Composition';
|
|
161
|
-
|
|
161
|
+
// Prefer concrete baseTypeCode (skip generic 'Resource'), then slice name
|
|
162
|
+
// (always the target resource type, e.g., 'Composition'), then profile URL.
|
|
163
|
+
if (child.baseTypeCode && child.baseTypeCode !== 'Resource') {
|
|
162
164
|
resourceType = capitalise(child.baseTypeCode);
|
|
163
165
|
}
|
|
166
|
+
else if (/^[A-Z][a-zA-Z]+$/.test(slice.name || '')) {
|
|
167
|
+
resourceType = slice.name;
|
|
168
|
+
}
|
|
164
169
|
else if (profileUrl) {
|
|
165
170
|
const profileName = profileUrl.split('/').pop() || '';
|
|
166
171
|
if (profileName.includes('-')) {
|
|
@@ -127,18 +127,24 @@ export function generateSliceValidations(ctx) {
|
|
|
127
127
|
generateExtensionUrlSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, extensionUrl, fieldPathMap, arrayFieldPaths, sliceValidations);
|
|
128
128
|
}
|
|
129
129
|
else {
|
|
130
|
-
|
|
130
|
+
const profileMatch = detectProfileDiscriminator(slice, fields);
|
|
131
|
+
if (profileMatch) {
|
|
132
|
+
generateProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, profileMatch, sliceValidations, ctx);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
sliceValidations.push(`
|
|
131
136
|
// Slice validation for ${relPath}:${sliceLabel} skipped: no pattern/value discriminator found
|
|
132
137
|
// Discriminator types 'type', 'profile', 'exists', 'position' are not yet supported`);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
addDiagnostic({
|
|
139
|
+
profileUrl: ctx.profileUrl || '',
|
|
140
|
+
profileName: ctx.profileName || '',
|
|
141
|
+
severity: 'warning',
|
|
142
|
+
category: 'discriminator',
|
|
143
|
+
elementPath: `${relPath}:${sliceLabel}`,
|
|
144
|
+
message: `Slice "${sliceLabel}" on "${relPath}" has no pattern/value discriminator β only type/profile/exists/position which are not yet evaluable.`,
|
|
145
|
+
impact: 'Strict validators may reject valid resources because they cannot match this slice discriminator.',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
}
|
|
@@ -382,3 +388,97 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
|
|
|
382
388
|
}
|
|
383
389
|
}
|
|
384
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Detect whether a slice uses a `profile` discriminator on a `.resource` child.
|
|
393
|
+
* Returns the expected resourceType and profile URLs if found, undefined otherwise.
|
|
394
|
+
*
|
|
395
|
+
* Heuristic: the child field `<sliceElementId>.resource` with `profileUrls`
|
|
396
|
+
* indicates a profile-based entry slice. The resource type is derived from
|
|
397
|
+
* the profile URL (last path segment, PascalCase) or the slice name itself
|
|
398
|
+
* when the profile URL belongs to a non-core IG.
|
|
399
|
+
*/
|
|
400
|
+
function detectProfileDiscriminator(slice, fields) {
|
|
401
|
+
const sliceElemId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
|
|
402
|
+
const resourceChild = fields.find(f => {
|
|
403
|
+
const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
|
|
404
|
+
return fId === `${sliceElemId}.resource`;
|
|
405
|
+
});
|
|
406
|
+
if (!resourceChild?.profileUrls?.length)
|
|
407
|
+
return undefined;
|
|
408
|
+
const profileUrls = resourceChild.profileUrls;
|
|
409
|
+
// Derive base resource type from the profile URL: the last path segment of a
|
|
410
|
+
// core FHIR profile IS the resource type (e.g., .../StructureDefinition/Patient β Patient).
|
|
411
|
+
// For IG-specific profiles (e.g., .../ch-core-composition), the slice name is
|
|
412
|
+
// typically the resource type (e.g., "Composition"), so prefer that.
|
|
413
|
+
const sliceName = slice.sliceName || '';
|
|
414
|
+
// Validate: slice name should start with uppercase and be a plausible resource type
|
|
415
|
+
if (/^[A-Z][a-zA-Z]+$/.test(sliceName)) {
|
|
416
|
+
return { resourceType: sliceName, profileUrls };
|
|
417
|
+
}
|
|
418
|
+
// Fallback: try to extract from profile URL
|
|
419
|
+
const profileUrl = profileUrls[0];
|
|
420
|
+
const lastSegment = profileUrl.split('/').pop() || '';
|
|
421
|
+
if (/^[A-Z][a-zA-Z]+$/.test(lastSegment)) {
|
|
422
|
+
return { resourceType: lastSegment, profileUrls };
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Generate slice validation for profile-discriminated entry slices.
|
|
428
|
+
* Matches Bundle entries by `entry[i].resource.resourceType`, enforces min/max,
|
|
429
|
+
* and delegates to the profiled validator when the profile URL is in profileUrlToName.
|
|
430
|
+
*/
|
|
431
|
+
function generateProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, match, out, ctx) {
|
|
432
|
+
const { resourceType, profileUrls } = match;
|
|
433
|
+
const max = typeof slice.max === 'number' && slice.max !== Infinity ? slice.max : undefined;
|
|
434
|
+
const parentPath = relPath.includes('.') ? relPath.split('.').slice(0, -1).join('.') : '';
|
|
435
|
+
const accessor = parentPath ? `(resource as any)?.${parentPath.replace(/\./g, '?.')}` : `resource.${relPath}`;
|
|
436
|
+
// For top-level entry slices, access resource.entry directly
|
|
437
|
+
const arrayAccessor = relPath === 'entry' || !relPath.includes('.') ? `resource.${relPath}` : accessor;
|
|
438
|
+
// Resolve profiled validator for delegation
|
|
439
|
+
let delegateInterfaceName;
|
|
440
|
+
if (ctx.profileUrlToName && ctx.validatorImports) {
|
|
441
|
+
for (const pUrl of profileUrls) {
|
|
442
|
+
const name = ctx.profileUrlToName.get(pUrl);
|
|
443
|
+
if (name && name !== ctx.profileName) {
|
|
444
|
+
delegateInterfaceName = name;
|
|
445
|
+
ctx.validatorImports.set(name, `./${name}.js`);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// When delegating, annotate the array so .resource is accessible without casts
|
|
451
|
+
const entriesType = delegateInterfaceName ? ': Array<Record<string, any>>' : '';
|
|
452
|
+
let code = `
|
|
453
|
+
// Required slice validation for ${relPath}:${sliceLabel} (profile discriminator β ${resourceType})
|
|
454
|
+
{
|
|
455
|
+
const _${varName}Entries${entriesType} = (Array.isArray(${arrayAccessor}) ? ${arrayAccessor} : []).filter(
|
|
456
|
+
(e: any) => e?.resource?.resourceType === '${resourceType}'
|
|
457
|
+
);`;
|
|
458
|
+
if (min > 0) {
|
|
459
|
+
code += `
|
|
460
|
+
if (_${varName}Entries.length < ${min}) {
|
|
461
|
+
errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
|
|
462
|
+
}`;
|
|
463
|
+
}
|
|
464
|
+
if (max !== undefined) {
|
|
465
|
+
code += `
|
|
466
|
+
if (_${varName}Entries.length > ${max}) {
|
|
467
|
+
errors.push("${errorPath}:${sliceLabel}: max allowed = ${max}, but found " + _${varName}Entries.length);
|
|
468
|
+
}`;
|
|
469
|
+
}
|
|
470
|
+
// Delegate to profiled validator for each matched entry
|
|
471
|
+
if (delegateInterfaceName) {
|
|
472
|
+
code += `
|
|
473
|
+
for (const _e of _${varName}Entries) {
|
|
474
|
+
if (_e.resource) {
|
|
475
|
+
const _sub = await validate${delegateInterfaceName}(_e.resource, options);
|
|
476
|
+
errors.push(..._sub.errors);
|
|
477
|
+
warnings.push(..._sub.warnings);
|
|
478
|
+
}
|
|
479
|
+
}`;
|
|
480
|
+
}
|
|
481
|
+
code += `
|
|
482
|
+
}`;
|
|
483
|
+
out.push(code);
|
|
484
|
+
}
|
|
@@ -49,7 +49,7 @@ export function buildBindingValidations(fields, fieldParentMap, arrayFieldPaths,
|
|
|
49
49
|
// ββ System-presence fallback ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
50
50
|
function emitSystemPresenceFallback(output, field, rel, isNested, arrayFieldPaths) {
|
|
51
51
|
const type = field.type || field.baseTypeCode;
|
|
52
|
-
if (type !== 'CodeableConcept' && type !== 'Coding')
|
|
52
|
+
if (type !== 'CodeableConcept' && type !== 'Coding' && type !== 'Quantity')
|
|
53
53
|
return;
|
|
54
54
|
const vsUri = (field.binding.uri || '').split('|')[0];
|
|
55
55
|
const vsDisplayName = vsUri.split('/').pop() || vsUri;
|
|
@@ -87,7 +87,7 @@ function emitTopLevelSystemPresence(output, field, rel, type, vsDisplayName, vsU
|
|
|
87
87
|
}`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
else {
|
|
90
|
+
else if (type === 'Coding') {
|
|
91
91
|
if (field.isArray) {
|
|
92
92
|
output.push(`
|
|
93
93
|
// Required binding system-presence fallback for ${rel} (Coding array, ValueSet unresolved)
|
|
@@ -111,11 +111,35 @@ function emitTopLevelSystemPresence(output, field, rel, type, vsDisplayName, vsU
|
|
|
111
111
|
}`);
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
else if (type === 'Quantity') {
|
|
115
|
+
if (field.isArray) {
|
|
116
|
+
output.push(`
|
|
117
|
+
// Required binding system-presence fallback for ${rel} (Quantity array, ValueSet unresolved)
|
|
118
|
+
if (_bRes.${rel} && Array.isArray(_bRes.${rel})) {
|
|
119
|
+
for (const _spQ of _bRes.${rel}) {
|
|
120
|
+
const _q = _spQ as { code?: string; system?: string };
|
|
121
|
+
if (_q.code !== undefined && (_q.system === undefined || _q.system === '')) {
|
|
122
|
+
errors.push("The System URI could not be determined for the code '" + _q.code + "' in the ValueSet '${vsDisplayName}' (${vsUri})");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
output.push(`
|
|
129
|
+
// Required binding system-presence fallback for ${rel} (Quantity, ValueSet unresolved)
|
|
130
|
+
if (_bRes.${rel}) {
|
|
131
|
+
const _q = _bRes.${rel} as { code?: string; system?: string };
|
|
132
|
+
if (_q.code !== undefined && (_q.system === undefined || _q.system === '')) {
|
|
133
|
+
errors.push("The System URI could not be determined for the code '" + _q.code + "' in the ValueSet '${vsDisplayName}' (${vsUri})");
|
|
134
|
+
}
|
|
135
|
+
}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
114
138
|
}
|
|
115
139
|
// ββ System-only binding βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
116
140
|
function emitSystemOnlyBinding(output, field, rel, isNested, vsInfo, arrayFieldPaths) {
|
|
117
141
|
const type = field.type || field.baseTypeCode;
|
|
118
|
-
if (type !== 'CodeableConcept' && type !== 'Coding')
|
|
142
|
+
if (type !== 'CodeableConcept' && type !== 'Coding' && type !== 'Quantity')
|
|
119
143
|
return;
|
|
120
144
|
const sysVsUri = (field.binding.uri || '').split('|')[0];
|
|
121
145
|
const systemsLiteral = JSON.stringify(vsInfo.systems);
|
|
@@ -188,6 +212,32 @@ function emitTopLevelSystemOnly(output, field, rel, type, vsDisplayName, sysVsUr
|
|
|
188
212
|
}`);
|
|
189
213
|
}
|
|
190
214
|
}
|
|
215
|
+
else if (type === 'Quantity') {
|
|
216
|
+
if (field.isArray) {
|
|
217
|
+
output.push(`
|
|
218
|
+
// Required ValueSet system-level binding validation for ${rel} (Quantity array)
|
|
219
|
+
if (_bRes.${rel} && Array.isArray(_bRes.${rel})) {
|
|
220
|
+
for (const _sysQ of _bRes.${rel}) {
|
|
221
|
+
const _q = _sysQ as { code?: string; system?: string };
|
|
222
|
+
const _allowedSystems = ${systemsLiteral};
|
|
223
|
+
if (_q.code !== undefined && _q.system !== undefined && !_allowedSystems.includes(_q.system)) {
|
|
224
|
+
errors.push("Code '" + _q.code + "' from system '" + _q.system + "' does not exist in the value set '${vsDisplayName}' (${sysVsUri}), but the binding is of strength 'required'");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
output.push(`
|
|
231
|
+
// Required ValueSet system-level binding validation for ${rel} (Quantity)
|
|
232
|
+
if (_bRes.${rel}) {
|
|
233
|
+
const _q = _bRes.${rel} as { code?: string; system?: string };
|
|
234
|
+
const _allowedSystems = ${systemsLiteral};
|
|
235
|
+
if (_q.code !== undefined && _q.system !== undefined && !_allowedSystems.includes(_q.system)) {
|
|
236
|
+
errors.push("Code '" + _q.code + "' from system '" + _q.system + "' does not exist in the value set '${vsDisplayName}' (${sysVsUri}), but the binding is of strength 'required'");
|
|
237
|
+
}
|
|
238
|
+
}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
191
241
|
}
|
|
192
242
|
// ββ Resolved code-level binding βββββββββββββββββββββββββββββββββββββββββββββ
|
|
193
243
|
function emitResolvedBinding(output, field, rel, isNested, vsInfo, arrayFieldPaths) {
|
|
@@ -74,7 +74,7 @@ export function emitSystemPresenceLeaf(leafAcc, indent, type, vsDisplayName, vsU
|
|
|
74
74
|
code += `\n${indent}}`;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
else {
|
|
77
|
+
else if (type === 'Coding') {
|
|
78
78
|
if (isArray) {
|
|
79
79
|
code += `\n${indent}if (${leafAcc} && Array.isArray(${leafAcc})) {`;
|
|
80
80
|
indent += ' ';
|
|
@@ -99,6 +99,31 @@ export function emitSystemPresenceLeaf(leafAcc, indent, type, vsDisplayName, vsU
|
|
|
99
99
|
code += `\n${indent}}`;
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
else if (type === 'Quantity') {
|
|
103
|
+
if (isArray) {
|
|
104
|
+
code += `\n${indent}if (${leafAcc} && Array.isArray(${leafAcc})) {`;
|
|
105
|
+
indent += ' ';
|
|
106
|
+
code += `\n${indent}for (const _spQ of ${leafAcc}) {`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
code += `\n${indent}if (${leafAcc} !== undefined) {`;
|
|
110
|
+
indent += ' ';
|
|
111
|
+
code += `\n${indent}const _spQ = ${leafAcc};`;
|
|
112
|
+
}
|
|
113
|
+
indent += ' ';
|
|
114
|
+
code += `\n${indent}const _q = _spQ as { code?: string; system?: string };`;
|
|
115
|
+
code += `\n${indent}if (_q.code !== undefined && (_q.system === undefined || _q.system === '')) {`;
|
|
116
|
+
indent += ' ';
|
|
117
|
+
code += `\n${indent}errors.push("The System URI could not be determined for the code '" + _q.code + "' in the ValueSet '${vsDisplayName}' (${vsUri})");`;
|
|
118
|
+
indent = indent.slice(2);
|
|
119
|
+
code += `\n${indent}}`;
|
|
120
|
+
indent = indent.slice(2);
|
|
121
|
+
code += `\n${indent}}`;
|
|
122
|
+
if (isArray) {
|
|
123
|
+
indent = indent.slice(2);
|
|
124
|
+
code += `\n${indent}}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
102
127
|
return code;
|
|
103
128
|
}
|
|
104
129
|
export function emitSystemOnlyLeaf(leafAcc, indent, type, vsDisplayName, sysVsUri, systemsLiteral, isArray) {
|
|
@@ -177,6 +202,38 @@ export function emitSystemOnlyLeaf(leafAcc, indent, type, vsDisplayName, sysVsUr
|
|
|
177
202
|
code += `\n${indent}}`;
|
|
178
203
|
}
|
|
179
204
|
}
|
|
205
|
+
else if (type === 'Quantity') {
|
|
206
|
+
if (isArray) {
|
|
207
|
+
code += `\n${indent}if (${leafAcc} && Array.isArray(${leafAcc})) {`;
|
|
208
|
+
indent += ' ';
|
|
209
|
+
code += `\n${indent}for (const _sysQ of ${leafAcc}) {`;
|
|
210
|
+
indent += ' ';
|
|
211
|
+
code += `\n${indent}const _q = _sysQ as { code?: string; system?: string };`;
|
|
212
|
+
code += `\n${indent}const _allowedSystems = ${systemsLiteral};`;
|
|
213
|
+
code += `\n${indent}if (_q.code !== undefined && _q.system !== undefined && !_allowedSystems.includes(_q.system)) {`;
|
|
214
|
+
indent += ' ';
|
|
215
|
+
code += `\n${indent}errors.push("Code '" + _q.code + "' from system '" + _q.system + "' does not exist in the value set '${vsDisplayName}' (${sysVsUri}), but the binding is of strength 'required'");`;
|
|
216
|
+
indent = indent.slice(2);
|
|
217
|
+
code += `\n${indent}}`;
|
|
218
|
+
indent = indent.slice(2);
|
|
219
|
+
code += `\n${indent}}`;
|
|
220
|
+
indent = indent.slice(2);
|
|
221
|
+
code += `\n${indent}}`;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
code += `\n${indent}{`;
|
|
225
|
+
indent += ' ';
|
|
226
|
+
code += `\n${indent}const _q = (${leafAcc} as any);`;
|
|
227
|
+
code += `\n${indent}const _allowedSystems = ${systemsLiteral};`;
|
|
228
|
+
code += `\n${indent}if (_q?.code !== undefined && _q?.system !== undefined && !_allowedSystems.includes(_q.system)) {`;
|
|
229
|
+
indent += ' ';
|
|
230
|
+
code += `\n${indent}errors.push("Code '" + _q.code + "' from system '" + _q.system + "' does not exist in the value set '${vsDisplayName}' (${sysVsUri}), but the binding is of strength 'required'");`;
|
|
231
|
+
indent = indent.slice(2);
|
|
232
|
+
code += `\n${indent}}`;
|
|
233
|
+
indent = indent.slice(2);
|
|
234
|
+
code += `\n${indent}}`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
180
237
|
return code;
|
|
181
238
|
}
|
|
182
239
|
export function emitResolvedLeaf(leafAcc, indent, type, vsName, vsDisplayName, vsUri, isArray) {
|
|
@@ -27,8 +27,9 @@ export interface ValidatorOptions {
|
|
|
27
27
|
}
|
|
28
28
|
`;
|
|
29
29
|
}
|
|
30
|
-
export function generateValidateProfileFunction(interfaceName, fields, valueSets, profileUrl, baseResourceType) {
|
|
30
|
+
export function generateValidateProfileFunction(interfaceName, fields, valueSets, profileUrl, baseResourceType, profileUrlToName) {
|
|
31
31
|
const valueSetImports = new Map();
|
|
32
|
+
const validatorImports = new Map();
|
|
32
33
|
// ββ Collect field-level constraints ββββββββββββββββββββββββββββββββββββββ
|
|
33
34
|
const constraintsWithContext = [];
|
|
34
35
|
fields.forEach((field) => {
|
|
@@ -143,6 +144,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
143
144
|
const sliceValidations = generateSliceValidations({
|
|
144
145
|
fields, fieldPathMap, arrayFieldPaths, getValueSetForBinding,
|
|
145
146
|
valueSetImports, profileUrl, profileName: interfaceName, baseResourceType,
|
|
147
|
+
profileUrlToName, validatorImports,
|
|
146
148
|
});
|
|
147
149
|
const primitiveFormatValidations = buildPrimitiveFormatValidations(fields);
|
|
148
150
|
const bundleRefValidation = generateBundleRefValidation(baseResourceType);
|
|
@@ -153,14 +155,23 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
153
155
|
const filteredConstraints = uniqueConstraints.filter(c => {
|
|
154
156
|
const expr = c.constraint.expression || "";
|
|
155
157
|
const fieldPath = c.fieldPath || "";
|
|
158
|
+
// Filter out text.div-related expressions (narrative HTML), XPath-style
|
|
159
|
+
// variables ($Aβ¦$Z), and value[x] type-discriminator patterns.
|
|
156
160
|
if (/(text\.div\b|\.all\(div\b|\$[A-Z]|value\.[A-Z])/.test(expr))
|
|
157
161
|
return false;
|
|
162
|
+
// Filter $this on choice-type fields β fhirpath scoping differs from HL7.
|
|
158
163
|
if (c.isChoiceType && /\$this\b/.test(expr))
|
|
159
164
|
return false;
|
|
165
|
+
// For nested fields (depth > 1), filter only base FHIR invariants whose
|
|
166
|
+
// evaluation diverges between fhirpath.js and HL7:
|
|
167
|
+
// ele-1: hasValue() or (children().count() > id.count())
|
|
168
|
+
// ext-1: extension.exists() != value.exists()
|
|
169
|
+
// Profile-specific constraints at nested depth are allowed through β
|
|
170
|
+
// the .all() wrapping handles scoping correctly for these.
|
|
160
171
|
if (fieldPath && fieldPath.split('.').length > 1) {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
172
|
+
const isEle1 = /^hasValue\(\)\s+or\s+\(children\(\)\.count\(\)\s*>\s*id\.count\(\)\)$/.test(expr);
|
|
173
|
+
const isExt1 = /^extension\.exists\(\)\s*!=\s*value\.exists\(\)$/.test(expr);
|
|
174
|
+
if (isEle1 || isExt1)
|
|
164
175
|
return false;
|
|
165
176
|
}
|
|
166
177
|
return true;
|
|
@@ -198,20 +209,30 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
198
209
|
}
|
|
199
210
|
}
|
|
200
211
|
const needsTerminology = /\bmemberOf\b/.test(constraint.expression || '');
|
|
201
|
-
const needsResolve = /\bresolve\b/.test(constraint.expression || '');
|
|
202
212
|
let evaluateBlock = `
|
|
203
213
|
const result${index} = await fhirpath.evaluate(resource, "${escapedExpression}", { resource }, fhirpath_model, fhirpathOptions);
|
|
204
214
|
if (!result${index}.every(Boolean)) {
|
|
205
215
|
${constraint.severity === "error" ? "errors" : "warnings"}.push("Constraint violation: ${escapedHuman}");
|
|
206
216
|
}`;
|
|
217
|
+
// Gate memberOf() behind terminologyUrl. For resolve(), use try/catch
|
|
218
|
+
// instead of a hard gate: FHIRPath `and` short-circuits, so expressions
|
|
219
|
+
// like `(A and resolve().is(X)) or (B and focus.exists().not())` can still
|
|
220
|
+
// evaluate the resolve-free branch when A is false. If resolve() IS
|
|
221
|
+
// reached without a fhirServerUrl, fhirpath.js throws β catch it and
|
|
222
|
+
// degrade to a warning instead of silently skipping the entire constraint.
|
|
223
|
+
const needsResolve = /\bresolve\b/.test(constraint.expression || '');
|
|
207
224
|
if (needsTerminology) {
|
|
208
225
|
evaluateBlock = `
|
|
209
226
|
if (options?.terminologyUrl) {${evaluateBlock}
|
|
227
|
+
} else {
|
|
228
|
+
warnings.push("Constraint not evaluated (no terminologyUrl): ${escapedHuman}");
|
|
210
229
|
}`;
|
|
211
230
|
}
|
|
212
231
|
else if (needsResolve) {
|
|
213
232
|
evaluateBlock = `
|
|
214
|
-
|
|
233
|
+
try {${evaluateBlock}
|
|
234
|
+
} catch {
|
|
235
|
+
warnings.push("Constraint partially not evaluated (resolve requires fhirServerUrl): ${escapedHuman}");
|
|
215
236
|
}`;
|
|
216
237
|
}
|
|
217
238
|
return evaluateBlock;
|
|
@@ -260,9 +281,15 @@ ${extensionStructuralValidation}${containedRefValidation}
|
|
|
260
281
|
.map(([name, path]) => `import { isValid${name}Code } from '${path}';`)
|
|
261
282
|
.join('\n') + '\n\n';
|
|
262
283
|
}
|
|
284
|
+
let validatorImportStatements = '';
|
|
285
|
+
if (validatorImports.size > 0) {
|
|
286
|
+
validatorImportStatements = Array.from(validatorImports.entries())
|
|
287
|
+
.map(([name, importPath]) => `import { validate${name} } from '${importPath}';`)
|
|
288
|
+
.join('\n') + '\n\n';
|
|
289
|
+
}
|
|
263
290
|
const fixedPatternValidations = patternValidations.map(v => v.replace(/if \(resource\.(\w+)\.(\w+)/g, (_match, parent, child) => `if (resource.${parent} && resource.${parent}.${child}`));
|
|
264
291
|
return {
|
|
265
|
-
code: `${fhirpathImport}${optionsImport}${valueSetImportStatements}export async function validate${interfaceName}(resource: ${interfaceName}, options?: ValidatorOptions): Promise<{ errors: string[]; warnings: string[] }> {
|
|
292
|
+
code: `${fhirpathImport}${optionsImport}${valueSetImportStatements}${validatorImportStatements}export async function validate${interfaceName}(resource: ${interfaceName}, options?: ValidatorOptions): Promise<{ errors: string[]; warnings: string[] }> {
|
|
266
293
|
const errors: string[] = [];
|
|
267
294
|
const warnings: string[] = [];
|
|
268
295
|
${fhirpathOptionsBlock}
|
|
@@ -104,10 +104,14 @@ export function generateExtensionStructuralValidation() {
|
|
|
104
104
|
return `
|
|
105
105
|
// Base FHIR structural validation: extension.url required, ext-1 constraint, empty objects
|
|
106
106
|
{
|
|
107
|
-
const _checkExtensions = (obj: unknown, path: string): void => {
|
|
107
|
+
const _checkExtensions = (obj: unknown, path: string, depth: number): void => {
|
|
108
108
|
if (!obj || typeof obj !== 'object') return;
|
|
109
|
-
if (Array.isArray(obj)) { obj.forEach((item, i) => _checkExtensions(item, path + '[' + i + ']')); return; }
|
|
109
|
+
if (Array.isArray(obj)) { obj.forEach((item, i) => _checkExtensions(item, path + '[' + i + ']', depth + 1)); return; }
|
|
110
110
|
const rec = obj as Record<string, unknown>;
|
|
111
|
+
// Empty object check β HL7 validates all FHIR elements must have content
|
|
112
|
+
if (depth > 0 && Object.keys(rec).length === 0) {
|
|
113
|
+
errors.push('Object must have some content');
|
|
114
|
+
}
|
|
111
115
|
// Validate extension arrays
|
|
112
116
|
for (const extKey of ['extension', 'modifierExtension'] as const) {
|
|
113
117
|
const exts = rec[extKey];
|
|
@@ -115,11 +119,6 @@ export function generateExtensionStructuralValidation() {
|
|
|
115
119
|
for (const ext of exts) {
|
|
116
120
|
if (ext && typeof ext === 'object' && !Array.isArray(ext)) {
|
|
117
121
|
const e = ext as Record<string, unknown>;
|
|
118
|
-
// Empty extension object check
|
|
119
|
-
if (Object.keys(e).length === 0) {
|
|
120
|
-
errors.push('Object must have some content');
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
122
|
if (typeof e.url !== 'string' || !e.url) {
|
|
124
123
|
errors.push('Extension.url is required in order to identify, use and validate the extension');
|
|
125
124
|
}
|
|
@@ -136,11 +135,11 @@ export function generateExtensionStructuralValidation() {
|
|
|
136
135
|
// Recurse into child objects (skip primitive values)
|
|
137
136
|
for (const [_key, _val] of Object.entries(rec)) {
|
|
138
137
|
if (_val && typeof _val === 'object') {
|
|
139
|
-
_checkExtensions(_val, path + '.' + _key);
|
|
138
|
+
_checkExtensions(_val, path + '.' + _key, depth + 1);
|
|
140
139
|
}
|
|
141
140
|
}
|
|
142
141
|
};
|
|
143
|
-
_checkExtensions(resource, '');
|
|
142
|
+
_checkExtensions(resource, '', 0);
|
|
144
143
|
}
|
|
145
144
|
`;
|
|
146
145
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_description": "Curated sample codes for large external CodeSystems (LOINC, SNOMED CT, etc.) that cannot be
|
|
2
|
+
"_description": "Curated sample codes for large external CodeSystems (LOINC, SNOMED CT, etc.) and complete code sets for small core FHIR R4 CodeSystems that cannot be loaded from packages skipped by isLargeHl7Package (hl7.fhir.r4.core, hl7.terminology, etc.). Used as fallback to allow ValueSet resolution for core R4 ValueSets without requiring a terminology server.",
|
|
3
3
|
"_valueSets": "Maps core R4 ValueSet canonical URLs to included CodeSystem URLs. Used when the dependency package containing the ValueSet is not cached locally.",
|
|
4
4
|
"systems": {
|
|
5
5
|
"http://loinc.org": [
|
|
@@ -91,6 +91,370 @@
|
|
|
91
91
|
{ "code": "failed", "display": "Failed" },
|
|
92
92
|
{ "code": "completed", "display": "Completed" },
|
|
93
93
|
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
94
|
+
],
|
|
95
|
+
"http://hl7.org/fhir/event-status": [
|
|
96
|
+
{ "code": "preparation", "display": "Preparation" },
|
|
97
|
+
{ "code": "in-progress", "display": "In Progress" },
|
|
98
|
+
{ "code": "not-done", "display": "Not Done" },
|
|
99
|
+
{ "code": "on-hold", "display": "On Hold" },
|
|
100
|
+
{ "code": "stopped", "display": "Stopped" },
|
|
101
|
+
{ "code": "completed", "display": "Completed" },
|
|
102
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
103
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
104
|
+
],
|
|
105
|
+
"http://hl7.org/fhir/observation-status": [
|
|
106
|
+
{ "code": "registered", "display": "Registered" },
|
|
107
|
+
{ "code": "preliminary", "display": "Preliminary" },
|
|
108
|
+
{ "code": "final", "display": "Final" },
|
|
109
|
+
{ "code": "amended", "display": "Amended" },
|
|
110
|
+
{ "code": "corrected", "display": "Corrected" },
|
|
111
|
+
{ "code": "cancelled", "display": "Cancelled" },
|
|
112
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
113
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
114
|
+
],
|
|
115
|
+
"http://hl7.org/fhir/administrative-gender": [
|
|
116
|
+
{ "code": "male", "display": "Male" },
|
|
117
|
+
{ "code": "female", "display": "Female" },
|
|
118
|
+
{ "code": "other", "display": "Other" },
|
|
119
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
120
|
+
],
|
|
121
|
+
"http://hl7.org/fhir/encounter-status": [
|
|
122
|
+
{ "code": "planned", "display": "Planned" },
|
|
123
|
+
{ "code": "arrived", "display": "Arrived" },
|
|
124
|
+
{ "code": "triaged", "display": "Triaged" },
|
|
125
|
+
{ "code": "in-progress", "display": "In Progress" },
|
|
126
|
+
{ "code": "onleave", "display": "On Leave" },
|
|
127
|
+
{ "code": "finished", "display": "Finished" },
|
|
128
|
+
{ "code": "cancelled", "display": "Cancelled" },
|
|
129
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
130
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
131
|
+
],
|
|
132
|
+
"http://hl7.org/fhir/request-status": [
|
|
133
|
+
{ "code": "draft", "display": "Draft" },
|
|
134
|
+
{ "code": "active", "display": "Active" },
|
|
135
|
+
{ "code": "on-hold", "display": "On Hold" },
|
|
136
|
+
{ "code": "revoked", "display": "Revoked" },
|
|
137
|
+
{ "code": "completed", "display": "Completed" },
|
|
138
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
139
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
140
|
+
],
|
|
141
|
+
"http://hl7.org/fhir/narrative-status": [
|
|
142
|
+
{ "code": "generated", "display": "Generated" },
|
|
143
|
+
{ "code": "extensions", "display": "Extensions" },
|
|
144
|
+
{ "code": "additional", "display": "Additional" },
|
|
145
|
+
{ "code": "empty", "display": "Empty" }
|
|
146
|
+
],
|
|
147
|
+
"http://hl7.org/fhir/composition-status": [
|
|
148
|
+
{ "code": "preliminary", "display": "Preliminary" },
|
|
149
|
+
{ "code": "final", "display": "Final" },
|
|
150
|
+
{ "code": "amended", "display": "Amended" },
|
|
151
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
152
|
+
],
|
|
153
|
+
"http://hl7.org/fhir/document-reference-status": [
|
|
154
|
+
{ "code": "current", "display": "Current" },
|
|
155
|
+
{ "code": "superseded", "display": "Superseded" },
|
|
156
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
157
|
+
],
|
|
158
|
+
"http://hl7.org/fhir/list-status": [
|
|
159
|
+
{ "code": "current", "display": "Current" },
|
|
160
|
+
{ "code": "retired", "display": "Retired" },
|
|
161
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
162
|
+
],
|
|
163
|
+
"http://hl7.org/fhir/fm-status": [
|
|
164
|
+
{ "code": "active", "display": "Active" },
|
|
165
|
+
{ "code": "cancelled", "display": "Cancelled" },
|
|
166
|
+
{ "code": "draft", "display": "Draft" },
|
|
167
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
168
|
+
],
|
|
169
|
+
"http://hl7.org/fhir/diagnostic-report-status": [
|
|
170
|
+
{ "code": "registered", "display": "Registered" },
|
|
171
|
+
{ "code": "partial", "display": "Partial" },
|
|
172
|
+
{ "code": "preliminary", "display": "Preliminary" },
|
|
173
|
+
{ "code": "final", "display": "Final" },
|
|
174
|
+
{ "code": "amended", "display": "Amended" },
|
|
175
|
+
{ "code": "corrected", "display": "Corrected" },
|
|
176
|
+
{ "code": "appended", "display": "Appended" },
|
|
177
|
+
{ "code": "cancelled", "display": "Cancelled" },
|
|
178
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
179
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
180
|
+
],
|
|
181
|
+
"http://hl7.org/fhir/bundle-type": [
|
|
182
|
+
{ "code": "document", "display": "Document" },
|
|
183
|
+
{ "code": "message", "display": "Message" },
|
|
184
|
+
{ "code": "transaction", "display": "Transaction" },
|
|
185
|
+
{ "code": "transaction-response", "display": "Transaction Response" },
|
|
186
|
+
{ "code": "batch", "display": "Batch" },
|
|
187
|
+
{ "code": "batch-response", "display": "Batch Response" },
|
|
188
|
+
{ "code": "history", "display": "History List" },
|
|
189
|
+
{ "code": "searchset", "display": "Search Results" },
|
|
190
|
+
{ "code": "collection", "display": "Collection" }
|
|
191
|
+
],
|
|
192
|
+
"http://hl7.org/fhir/allergy-intolerance-type": [
|
|
193
|
+
{ "code": "allergy", "display": "Allergy" },
|
|
194
|
+
{ "code": "intolerance", "display": "Intolerance" }
|
|
195
|
+
],
|
|
196
|
+
"http://hl7.org/fhir/group-type": [
|
|
197
|
+
{ "code": "person", "display": "Person" },
|
|
198
|
+
{ "code": "animal", "display": "Animal" },
|
|
199
|
+
{ "code": "practitioner", "display": "Practitioner" },
|
|
200
|
+
{ "code": "device", "display": "Device" },
|
|
201
|
+
{ "code": "medication", "display": "Medication" },
|
|
202
|
+
{ "code": "substance", "display": "Substance" }
|
|
203
|
+
],
|
|
204
|
+
"http://hl7.org/fhir/measure-report-status": [
|
|
205
|
+
{ "code": "complete", "display": "Complete" },
|
|
206
|
+
{ "code": "pending", "display": "Pending" },
|
|
207
|
+
{ "code": "error", "display": "Error" }
|
|
208
|
+
],
|
|
209
|
+
"http://hl7.org/fhir/measure-report-type": [
|
|
210
|
+
{ "code": "individual", "display": "Individual" },
|
|
211
|
+
{ "code": "subject-list", "display": "Subject List" },
|
|
212
|
+
{ "code": "summary", "display": "Summary" },
|
|
213
|
+
{ "code": "data-collection", "display": "Data Collection" }
|
|
214
|
+
],
|
|
215
|
+
"http://hl7.org/fhir/goal-status": [
|
|
216
|
+
{ "code": "proposed", "display": "Proposed" },
|
|
217
|
+
{ "code": "planned", "display": "Planned" },
|
|
218
|
+
{ "code": "accepted", "display": "Accepted" },
|
|
219
|
+
{ "code": "active", "display": "Active" },
|
|
220
|
+
{ "code": "on-hold", "display": "On Hold" },
|
|
221
|
+
{ "code": "completed", "display": "Completed" },
|
|
222
|
+
{ "code": "cancelled", "display": "Cancelled" },
|
|
223
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
224
|
+
{ "code": "rejected", "display": "Rejected" }
|
|
225
|
+
],
|
|
226
|
+
"http://hl7.org/fhir/episode-of-care-status": [
|
|
227
|
+
{ "code": "planned", "display": "Planned" },
|
|
228
|
+
{ "code": "waitlist", "display": "Waitlist" },
|
|
229
|
+
{ "code": "active", "display": "Active" },
|
|
230
|
+
{ "code": "onhold", "display": "On Hold" },
|
|
231
|
+
{ "code": "finished", "display": "Finished" },
|
|
232
|
+
{ "code": "cancelled", "display": "Cancelled" },
|
|
233
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
234
|
+
],
|
|
235
|
+
"http://hl7.org/fhir/specimen-status": [
|
|
236
|
+
{ "code": "available", "display": "Available" },
|
|
237
|
+
{ "code": "unavailable", "display": "Unavailable" },
|
|
238
|
+
{ "code": "unsatisfactory", "display": "Unsatisfactory" },
|
|
239
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
240
|
+
],
|
|
241
|
+
"http://hl7.org/fhir/flag-status": [
|
|
242
|
+
{ "code": "active", "display": "Active" },
|
|
243
|
+
{ "code": "inactive", "display": "Inactive" },
|
|
244
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
245
|
+
],
|
|
246
|
+
"http://hl7.org/fhir/address-use": [
|
|
247
|
+
{ "code": "home", "display": "Home" },
|
|
248
|
+
{ "code": "work", "display": "Work" },
|
|
249
|
+
{ "code": "temp", "display": "Temp" },
|
|
250
|
+
{ "code": "old", "display": "Old / Incorrect" },
|
|
251
|
+
{ "code": "billing", "display": "Billing" }
|
|
252
|
+
],
|
|
253
|
+
"http://hl7.org/fhir/address-type": [
|
|
254
|
+
{ "code": "postal", "display": "Postal" },
|
|
255
|
+
{ "code": "physical", "display": "Physical" },
|
|
256
|
+
{ "code": "both", "display": "Postal & Physical" }
|
|
257
|
+
],
|
|
258
|
+
"http://hl7.org/fhir/contact-point-system": [
|
|
259
|
+
{ "code": "phone", "display": "Phone" },
|
|
260
|
+
{ "code": "fax", "display": "Fax" },
|
|
261
|
+
{ "code": "email", "display": "Email" },
|
|
262
|
+
{ "code": "pager", "display": "Pager" },
|
|
263
|
+
{ "code": "url", "display": "URL" },
|
|
264
|
+
{ "code": "sms", "display": "SMS" },
|
|
265
|
+
{ "code": "other", "display": "Other" }
|
|
266
|
+
],
|
|
267
|
+
"http://hl7.org/fhir/contact-point-use": [
|
|
268
|
+
{ "code": "home", "display": "Home" },
|
|
269
|
+
{ "code": "work", "display": "Work" },
|
|
270
|
+
{ "code": "temp", "display": "Temp" },
|
|
271
|
+
{ "code": "old", "display": "Old" },
|
|
272
|
+
{ "code": "mobile", "display": "Mobile" }
|
|
273
|
+
],
|
|
274
|
+
"http://hl7.org/fhir/identifier-use": [
|
|
275
|
+
{ "code": "usual", "display": "Usual" },
|
|
276
|
+
{ "code": "official", "display": "Official" },
|
|
277
|
+
{ "code": "temp", "display": "Temp" },
|
|
278
|
+
{ "code": "secondary", "display": "Secondary" },
|
|
279
|
+
{ "code": "old", "display": "Old" }
|
|
280
|
+
],
|
|
281
|
+
"http://hl7.org/fhir/name-use": [
|
|
282
|
+
{ "code": "usual", "display": "Usual" },
|
|
283
|
+
{ "code": "official", "display": "Official" },
|
|
284
|
+
{ "code": "temp", "display": "Temp" },
|
|
285
|
+
{ "code": "nickname", "display": "Nickname" },
|
|
286
|
+
{ "code": "anonymous", "display": "Anonymous" },
|
|
287
|
+
{ "code": "old", "display": "Old" },
|
|
288
|
+
{ "code": "maiden", "display": "Name changed for Marriage" }
|
|
289
|
+
],
|
|
290
|
+
"http://hl7.org/fhir/link-type": [
|
|
291
|
+
{ "code": "replaced-by", "display": "Replaced-by" },
|
|
292
|
+
{ "code": "replaces", "display": "Replaces" },
|
|
293
|
+
{ "code": "refer", "display": "Refer" },
|
|
294
|
+
{ "code": "seealso", "display": "See also" }
|
|
295
|
+
],
|
|
296
|
+
"http://hl7.org/fhir/account-status": [
|
|
297
|
+
{ "code": "active", "display": "Active" },
|
|
298
|
+
{ "code": "inactive", "display": "Inactive" },
|
|
299
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
300
|
+
{ "code": "on-hold", "display": "On Hold" },
|
|
301
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
302
|
+
],
|
|
303
|
+
"http://hl7.org/fhir/consent-state-codes": [
|
|
304
|
+
{ "code": "draft", "display": "Pending" },
|
|
305
|
+
{ "code": "proposed", "display": "Proposed" },
|
|
306
|
+
{ "code": "active", "display": "Active" },
|
|
307
|
+
{ "code": "rejected", "display": "Rejected" },
|
|
308
|
+
{ "code": "inactive", "display": "Inactive" },
|
|
309
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
310
|
+
],
|
|
311
|
+
"http://hl7.org/fhir/substance-status": [
|
|
312
|
+
{ "code": "active", "display": "Active" },
|
|
313
|
+
{ "code": "inactive", "display": "Inactive" },
|
|
314
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
315
|
+
],
|
|
316
|
+
"http://hl7.org/fhir/questionnaire-answers-status": [
|
|
317
|
+
{ "code": "in-progress", "display": "In Progress" },
|
|
318
|
+
{ "code": "completed", "display": "Completed" },
|
|
319
|
+
{ "code": "amended", "display": "Amended" },
|
|
320
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
321
|
+
{ "code": "stopped", "display": "Stopped" }
|
|
322
|
+
],
|
|
323
|
+
"http://hl7.org/fhir/item-type": [
|
|
324
|
+
{ "code": "group", "display": "Group" },
|
|
325
|
+
{ "code": "display", "display": "Display" },
|
|
326
|
+
{ "code": "question", "display": "Question" },
|
|
327
|
+
{ "code": "boolean", "display": "Boolean" },
|
|
328
|
+
{ "code": "decimal", "display": "Decimal" },
|
|
329
|
+
{ "code": "integer", "display": "Integer" },
|
|
330
|
+
{ "code": "date", "display": "Date" },
|
|
331
|
+
{ "code": "dateTime", "display": "Date Time" },
|
|
332
|
+
{ "code": "time", "display": "Time" },
|
|
333
|
+
{ "code": "string", "display": "String" },
|
|
334
|
+
{ "code": "text", "display": "Text" },
|
|
335
|
+
{ "code": "url", "display": "Url" },
|
|
336
|
+
{ "code": "choice", "display": "Choice" },
|
|
337
|
+
{ "code": "open-choice", "display": "Open Choice" },
|
|
338
|
+
{ "code": "attachment", "display": "Attachment" },
|
|
339
|
+
{ "code": "reference", "display": "Reference" },
|
|
340
|
+
{ "code": "quantity", "display": "Quantity" }
|
|
341
|
+
],
|
|
342
|
+
"http://hl7.org/fhir/http-verb": [
|
|
343
|
+
{ "code": "GET", "display": "GET" },
|
|
344
|
+
{ "code": "HEAD", "display": "HEAD" },
|
|
345
|
+
{ "code": "POST", "display": "POST" },
|
|
346
|
+
{ "code": "PUT", "display": "PUT" },
|
|
347
|
+
{ "code": "DELETE", "display": "DELETE" },
|
|
348
|
+
{ "code": "PATCH", "display": "PATCH" }
|
|
349
|
+
],
|
|
350
|
+
"http://hl7.org/fhir/capability-statement-kind": [
|
|
351
|
+
{ "code": "instance", "display": "Instance" },
|
|
352
|
+
{ "code": "capability", "display": "Capability" },
|
|
353
|
+
{ "code": "requirements", "display": "Requirements" }
|
|
354
|
+
],
|
|
355
|
+
"http://hl7.org/fhir/restful-capability-mode": [
|
|
356
|
+
{ "code": "client", "display": "Client" },
|
|
357
|
+
{ "code": "server", "display": "Server" }
|
|
358
|
+
],
|
|
359
|
+
"http://hl7.org/fhir/search-param-type": [
|
|
360
|
+
{ "code": "number", "display": "Number" },
|
|
361
|
+
{ "code": "date", "display": "Date/DateTime" },
|
|
362
|
+
{ "code": "string", "display": "String" },
|
|
363
|
+
{ "code": "token", "display": "Token" },
|
|
364
|
+
{ "code": "reference", "display": "Reference" },
|
|
365
|
+
{ "code": "composite", "display": "Composite" },
|
|
366
|
+
{ "code": "quantity", "display": "Quantity" },
|
|
367
|
+
{ "code": "uri", "display": "URI" },
|
|
368
|
+
{ "code": "special", "display": "Special" }
|
|
369
|
+
],
|
|
370
|
+
"http://hl7.org/fhir/filter-operator": [
|
|
371
|
+
{ "code": "=", "display": "Equals" },
|
|
372
|
+
{ "code": "is-a", "display": "Is A (by subsumption)" },
|
|
373
|
+
{ "code": "descendent-of", "display": "Descendent Of (by subsumption)" },
|
|
374
|
+
{ "code": "is-not-a", "display": "Not (Is A) (by subsumption)" },
|
|
375
|
+
{ "code": "regex", "display": "Regular Expression" },
|
|
376
|
+
{ "code": "in", "display": "Not in Set" },
|
|
377
|
+
{ "code": "not-in", "display": "Not in Set" },
|
|
378
|
+
{ "code": "generalizes", "display": "Generalizes (by Subsumption)" },
|
|
379
|
+
{ "code": "exists", "display": "Exists" }
|
|
380
|
+
],
|
|
381
|
+
"http://hl7.org/fhir/CodeSystem/medicationrequest-status": [
|
|
382
|
+
{ "code": "active", "display": "Active" },
|
|
383
|
+
{ "code": "on-hold", "display": "On Hold" },
|
|
384
|
+
{ "code": "cancelled", "display": "Cancelled" },
|
|
385
|
+
{ "code": "completed", "display": "Completed" },
|
|
386
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
387
|
+
{ "code": "stopped", "display": "Stopped" },
|
|
388
|
+
{ "code": "draft", "display": "Draft" },
|
|
389
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
390
|
+
],
|
|
391
|
+
"http://hl7.org/fhir/CodeSystem/medication-status": [
|
|
392
|
+
{ "code": "active", "display": "Active" },
|
|
393
|
+
{ "code": "inactive", "display": "Inactive" },
|
|
394
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
395
|
+
],
|
|
396
|
+
"http://hl7.org/fhir/CodeSystem/medication-admin-status": [
|
|
397
|
+
{ "code": "in-progress", "display": "In Progress" },
|
|
398
|
+
{ "code": "not-done", "display": "Not Done" },
|
|
399
|
+
{ "code": "on-hold", "display": "On Hold" },
|
|
400
|
+
{ "code": "completed", "display": "Completed" },
|
|
401
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
402
|
+
{ "code": "stopped", "display": "Stopped" },
|
|
403
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
404
|
+
],
|
|
405
|
+
"http://hl7.org/fhir/CodeSystem/medicationdispense-status": [
|
|
406
|
+
{ "code": "preparation", "display": "Preparation" },
|
|
407
|
+
{ "code": "in-progress", "display": "In Progress" },
|
|
408
|
+
{ "code": "cancelled", "display": "Cancelled" },
|
|
409
|
+
{ "code": "on-hold", "display": "On Hold" },
|
|
410
|
+
{ "code": "completed", "display": "Completed" },
|
|
411
|
+
{ "code": "entered-in-error", "display": "Entered in Error" },
|
|
412
|
+
{ "code": "stopped", "display": "Stopped" },
|
|
413
|
+
{ "code": "declined", "display": "Declined" },
|
|
414
|
+
{ "code": "unknown", "display": "Unknown" }
|
|
415
|
+
],
|
|
416
|
+
"http://hl7.org/fhir/request-intent": [
|
|
417
|
+
{ "code": "proposal", "display": "Proposal" },
|
|
418
|
+
{ "code": "plan", "display": "Plan" },
|
|
419
|
+
{ "code": "directive", "display": "Directive" },
|
|
420
|
+
{ "code": "order", "display": "Order" },
|
|
421
|
+
{ "code": "original-order", "display": "Original Order" },
|
|
422
|
+
{ "code": "reflex-order", "display": "Reflex Order" },
|
|
423
|
+
{ "code": "filler-order", "display": "Filler Order" },
|
|
424
|
+
{ "code": "instance-order", "display": "Instance Order" },
|
|
425
|
+
{ "code": "option", "display": "Option" }
|
|
426
|
+
],
|
|
427
|
+
"http://hl7.org/fhir/request-priority": [
|
|
428
|
+
{ "code": "routine", "display": "Routine" },
|
|
429
|
+
{ "code": "urgent", "display": "Urgent" },
|
|
430
|
+
{ "code": "asap", "display": "ASAP" },
|
|
431
|
+
{ "code": "stat", "display": "STAT" }
|
|
432
|
+
],
|
|
433
|
+
"http://hl7.org/fhir/care-plan-activity-status": [
|
|
434
|
+
{ "code": "not-started", "display": "Not Started" },
|
|
435
|
+
{ "code": "scheduled", "display": "Scheduled" },
|
|
436
|
+
{ "code": "in-progress", "display": "In Progress" },
|
|
437
|
+
{ "code": "on-hold", "display": "On Hold" },
|
|
438
|
+
{ "code": "completed", "display": "Completed" },
|
|
439
|
+
{ "code": "cancelled", "display": "Cancelled" },
|
|
440
|
+
{ "code": "stopped", "display": "Stopped" },
|
|
441
|
+
{ "code": "unknown", "display": "Unknown" },
|
|
442
|
+
{ "code": "entered-in-error", "display": "Entered in Error" }
|
|
443
|
+
],
|
|
444
|
+
"http://hl7.org/fhir/quantity-comparator": [
|
|
445
|
+
{ "code": "<", "display": "Less than" },
|
|
446
|
+
{ "code": "<=", "display": "Less or Equal to" },
|
|
447
|
+
{ "code": ">=", "display": "Greater or Equal to" },
|
|
448
|
+
{ "code": ">", "display": "Greater than" }
|
|
449
|
+
],
|
|
450
|
+
"http://hl7.org/fhir/days-of-week": [
|
|
451
|
+
{ "code": "mon", "display": "Monday" },
|
|
452
|
+
{ "code": "tue", "display": "Tuesday" },
|
|
453
|
+
{ "code": "wed", "display": "Wednesday" },
|
|
454
|
+
{ "code": "thu", "display": "Thursday" },
|
|
455
|
+
{ "code": "fri", "display": "Friday" },
|
|
456
|
+
{ "code": "sat", "display": "Saturday" },
|
|
457
|
+
{ "code": "sun", "display": "Sunday" }
|
|
94
458
|
]
|
|
95
459
|
},
|
|
96
460
|
"valueSets": {
|
|
@@ -103,6 +467,53 @@
|
|
|
103
467
|
"http://hl7.org/fhir/ValueSet/namingsystem-type": ["http://hl7.org/fhir/namingsystem-type"],
|
|
104
468
|
"http://hl7.org/fhir/ValueSet/namingsystem-identifier-type": ["http://hl7.org/fhir/namingsystem-identifier-type"],
|
|
105
469
|
"http://hl7.org/fhir/ValueSet/publication-status": ["http://hl7.org/fhir/publication-status"],
|
|
106
|
-
"http://hl7.org/fhir/ValueSet/task-status": ["http://hl7.org/fhir/task-status"]
|
|
470
|
+
"http://hl7.org/fhir/ValueSet/task-status": ["http://hl7.org/fhir/task-status"],
|
|
471
|
+
"http://hl7.org/fhir/ValueSet/event-status": ["http://hl7.org/fhir/event-status"],
|
|
472
|
+
"http://hl7.org/fhir/ValueSet/immunization-status": ["http://hl7.org/fhir/event-status"],
|
|
473
|
+
"http://hl7.org/fhir/ValueSet/observation-status": ["http://hl7.org/fhir/observation-status"],
|
|
474
|
+
"http://hl7.org/fhir/ValueSet/administrative-gender": ["http://hl7.org/fhir/administrative-gender"],
|
|
475
|
+
"http://hl7.org/fhir/ValueSet/encounter-status": ["http://hl7.org/fhir/encounter-status"],
|
|
476
|
+
"http://hl7.org/fhir/ValueSet/request-status": ["http://hl7.org/fhir/request-status"],
|
|
477
|
+
"http://hl7.org/fhir/ValueSet/narrative-status": ["http://hl7.org/fhir/narrative-status"],
|
|
478
|
+
"http://hl7.org/fhir/ValueSet/composition-status": ["http://hl7.org/fhir/composition-status"],
|
|
479
|
+
"http://hl7.org/fhir/ValueSet/document-reference-status": ["http://hl7.org/fhir/document-reference-status"],
|
|
480
|
+
"http://hl7.org/fhir/ValueSet/list-status": ["http://hl7.org/fhir/list-status"],
|
|
481
|
+
"http://hl7.org/fhir/ValueSet/fm-status": ["http://hl7.org/fhir/fm-status"],
|
|
482
|
+
"http://hl7.org/fhir/ValueSet/diagnostic-report-status": ["http://hl7.org/fhir/diagnostic-report-status"],
|
|
483
|
+
"http://hl7.org/fhir/ValueSet/bundle-type": ["http://hl7.org/fhir/bundle-type"],
|
|
484
|
+
"http://hl7.org/fhir/ValueSet/allergy-intolerance-type": ["http://hl7.org/fhir/allergy-intolerance-type"],
|
|
485
|
+
"http://hl7.org/fhir/ValueSet/group-type": ["http://hl7.org/fhir/group-type"],
|
|
486
|
+
"http://hl7.org/fhir/ValueSet/measure-report-status": ["http://hl7.org/fhir/measure-report-status"],
|
|
487
|
+
"http://hl7.org/fhir/ValueSet/measure-report-type": ["http://hl7.org/fhir/measure-report-type"],
|
|
488
|
+
"http://hl7.org/fhir/ValueSet/goal-status": ["http://hl7.org/fhir/goal-status"],
|
|
489
|
+
"http://hl7.org/fhir/ValueSet/episode-of-care-status": ["http://hl7.org/fhir/episode-of-care-status"],
|
|
490
|
+
"http://hl7.org/fhir/ValueSet/specimen-status": ["http://hl7.org/fhir/specimen-status"],
|
|
491
|
+
"http://hl7.org/fhir/ValueSet/flag-status": ["http://hl7.org/fhir/flag-status"],
|
|
492
|
+
"http://hl7.org/fhir/ValueSet/address-use": ["http://hl7.org/fhir/address-use"],
|
|
493
|
+
"http://hl7.org/fhir/ValueSet/address-type": ["http://hl7.org/fhir/address-type"],
|
|
494
|
+
"http://hl7.org/fhir/ValueSet/contact-point-system": ["http://hl7.org/fhir/contact-point-system"],
|
|
495
|
+
"http://hl7.org/fhir/ValueSet/contact-point-use": ["http://hl7.org/fhir/contact-point-use"],
|
|
496
|
+
"http://hl7.org/fhir/ValueSet/identifier-use": ["http://hl7.org/fhir/identifier-use"],
|
|
497
|
+
"http://hl7.org/fhir/ValueSet/name-use": ["http://hl7.org/fhir/name-use"],
|
|
498
|
+
"http://hl7.org/fhir/ValueSet/link-type": ["http://hl7.org/fhir/link-type"],
|
|
499
|
+
"http://hl7.org/fhir/ValueSet/account-status": ["http://hl7.org/fhir/account-status"],
|
|
500
|
+
"http://hl7.org/fhir/ValueSet/consent-state-codes": ["http://hl7.org/fhir/consent-state-codes"],
|
|
501
|
+
"http://hl7.org/fhir/ValueSet/substance-status": ["http://hl7.org/fhir/substance-status"],
|
|
502
|
+
"http://hl7.org/fhir/ValueSet/questionnaire-answers-status": ["http://hl7.org/fhir/questionnaire-answers-status"],
|
|
503
|
+
"http://hl7.org/fhir/ValueSet/item-type": ["http://hl7.org/fhir/item-type"],
|
|
504
|
+
"http://hl7.org/fhir/ValueSet/http-verb": ["http://hl7.org/fhir/http-verb"],
|
|
505
|
+
"http://hl7.org/fhir/ValueSet/capability-statement-kind": ["http://hl7.org/fhir/capability-statement-kind"],
|
|
506
|
+
"http://hl7.org/fhir/ValueSet/restful-capability-mode": ["http://hl7.org/fhir/restful-capability-mode"],
|
|
507
|
+
"http://hl7.org/fhir/ValueSet/search-param-type": ["http://hl7.org/fhir/search-param-type"],
|
|
508
|
+
"http://hl7.org/fhir/ValueSet/filter-operator": ["http://hl7.org/fhir/filter-operator"],
|
|
509
|
+
"http://hl7.org/fhir/ValueSet/medicationrequest-status": ["http://hl7.org/fhir/CodeSystem/medicationrequest-status"],
|
|
510
|
+
"http://hl7.org/fhir/ValueSet/medication-status": ["http://hl7.org/fhir/CodeSystem/medication-status"],
|
|
511
|
+
"http://hl7.org/fhir/ValueSet/medication-admin-status": ["http://hl7.org/fhir/CodeSystem/medication-admin-status"],
|
|
512
|
+
"http://hl7.org/fhir/ValueSet/medicationdispense-status": ["http://hl7.org/fhir/CodeSystem/medicationdispense-status"],
|
|
513
|
+
"http://hl7.org/fhir/ValueSet/request-intent": ["http://hl7.org/fhir/request-intent"],
|
|
514
|
+
"http://hl7.org/fhir/ValueSet/request-priority": ["http://hl7.org/fhir/request-priority"],
|
|
515
|
+
"http://hl7.org/fhir/ValueSet/care-plan-activity-status": ["http://hl7.org/fhir/care-plan-activity-status"],
|
|
516
|
+
"http://hl7.org/fhir/ValueSet/quantity-comparator": ["http://hl7.org/fhir/quantity-comparator"],
|
|
517
|
+
"http://hl7.org/fhir/ValueSet/days-of-week": ["http://hl7.org/fhir/days-of-week"]
|
|
107
518
|
}
|
|
108
519
|
}
|
|
@@ -356,7 +356,7 @@ export async function createPackageFromDir(sourceDir, outFile) {
|
|
|
356
356
|
* a map of CodeSystem URL β concept[]. This allows ValueSet resolution for
|
|
357
357
|
* compose.include entries that reference a whole system without listing concepts.
|
|
358
358
|
*/
|
|
359
|
-
export function readCodeSystemConceptsFromDir(extractedRoot) {
|
|
359
|
+
export function readCodeSystemConceptsFromDir(extractedRoot, maxConceptCount) {
|
|
360
360
|
const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'CodeSystem-');
|
|
361
361
|
const map = {};
|
|
362
362
|
for (const file of jsonFiles) {
|
|
@@ -371,6 +371,9 @@ export function readCodeSystemConceptsFromDir(extractedRoot) {
|
|
|
371
371
|
if (content.content !== 'complete')
|
|
372
372
|
continue;
|
|
373
373
|
const concepts = flattenCodeSystemConcepts(content.concept);
|
|
374
|
+
// When a cap is set (large-package selective loading), skip huge CodeSystems
|
|
375
|
+
if (maxConceptCount !== undefined && concepts.length > maxConceptCount)
|
|
376
|
+
continue;
|
|
374
377
|
if (concepts.length > 0) {
|
|
375
378
|
map[url] = concepts;
|
|
376
379
|
}
|
|
@@ -570,6 +573,13 @@ export async function ensureDependenciesDownloaded(extractedRoot) {
|
|
|
570
573
|
* Core SDs are derived via `deriveVersionData()` / bundled base definitions.
|
|
571
574
|
* Extensions and terminology resources are fetched on-demand as needed.
|
|
572
575
|
*/
|
|
576
|
+
/**
|
|
577
|
+
* Maximum number of flattened concepts a CodeSystem may have to be loaded from
|
|
578
|
+
* a large HL7 package during selective loading. 973 of the 1 054 complete
|
|
579
|
+
* CodeSystems in hl7.fhir.r4.core have β€ 20 concepts; a cap of 500 captures
|
|
580
|
+
* every practically-used VS while keeping memory bounded.
|
|
581
|
+
*/
|
|
582
|
+
const LARGE_PKG_MAX_CONCEPTS = 500;
|
|
573
583
|
export function isLargeHl7Package(depName) {
|
|
574
584
|
// hl7.terminology.r5, hl7.terminology.r4, etc. β large CodeSystem/ValueSet packages
|
|
575
585
|
if (depName.startsWith('hl7.terminology'))
|
|
@@ -667,11 +677,20 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
|
|
|
667
677
|
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
668
678
|
const deps = pkgJson.dependencies || {};
|
|
669
679
|
for (const [depName, depVersion] of Object.entries(deps)) {
|
|
670
|
-
if (isLargeHl7Package(depName))
|
|
671
|
-
continue;
|
|
672
680
|
const depDir = path.join(cacheDir, `${depName}@${depVersion}`);
|
|
673
|
-
if (fs.existsSync(depDir))
|
|
681
|
+
if (!fs.existsSync(depDir))
|
|
682
|
+
continue;
|
|
683
|
+
if (isLargeHl7Package(depName)) {
|
|
684
|
+
// Selectively load small CodeSystems from large packages
|
|
685
|
+
const smallCs = readCodeSystemConceptsFromDir(depDir, LARGE_PKG_MAX_CONCEPTS);
|
|
686
|
+
for (const [url, concepts] of Object.entries(smallCs)) {
|
|
687
|
+
if (!mergedCodeSystems[url])
|
|
688
|
+
mergedCodeSystems[url] = concepts;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
674
692
|
collectCodeSystems(depDir);
|
|
693
|
+
}
|
|
675
694
|
}
|
|
676
695
|
}
|
|
677
696
|
catch { /* skip */ }
|
|
@@ -715,11 +734,19 @@ export function readValueSetCodesWithDependencies(extractedRoot) {
|
|
|
715
734
|
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
716
735
|
const deps = pkgJson.dependencies || {};
|
|
717
736
|
for (const [depName, depVersion] of Object.entries(deps)) {
|
|
718
|
-
if (isLargeHl7Package(depName))
|
|
719
|
-
continue;
|
|
720
737
|
// Look for dependency in cache: name@version format
|
|
721
738
|
const depDir = path.join(cacheDir, `${depName}@${depVersion}`);
|
|
722
|
-
if (fs.existsSync(depDir))
|
|
739
|
+
if (!fs.existsSync(depDir))
|
|
740
|
+
continue;
|
|
741
|
+
if (isLargeHl7Package(depName)) {
|
|
742
|
+
// Selectively load ValueSets from large packages (CodeSystems already loaded in pass 1)
|
|
743
|
+
const vsCodes = readValueSetCodesFromDir(depDir, mergedCodeSystems);
|
|
744
|
+
for (const [uri, codeBucket] of Object.entries(vsCodes)) {
|
|
745
|
+
if (!merged[uri])
|
|
746
|
+
merged[uri] = codeBucket;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
723
750
|
log.info(`Loading ValueSets from dependency: ${depName}@${depVersion}`);
|
|
724
751
|
loadPackageAndDeps(depDir);
|
|
725
752
|
}
|
|
@@ -463,7 +463,7 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
463
463
|
// Post-processing: Aggregate required slices onto parent array fields
|
|
464
464
|
// This populates requiredSlices on array fields that have slices with min >= 1
|
|
465
465
|
// Pass both newFields and baseFields so we can find parent fields from base profiles
|
|
466
|
-
await aggregateRequiredSlices(newFields, baseFields);
|
|
466
|
+
await aggregateRequiredSlices(newFields, baseFields, valueSetCodesMap);
|
|
467
467
|
return { newFields, oldFields: baseFields };
|
|
468
468
|
}
|
|
469
469
|
// NOTE: buildFhirChildTypeMap was removed - now using pre-generated fhirDataTypes.json instead
|
|
@@ -103,8 +103,9 @@ export function synthesizeSlicePatternsFromChildren(fields) {
|
|
|
103
103
|
*
|
|
104
104
|
* @param fields - The new fields from the differential (slices are defined here)
|
|
105
105
|
* @param baseFields - Optional fields from base profiles (parent array fields may be here)
|
|
106
|
+
* @param valueSetCodesMap - Resolved ValueSetβcodes map for IG-specific bindings
|
|
106
107
|
*/
|
|
107
|
-
export async function aggregateRequiredSlices(fields, baseFields = []) {
|
|
108
|
+
export async function aggregateRequiredSlices(fields, baseFields = [], valueSetCodesMap) {
|
|
108
109
|
const allFields = [...fields, ...baseFields];
|
|
109
110
|
const slicesByParentPath = new Map();
|
|
110
111
|
for (const field of fields) {
|
|
@@ -264,7 +265,14 @@ export async function aggregateRequiredSlices(fields, baseFields = []) {
|
|
|
264
265
|
elemAny.patternString;
|
|
265
266
|
const isChoiceElement = /^value\[x\]$/i.test(childPath);
|
|
266
267
|
const bindingUri = elem.binding?.valueSet;
|
|
267
|
-
|
|
268
|
+
let resolvedDefault = bindingUri ? resolveValueSetDefault(bindingUri) : undefined;
|
|
269
|
+
if (!resolvedDefault && bindingUri && valueSetCodesMap) {
|
|
270
|
+
const bare = bindingUri.split('|')[0];
|
|
271
|
+
const codes = valueSetCodesMap[bare] ?? valueSetCodesMap[bindingUri];
|
|
272
|
+
if (codes?.length) {
|
|
273
|
+
resolvedDefault = { code: codes[0].code, system: codes[0].system };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
268
276
|
const isSimpleExtension = isChoiceElement && elem.type?.length &&
|
|
269
277
|
typeProfile?.type === 'Extension' &&
|
|
270
278
|
!profileElements.some(pe => pe.path === 'Extension.extension' && pe.sliceName);
|
|
@@ -19,6 +19,7 @@ export function parseValueSet(valueSet) {
|
|
|
19
19
|
const concepts = [];
|
|
20
20
|
// Collect system URIs from compose.include for system-level validation
|
|
21
21
|
const systems = [];
|
|
22
|
+
let hasFilter = false;
|
|
22
23
|
// Parse compose.include sections
|
|
23
24
|
if (vs.compose?.include) {
|
|
24
25
|
for (const include of vs.compose.include) {
|
|
@@ -36,18 +37,11 @@ export function parseValueSet(valueSet) {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
// Filter-based inclusion (e.g., all codes from a system)
|
|
40
|
+
// We can't enumerate filter codes at build time, but we keep any
|
|
41
|
+
// explicit concepts from other includes and still process expansion below.
|
|
39
42
|
if (include.filter) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
log.debug(`ValueSet ${name} uses filters, skipping union type generation`);
|
|
43
|
-
return {
|
|
44
|
-
url: url || '',
|
|
45
|
-
name: name || 'Unknown',
|
|
46
|
-
concepts: [],
|
|
47
|
-
size: Infinity,
|
|
48
|
-
isSmall: false,
|
|
49
|
-
systems: systems.length > 0 ? systems : undefined,
|
|
50
|
-
};
|
|
43
|
+
hasFilter = true;
|
|
44
|
+
log.debug(`ValueSet ${name} uses filters, union type generation skipped`);
|
|
51
45
|
}
|
|
52
46
|
}
|
|
53
47
|
}
|
|
@@ -61,8 +55,11 @@ export function parseValueSet(valueSet) {
|
|
|
61
55
|
});
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
// When filters are present, the concept list may be incomplete.
|
|
59
|
+
// Use Infinity size to prevent union type generation, but still expose
|
|
60
|
+
// whatever concepts we did collect (explicit + expansion) for binding validation.
|
|
61
|
+
const size = hasFilter ? Infinity : concepts.length;
|
|
62
|
+
const isSmall = !hasFilter && concepts.length > 0 && concepts.length <= ctx().valueSetThresholds.UNION_TYPE;
|
|
66
63
|
return {
|
|
67
64
|
url: url || '',
|
|
68
65
|
name: name || 'Unknown',
|
|
@@ -153,7 +153,7 @@ export async function processStructureDefinition(sd, ctx) {
|
|
|
153
153
|
finalInterfaceContent = dedupedLines.join('\n');
|
|
154
154
|
// ββ Required fields ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
155
155
|
const merged = mergeFields(aligned.alignedNewFields, parsed.oldFields);
|
|
156
|
-
const validateResult = generateValidateProfileFunction(interfaceName, merged, valueSets, sd.url, sd.type);
|
|
156
|
+
const validateResult = generateValidateProfileFunction(interfaceName, merged, valueSets, sd.url, sd.type, profileUrlToName);
|
|
157
157
|
const validateFn = validateResult.code;
|
|
158
158
|
const candidateRequiredFields = [...aligned.alignedNewFields, ...parsed.oldFields];
|
|
159
159
|
const profileDiffFieldNames = new Set(aligned.alignedNewFields.map(f => f.name));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B/R5 StructureDefinitions (profiles) directly inside package archives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "out/src/main.js",
|