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
|
@@ -114,21 +114,28 @@ async function promptInstallRedirect(packageFilter, confirm) {
|
|
|
114
114
|
return { redirectInstall: packageFilter };
|
|
115
115
|
return {};
|
|
116
116
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Default Y/N prompt reading from stdin.
|
|
119
|
+
*
|
|
120
|
+
* The readline import is awaited rather than `.then`-ed inside a `new Promise`:
|
|
121
|
+
* that form dropped a rejection on the floor, and since nothing ever called
|
|
122
|
+
* `resolve`, a failed import left the caller awaiting a promise that could not
|
|
123
|
+
* settle. Declining is the safe answer when the prompt cannot be shown.
|
|
124
|
+
*/
|
|
125
|
+
async function defaultConfirm(question) {
|
|
126
|
+
process.stdout.write(question);
|
|
127
|
+
const { createInterface } = await import('readline');
|
|
128
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
129
|
+
try {
|
|
130
|
+
return await new Promise((resolve) => {
|
|
131
|
+
rl.question('', (answer) => {
|
|
128
132
|
resolve(!answer || answer.toLowerCase().startsWith('y'));
|
|
129
133
|
});
|
|
130
134
|
});
|
|
131
|
-
}
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
rl.close();
|
|
138
|
+
}
|
|
132
139
|
}
|
|
133
140
|
// ── Discovery ───────────────────────────────────────────────────────────────
|
|
134
141
|
/** Find all lib/ directories starting from startDir. Non-recursive returns only startDir/lib. */
|
|
@@ -57,6 +57,25 @@ export const FETCH_TIMEOUT_MS = 15_000;
|
|
|
57
57
|
export const TX_TIMEOUT_MS = 30_000;
|
|
58
58
|
/** Max filename length for terminology expansion cache files */
|
|
59
59
|
export const MAX_CACHE_FILENAME_LENGTH = 200;
|
|
60
|
+
/**
|
|
61
|
+
* FHIR package registries in priority order.
|
|
62
|
+
*
|
|
63
|
+
* packages.fhir.org is the canonical source; packages.simplifier.net is the
|
|
64
|
+
* fallback and carries some national IGs the canonical registry does not mirror.
|
|
65
|
+
* Both speak the npm metadata shape, so `GET <registry>/<packageId>` returns
|
|
66
|
+
* `dist-tags` and `versions`.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Attempts per registry when downloading a package tarball.
|
|
70
|
+
*
|
|
71
|
+
* Registry blips are the single largest cause of spurious parity failures: a run
|
|
72
|
+
* pulls ~30 packages and a lone `fetch failed` aborted the whole generation.
|
|
73
|
+
*/
|
|
74
|
+
export const PACKAGE_DOWNLOAD_ATTEMPTS = 3;
|
|
75
|
+
export const FHIR_REGISTRIES = [
|
|
76
|
+
'https://packages.fhir.org',
|
|
77
|
+
'https://packages.simplifier.net',
|
|
78
|
+
];
|
|
60
79
|
// ─── FHIR StructureDefinition URL prefixes ───────────────────────────────────
|
|
61
80
|
export const FHIR_SD_URL_PREFIX = 'http://hl7.org/fhir/StructureDefinition/';
|
|
62
81
|
export const FHIR_SD_URL_PREFIX_HTTPS = 'https://hl7.org/fhir/StructureDefinition/';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading what an Extension StructureDefinition declares about its own contents.
|
|
3
|
+
*
|
|
4
|
+
* A complex extension carries its requirements in its own SD. A profile that
|
|
5
|
+
* references one re-declares the sub-extension paths as mustSupport markers but
|
|
6
|
+
* copies none of the constraints, so anything read only from the profile's field
|
|
7
|
+
* model misses them entirely — KBV_PR_ERP_PracticeSupply's
|
|
8
|
+
* `extension:Kostentraeger.extension:Kostentraegertyp.value[x]:valueCoding` has no
|
|
9
|
+
* binding, while KBV_EX_ERP_PracticeSupply_Payor binds it required to
|
|
10
|
+
* KBV_VS_FOR_Payor_type.
|
|
11
|
+
*/
|
|
12
|
+
/** `Extension.extension:<name>` with no further path below it. */
|
|
13
|
+
const SUB_SLICE_ID = /^Extension\.extension:([^.:]+)$/;
|
|
14
|
+
/**
|
|
15
|
+
* The elements to read constraints from.
|
|
16
|
+
*
|
|
17
|
+
* Prefer the snapshot, but fall back to the differential: several published
|
|
18
|
+
* extension SDs ship without one (KBV_EX_ERP_PracticeSupply_Payor among them), and
|
|
19
|
+
* requiring a snapshot meant they resolved to no metadata at all.
|
|
20
|
+
*/
|
|
21
|
+
export function extensionElements(sd) {
|
|
22
|
+
const els = sd?.snapshot?.element ?? sd?.differential?.element;
|
|
23
|
+
return els?.length ? els : undefined;
|
|
24
|
+
}
|
|
25
|
+
/** Read a binding off an element, normalising the ValueSet reference to a bare uri. */
|
|
26
|
+
export function readBinding(el) {
|
|
27
|
+
const binding = el?.binding;
|
|
28
|
+
if (!binding?.strength || !binding.valueSet)
|
|
29
|
+
return undefined;
|
|
30
|
+
const raw = typeof binding.valueSet === 'string'
|
|
31
|
+
? binding.valueSet
|
|
32
|
+
: binding.valueSet?.reference;
|
|
33
|
+
if (!raw)
|
|
34
|
+
return undefined;
|
|
35
|
+
return { strength: binding.strength, uri: raw.split('|')[0] };
|
|
36
|
+
}
|
|
37
|
+
/** Type codes declared on an element. */
|
|
38
|
+
function typeCodes(el) {
|
|
39
|
+
return (el?.type ?? []).map(t => t.code).filter((c) => !!c);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `min` on the unsliced `Extension.extension` element.
|
|
43
|
+
*
|
|
44
|
+
* This is what HL7 reports as "Extension.extension: minimum required = N", and it is
|
|
45
|
+
* not the sum of the slice minimums — the SD states it directly.
|
|
46
|
+
*/
|
|
47
|
+
export function readSubExtensionMin(els) {
|
|
48
|
+
const el = els.find(e => (e.id ?? e.path) === 'Extension.extension' && !e.sliceName);
|
|
49
|
+
return typeof el?.min === 'number' ? el.min : undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Collect the sub-extension slices a complex extension declares.
|
|
53
|
+
*
|
|
54
|
+
* A slice with no resolvable `url` is dropped rather than reported under its slice
|
|
55
|
+
* name: the url is how a member is recognised at runtime, and without it any check
|
|
56
|
+
* would be guessing which member to apply itself to.
|
|
57
|
+
*/
|
|
58
|
+
export function collectSubExtensionSlices(els) {
|
|
59
|
+
const byId = new Map();
|
|
60
|
+
for (const el of els) {
|
|
61
|
+
const id = el.id ?? el.path;
|
|
62
|
+
if (id)
|
|
63
|
+
byId.set(id, el);
|
|
64
|
+
}
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const el of els) {
|
|
67
|
+
const id = el.id ?? el.path;
|
|
68
|
+
const match = id ? SUB_SLICE_ID.exec(id) : null;
|
|
69
|
+
if (!match)
|
|
70
|
+
continue;
|
|
71
|
+
const sliceName = el.sliceName ?? match[1];
|
|
72
|
+
const urlEl = byId.get(`Extension.extension:${sliceName}.url`);
|
|
73
|
+
const url = typeof urlEl?.fixedUri === 'string' ? urlEl.fixedUri : undefined;
|
|
74
|
+
if (!url)
|
|
75
|
+
continue;
|
|
76
|
+
const valueEl = byId.get(`Extension.extension:${sliceName}.value[x]`);
|
|
77
|
+
let valueTypes = typeCodes(valueEl);
|
|
78
|
+
let binding = readBinding(valueEl);
|
|
79
|
+
// The binding usually sits on the typed slice (…value[x]:valueCoding) rather than
|
|
80
|
+
// on value[x] itself, which is where the Payor extension puts it.
|
|
81
|
+
for (const code of valueTypes.length ? valueTypes : ['']) {
|
|
82
|
+
const suffix = code ? code.charAt(0).toUpperCase() + code.slice(1) : '';
|
|
83
|
+
const typedEl = suffix ? byId.get(`Extension.extension:${sliceName}.value[x]:value${suffix}`) : undefined;
|
|
84
|
+
if (!typedEl)
|
|
85
|
+
continue;
|
|
86
|
+
binding = binding ?? readBinding(typedEl);
|
|
87
|
+
if (valueTypes.length === 0)
|
|
88
|
+
valueTypes = typeCodes(typedEl);
|
|
89
|
+
}
|
|
90
|
+
out.push({
|
|
91
|
+
sliceName,
|
|
92
|
+
url,
|
|
93
|
+
min: typeof el.min === 'number' ? el.min : 0,
|
|
94
|
+
valueTypes,
|
|
95
|
+
binding,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
@@ -24,6 +24,38 @@ export function capitalize(str) {
|
|
|
24
24
|
return str;
|
|
25
25
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The property name a choice element takes for one of its permitted types.
|
|
29
|
+
*
|
|
30
|
+
* A choice element has no property of its own: FHIR declares one property per
|
|
31
|
+
* permitted type, named for the type code capitalized (`value[x]` as a
|
|
32
|
+
* SampledData is `valueSampledData`, as a `code` primitive it is `valueCode`).
|
|
33
|
+
*
|
|
34
|
+
* Pass the *type code*, never a profile name. R4 declares
|
|
35
|
+
* `Coverage.costToBeneficiary.value[x]` as a Quantity profiled to SimpleQuantity,
|
|
36
|
+
* and the property is `valueQuantity` — `valueSimpleQuantity` does not exist on
|
|
37
|
+
* any resource, so emitting it produces data every validator rejects.
|
|
38
|
+
*
|
|
39
|
+
* @param baseName the choice element's name with `[x]` already removed
|
|
40
|
+
*/
|
|
41
|
+
export function choicePropertyName(baseName, typeCode) {
|
|
42
|
+
return baseName + capitalize(typeCode);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The single type a choice element has been narrowed to, or undefined while it
|
|
46
|
+
* still permits more than one.
|
|
47
|
+
*
|
|
48
|
+
* A still-open choice has no one property name, so callers must leave the base
|
|
49
|
+
* definition's expanded properties in force rather than pick a type.
|
|
50
|
+
*/
|
|
51
|
+
export function narrowedChoiceType(field) {
|
|
52
|
+
if (field.typeOptions && field.typeOptions.length > 1)
|
|
53
|
+
return undefined;
|
|
54
|
+
const type = field.typeOptions?.[0]?.code || field.type;
|
|
55
|
+
if (!type || type === 'any' || type === 'unknown')
|
|
56
|
+
return undefined;
|
|
57
|
+
return type;
|
|
58
|
+
}
|
|
27
59
|
/**
|
|
28
60
|
* Extract base FHIR types from all interface declarations in a file.
|
|
29
61
|
* Handles:
|
|
@@ -149,6 +181,48 @@ export function alignFields(newFields, oldFields) {
|
|
|
149
181
|
// Return the original oldFields and the aligned newFields
|
|
150
182
|
return { alignedNewFields, oldFields };
|
|
151
183
|
}
|
|
184
|
+
/** A type the parser could not pin down from the element itself. */
|
|
185
|
+
function isUnstatedType(type) {
|
|
186
|
+
return !type || type === 'any';
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Carry forward what a differential element left unstated.
|
|
190
|
+
*
|
|
191
|
+
* A differential says only what it changes, so an element that constrains
|
|
192
|
+
* cardinality alone carries no `type` and an element that constrains `min` alone
|
|
193
|
+
* carries no `max`. Both are inherited rather than defaulted away.
|
|
194
|
+
*/
|
|
195
|
+
function inheritUnstatedFromBase(field, base) {
|
|
196
|
+
if (!base)
|
|
197
|
+
return field;
|
|
198
|
+
const merged = { ...field };
|
|
199
|
+
if (isUnstatedType(merged.type) && !isUnstatedType(base.type)) {
|
|
200
|
+
merged.type = base.type;
|
|
201
|
+
}
|
|
202
|
+
if (!merged.baseTypeCode && base.baseTypeCode) {
|
|
203
|
+
merged.baseTypeCode = base.baseTypeCode;
|
|
204
|
+
}
|
|
205
|
+
// `max` absent means the differential did not narrow the upper bound, so the
|
|
206
|
+
// base's bound — and with it whether the element repeats — still holds.
|
|
207
|
+
if (merged.max === undefined && base.max !== undefined) {
|
|
208
|
+
merged.max = base.max;
|
|
209
|
+
merged.isArray = base.isArray;
|
|
210
|
+
}
|
|
211
|
+
if (!merged.typeOptions?.length && base.typeOptions?.length) {
|
|
212
|
+
merged.typeOptions = base.typeOptions;
|
|
213
|
+
}
|
|
214
|
+
// An extension slice is recognised at runtime by the canonical its profile
|
|
215
|
+
// names. A derived profile that only tightens cardinality does not restate it —
|
|
216
|
+
// KBV_PR_ERP_Medication_PZN redeclares extension:Kategorie and extension:Normgroesse
|
|
217
|
+
// with no type.profile, while KBV_PR_Base_Medication gives them
|
|
218
|
+
// KBV_EX_Base_Medication_Type and .../normgroesse. Dropping the base's left two
|
|
219
|
+
// of four slices unidentifiable, so the closed-slicing check on
|
|
220
|
+
// Medication.extension could not build its allowed set and stayed silent.
|
|
221
|
+
if (!merged.profileUrls?.length && base.profileUrls?.length) {
|
|
222
|
+
merged.profileUrls = base.profileUrls;
|
|
223
|
+
}
|
|
224
|
+
return merged;
|
|
225
|
+
}
|
|
152
226
|
export function mergeFields(newFields, oldFields) {
|
|
153
227
|
const fieldMap = new Map();
|
|
154
228
|
// Create unique key for field - use elementId if available (includes slice names for child elements)
|
|
@@ -176,9 +250,22 @@ export function mergeFields(newFields, oldFields) {
|
|
|
176
250
|
}
|
|
177
251
|
fieldMap.set(key, field);
|
|
178
252
|
}
|
|
179
|
-
// Overwrite or add newFields to the map
|
|
253
|
+
// Overwrite or add newFields to the map.
|
|
254
|
+
//
|
|
255
|
+
// A differential element states only what it *changes*. Replacing the base
|
|
256
|
+
// field wholesale therefore threw away everything it stayed silent about:
|
|
257
|
+
// KBV constrains Medication.amount.numerator.value to max=0 and says nothing
|
|
258
|
+
// about its type, so the merged field read `any` and every type-aware check
|
|
259
|
+
// skipped it — HL7 reported "The value 'Example value' is not a valid decimal"
|
|
260
|
+
// where we said nothing. The same silence on `max` left Medication.ingredient
|
|
261
|
+
// with isArray=false despite being 0..* in the base, so paths beneath it were
|
|
262
|
+
// never walked.
|
|
263
|
+
//
|
|
264
|
+
// Only genuinely absent properties are filled in; anything the differential
|
|
265
|
+
// states still wins.
|
|
180
266
|
for (const field of newFields) {
|
|
181
|
-
|
|
267
|
+
const key = getFieldKey(field);
|
|
268
|
+
fieldMap.set(key, inheritUnstatedFromBase(field, fieldMap.get(key)));
|
|
182
269
|
}
|
|
183
270
|
// Return the merged fields as an array
|
|
184
271
|
return Array.from(fieldMap.values());
|
|
@@ -220,9 +307,16 @@ function caseAwareWriteFile(filePath, content) {
|
|
|
220
307
|
}
|
|
221
308
|
/**
|
|
222
309
|
* Extracts the base resource name from a baseDefinition URL.
|
|
310
|
+
*
|
|
311
|
+
* The version suffix has to go first. A canonical may be pinned
|
|
312
|
+
* (`http://hl7.org/fhir/StructureDefinition/Base|4.0.1`, as IPS 2.0.1's logical
|
|
313
|
+
* models write it), and keeping it yielded the base name `Base_4_0_1` after
|
|
314
|
+
* sanitising. That defeated the `baseResource === 'Base'` check that suppresses
|
|
315
|
+
* extending the abstract root, so the emitter produced
|
|
316
|
+
* `export type Document = Base_4_0_1` plus an import of a type nothing generates.
|
|
223
317
|
*/
|
|
224
318
|
export function getBaseResource(baseDefinition) {
|
|
225
|
-
return baseDefinition
|
|
319
|
+
return stripVersionFromCanonicalUrl(baseDefinition ?? '').split('/').pop() || 'Resource';
|
|
226
320
|
}
|
|
227
321
|
/**
|
|
228
322
|
* Sanitizes a string so it can be used as a valid TypeScript identifier.
|
|
@@ -26,7 +26,7 @@ export function renderClassTemplate(params) {
|
|
|
26
26
|
const helperImportList = Array.from(helperImports).join(', ');
|
|
27
27
|
const hasVsBindings = Object.keys(valueSetBindings).length > 0;
|
|
28
28
|
return `
|
|
29
|
-
import { validate${interfaceName}, ${interfaceName} } from "./${interfaceName}.js";
|
|
29
|
+
import { validate${interfaceName}, type ${interfaceName} } from "./${interfaceName}.js";
|
|
30
30
|
import type { ValidatorOptions } from "./ValidatorOptions.js";
|
|
31
31
|
// Only import helpers actually referenced below (dynamic based on required fields)
|
|
32
32
|
import { ${helperImportList} } from "./random/index.js";
|
|
@@ -55,7 +55,13 @@ function safeClone<T>(value: T): T {
|
|
|
55
55
|
|
|
56
56
|
export class ${className} {
|
|
57
57
|
${generatePatternMetadata(fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder, codeableConceptBindings)}
|
|
58
|
-
|
|
58
|
+
private resource: ${interfaceName};
|
|
59
|
+
|
|
60
|
+
// Declared and assigned rather than written as a parameter property, which
|
|
61
|
+
// erasableSyntaxOnly bans: it emits real assignment code from what reads as a
|
|
62
|
+
// type annotation. Generated output is held to the same strictness as src/.
|
|
63
|
+
constructor(resource: ${interfaceName}) {
|
|
64
|
+
this.resource = resource;
|
|
59
65
|
${constructorMetaProfile}
|
|
60
66
|
}
|
|
61
67
|
/** Validate current resource instance. */
|
|
@@ -221,7 +221,9 @@ export function buildNestedRequirementsObject(nestedRequirements) {
|
|
|
221
221
|
if (!nestedRequirements || nestedRequirements.length === 0) {
|
|
222
222
|
return null;
|
|
223
223
|
}
|
|
224
|
-
//
|
|
224
|
+
// Relative paths that are arrays, so nesting knows where to wrap. Keyed by the
|
|
225
|
+
// full relative path rather than by segment name: `name.part` is 0..* while the
|
|
226
|
+
// `part` string inside it is 1..1, and one shared verdict wrapped the string too.
|
|
225
227
|
const allArraySegments = new Set();
|
|
226
228
|
for (const req of nestedRequirements) {
|
|
227
229
|
if (req.arraySegments) {
|
|
@@ -250,7 +252,7 @@ export function buildNestedRequirementsObject(nestedRequirements) {
|
|
|
250
252
|
// Navigate/create nested objects for all but the last part
|
|
251
253
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
252
254
|
const part = parts[i];
|
|
253
|
-
const isArraySegment = allArraySegments.has(
|
|
255
|
+
const isArraySegment = allArraySegments.has(parts.slice(0, i + 1).join('.'));
|
|
254
256
|
if (!(part in current)) {
|
|
255
257
|
// Create either an array with one object or a plain object
|
|
256
258
|
if (isArraySegment) {
|
|
@@ -346,7 +348,7 @@ export function buildNestedRequirementsObject(nestedRequirements) {
|
|
|
346
348
|
}
|
|
347
349
|
// If the final segment is an array segment, wrap the value in an array.
|
|
348
350
|
// Also wrap if the field is inherently an array in FHIR base types
|
|
349
|
-
const shouldWrap = (allArraySegments.has(
|
|
351
|
+
const shouldWrap = (allArraySegments.has(req.path) || FHIR_BASE_ARRAY_FIELDS.has(lastPart))
|
|
350
352
|
&& !Array.isArray(value);
|
|
351
353
|
if (shouldWrap) {
|
|
352
354
|
current[lastPart] = [value];
|
|
@@ -176,11 +176,16 @@ export type FetchFn = typeof globalThis.fetch;
|
|
|
176
176
|
export class FhirResourceReader<T, S extends SearchParams = SearchParams> implements FhirResourceSearcher<T, S> {
|
|
177
177
|
private readonly fetchFn: FetchFn;
|
|
178
178
|
|
|
179
|
+
public readonly baseUrl: string;
|
|
180
|
+
public readonly resourceType: string;
|
|
181
|
+
|
|
179
182
|
constructor(
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
baseUrl: string,
|
|
184
|
+
resourceType: string,
|
|
182
185
|
fetchFn?: FetchFn,
|
|
183
186
|
) {
|
|
187
|
+
this.baseUrl = baseUrl;
|
|
188
|
+
this.resourceType = resourceType;
|
|
184
189
|
this.fetchFn = fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
185
190
|
}
|
|
186
191
|
|
|
@@ -321,11 +326,16 @@ import type { FetchFn } from "./resource-reader.js";
|
|
|
321
326
|
export class FhirResourceWriterImpl<T> implements FhirResourceWriter<T> {
|
|
322
327
|
private readonly fetchFn: FetchFn;
|
|
323
328
|
|
|
329
|
+
public readonly baseUrl: string;
|
|
330
|
+
public readonly resourceType: string;
|
|
331
|
+
|
|
324
332
|
constructor(
|
|
325
|
-
|
|
326
|
-
|
|
333
|
+
baseUrl: string,
|
|
334
|
+
resourceType: string,
|
|
327
335
|
fetchFn?: FetchFn,
|
|
328
336
|
) {
|
|
337
|
+
this.baseUrl = baseUrl;
|
|
338
|
+
this.resourceType = resourceType;
|
|
329
339
|
this.fetchFn = fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
330
340
|
}
|
|
331
341
|
|
|
@@ -451,7 +461,11 @@ export type BundleEntry<T extends FhirResource = FhirResource> = {
|
|
|
451
461
|
* FHIR Bundle Parser - provides type-safe parsing and filtering of FHIR Bundles
|
|
452
462
|
*/
|
|
453
463
|
export class BundleParser {
|
|
454
|
-
|
|
464
|
+
private readonly bundle: Bundle<FhirResource>;
|
|
465
|
+
|
|
466
|
+
constructor(bundle: Bundle<FhirResource>) {
|
|
467
|
+
this.bundle = bundle;
|
|
468
|
+
}
|
|
455
469
|
|
|
456
470
|
/**
|
|
457
471
|
* Get all resources from the bundle
|
|
@@ -659,11 +673,13 @@ ${writerMethods}
|
|
|
659
673
|
export class FhirClient {
|
|
660
674
|
private readonly readClient: FhirReadClient;
|
|
661
675
|
private readonly writeClient: FhirWriteClient;
|
|
676
|
+
public readonly baseUrl: string;
|
|
662
677
|
|
|
663
678
|
constructor(
|
|
664
|
-
|
|
679
|
+
baseUrl: string,
|
|
665
680
|
fetchFn?: FetchFn,
|
|
666
681
|
) {
|
|
682
|
+
this.baseUrl = baseUrl;
|
|
667
683
|
this.readClient = new FhirReadClient(baseUrl, fetchFn);
|
|
668
684
|
this.writeClient = new FhirWriteClient(baseUrl, fetchFn);
|
|
669
685
|
}
|
|
@@ -167,7 +167,10 @@ export class SmartAuth {
|
|
|
167
167
|
private readonly storage: Storage;
|
|
168
168
|
private readonly keys: ReturnType<typeof storageKeys>;
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
public readonly config: SmartConfig;
|
|
171
|
+
|
|
172
|
+
constructor(config: SmartConfig) {
|
|
173
|
+
this.config = config;
|
|
171
174
|
this.storage = config.storage ?? sessionStorage;
|
|
172
175
|
this.keys = storageKeys(config.storagePrefix ?? "smart_");
|
|
173
176
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* }
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
import { capitalize, sanitizeIdentifier } from '../../core/utils.js';
|
|
21
|
+
import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier } from '../../core/utils.js';
|
|
22
22
|
import { getRules } from '../../fhir/versionContext.js';
|
|
23
23
|
import { logger } from '../../../logger.js';
|
|
24
24
|
const log = logger.withTag('backbone-slices');
|
|
@@ -135,7 +135,7 @@ function collectSliceGroups(fields, baseFields, baseResource) {
|
|
|
135
135
|
if (!isEligibleBackbonePath(field.baseTypeCode || field.type || '', path, baseResource))
|
|
136
136
|
continue;
|
|
137
137
|
const childFields = gatherChildFields(fields, path);
|
|
138
|
-
const slicingRules = field.slicingRules
|
|
138
|
+
const slicingRules = field.slicingRules ?? 'open';
|
|
139
139
|
seenPaths.add(path);
|
|
140
140
|
groups.push({ parentField: field, sliceFields, childFields, slicingRules });
|
|
141
141
|
}
|
|
@@ -160,7 +160,7 @@ function collectSliceGroups(fields, baseFields, baseResource) {
|
|
|
160
160
|
if (sliceFields.length === 0)
|
|
161
161
|
continue;
|
|
162
162
|
const childFields = gatherChildFields(fields, path);
|
|
163
|
-
const slicingRules = parentField.slicingRules
|
|
163
|
+
const slicingRules = parentField.slicingRules ?? 'open';
|
|
164
164
|
seenPaths.add(path);
|
|
165
165
|
groups.push({ parentField, sliceFields, childFields, slicingRules });
|
|
166
166
|
}
|
|
@@ -263,7 +263,18 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
|
|
|
263
263
|
// metadata and their inherited type (e.g. Quantity) is misleading
|
|
264
264
|
if (choiceTypeParents.has(relativePath))
|
|
265
265
|
continue;
|
|
266
|
-
|
|
266
|
+
// A choice element has no property of its own, so it is named for the single
|
|
267
|
+
// type the slice narrowed it to. Stripping `[x]` produced `value`, `effective`
|
|
268
|
+
// and the like — properties no resource carries, so the slice demanded a
|
|
269
|
+
// property FHIR has no such thing as while leaving the real `valueSampledData`
|
|
270
|
+
// unconstrained. A choice still open here names nothing and is skipped.
|
|
271
|
+
let propName = relativePath;
|
|
272
|
+
if (propName.endsWith('[x]')) {
|
|
273
|
+
const narrowedTo = narrowedChoiceType(child);
|
|
274
|
+
if (!narrowedTo)
|
|
275
|
+
continue;
|
|
276
|
+
propName = choicePropertyName(propName.slice(0, -'[x]'.length), narrowedTo);
|
|
277
|
+
}
|
|
267
278
|
if (child.fixedValue !== undefined) {
|
|
268
279
|
// Fixed value constraint
|
|
269
280
|
const fixedLiteral = JSON.stringify(child.fixedValue);
|
|
@@ -191,6 +191,33 @@ export class ImportManager {
|
|
|
191
191
|
}
|
|
192
192
|
return statements.join('\n');
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Whether `name` is referenced in `body`, counting the profile ids that
|
|
196
|
+
* resolve to it as references too.
|
|
197
|
+
*
|
|
198
|
+
* An external profile can be reached under more than one name: the id taken
|
|
199
|
+
* from its canonical URL (`AuDosage` from `.../au-dosage`) and the interface
|
|
200
|
+
* name it is finally generated under (`AUBaseDosage`, from the SD's name).
|
|
201
|
+
* `generateImportStatements` deduplicates those to the resolved name, but the
|
|
202
|
+
* interface body is still written with the id at that point — a later pass
|
|
203
|
+
* rewrites it. Matching only the resolved name therefore pruned an import that
|
|
204
|
+
* the rewritten body goes on to need, leaving a dangling reference (TS2304 on
|
|
205
|
+
* `AUBaseDosage` in au-core 2.0.0's AUCoreMedicationStatement, and the same
|
|
206
|
+
* shape in davinci-pas and davinci-dtr).
|
|
207
|
+
*/
|
|
208
|
+
isReferencedAllowingAliases(name, body) {
|
|
209
|
+
if (new RegExp(`\\b${name}\\b`).test(body))
|
|
210
|
+
return true;
|
|
211
|
+
if (!this.profileIdToName)
|
|
212
|
+
return false;
|
|
213
|
+
for (const [alias, resolved] of this.profileIdToName) {
|
|
214
|
+
if (resolved !== name || alias === name)
|
|
215
|
+
continue;
|
|
216
|
+
if (new RegExp(`\\b${alias}\\b`).test(body))
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
194
221
|
/**
|
|
195
222
|
* Prunes unused imports from generated code.
|
|
196
223
|
* Removes import statements for types that aren't actually used.
|
|
@@ -213,8 +240,7 @@ export class ImportManager {
|
|
|
213
240
|
const used = [];
|
|
214
241
|
const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
|
|
215
242
|
for (const name of specifiers) {
|
|
216
|
-
|
|
217
|
-
if (re.test(output.replace(full, ''))) {
|
|
243
|
+
if (this.isReferencedAllowingAliases(name, output.replace(full, ''))) {
|
|
218
244
|
used.push(name);
|
|
219
245
|
}
|
|
220
246
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { capitalize, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
|
|
1
|
+
import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
|
|
2
2
|
import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
|
|
3
3
|
import { logger } from '../../../logger.js';
|
|
4
4
|
import { getRules } from '../../fhir/versionContext.js';
|
|
@@ -214,25 +214,30 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
214
214
|
});
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (fieldName === "event[x]" && field.type === "any" && field.fixedValue === undefined)
|
|
220
|
-
return;
|
|
221
|
-
if (fieldName === "event[x]")
|
|
222
|
-
fieldName = "event";
|
|
223
|
-
if (fieldName === "deceased[x]")
|
|
224
|
-
fieldName = "deceased";
|
|
217
|
+
// `extension[x]` is a parser artifact rather than a FHIR choice — Extension.extension
|
|
218
|
+
// is a plain repeating element — so it keeps its name.
|
|
225
219
|
if (fieldName === "extension[x]")
|
|
226
220
|
fieldName = "extension";
|
|
227
|
-
if (fieldName === "multipleBirth[x]")
|
|
228
|
-
fieldName = "multipleBirth";
|
|
229
221
|
// Helper to retrieve original last segment before local renaming
|
|
230
222
|
function fieldNameOriginal(fullName) { return fullName.split('.').pop() || fullName; }
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
223
|
+
// Choice elements carry no property of their own: FHIR declares one property
|
|
224
|
+
// per permitted type. A profile that narrows the choice to a single type names
|
|
225
|
+
// that property, so the constraint lands on the property the resource actually
|
|
226
|
+
// carries; anything still open is skipped, leaving the base definition's
|
|
227
|
+
// expanded choice properties in force (onsetDateTime, onsetAge, …).
|
|
228
|
+
//
|
|
229
|
+
// value[x], event[x], deceased[x] and multipleBirth[x] used to be renamed to
|
|
230
|
+
// value/event/deceased/multipleBirth, none of which exist on any resource.
|
|
231
|
+
// A slice narrowing Observation.component.value[x] to SampledData declared
|
|
232
|
+
// `value: SampledData`, so a caller writing correct FHIR failed to typecheck
|
|
233
|
+
// for a missing `value` while `valueSampledData` went unconstrained.
|
|
234
|
+
if (/\[x\]$/.test(fieldNameOriginal(field.name))) {
|
|
235
|
+
const narrowedTo = narrowedChoiceType(field);
|
|
236
|
+
if (!narrowedTo) {
|
|
237
|
+
debug('skip choice element to inherit base choice fields', field.name);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
fieldName = choicePropertyName(fieldName.replace(/\[x\]$/, ''), narrowedTo);
|
|
236
241
|
}
|
|
237
242
|
if (isFhirType(capitalize(fieldName)) && field.type === "any" && field.fixedValue === undefined && !(fieldName === 'extension' && hasSamePathExtensionSlices)) {
|
|
238
243
|
if (isAaaCodesContext)
|
|
@@ -13,6 +13,22 @@ export function canonicalToTypeName(name, profileUrlToName) {
|
|
|
13
13
|
}
|
|
14
14
|
t = t.split('/').pop() || t;
|
|
15
15
|
}
|
|
16
|
+
// A bare profile id also has a registered interface name, and it is not
|
|
17
|
+
// derivable from the id: davinci-pas 2.2.1's `profile-identifier` is generated
|
|
18
|
+
// as `PASIdentifier` (from the SD's `name`), so PascalCasing the id produced
|
|
19
|
+
// `ProfileIdentifier` — a type that is referenced and imported but never
|
|
20
|
+
// emitted (TS2307/TS2304 across seven files). Resolve through the registry by
|
|
21
|
+
// matching the URL's last segment, and only when it is unambiguous.
|
|
22
|
+
if (profileUrlToName && !/^https?:\/\//.test(t)) {
|
|
23
|
+
const matches = new Set();
|
|
24
|
+
for (const [url, generatedName] of profileUrlToName) {
|
|
25
|
+
if ((url.split('/').pop() || '').toLowerCase() === t.toLowerCase()) {
|
|
26
|
+
matches.add(generatedName);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (matches.size === 1)
|
|
30
|
+
return [...matches][0];
|
|
31
|
+
}
|
|
16
32
|
// if already a valid identifier with capitalization, return as-is
|
|
17
33
|
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(t) && /[A-Z]/.test(t[0]))
|
|
18
34
|
return t;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { capitalize, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
|
|
6
6
|
import { logger } from '../../../logger.js';
|
|
7
|
+
import {} from './importManager.js';
|
|
7
8
|
import { getRules } from '../../fhir/versionContext.js';
|
|
8
9
|
export function postProcessExtensions(params) {
|
|
9
10
|
const { interfaces, interfaceName, baseResource, newFields, baseFieldByLastSegment, importManager, existingProfiles, profileUrlToName, generatedAliasTypes, findInterfaceIndex, inferBackboneType, addTypeImport } = params;
|