babelfhir-ts 1.5.18 → 1.5.20
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 +37 -46
- package/out/src/generator/core/constants.js +40 -0
- package/out/src/generator/core/generatedPackageDeps.js +86 -11
- package/out/src/generator/core/utils.js +40 -1
- package/out/src/generator/emitters/class/classGenerator.js +2 -1
- package/out/src/generator/emitters/class/classGeneratorHelpers.js +8 -1
- package/out/src/generator/emitters/class/classTemplate.js +3 -3
- package/out/src/generator/emitters/class/enrichContextTemplate.js +11 -1
- package/out/src/generator/emitters/class/enrichCtxTransform.js +1 -0
- package/out/src/generator/emitters/class/enrichResourceHandlers.js +21 -2
- package/out/src/generator/emitters/class/enrichResourceObservationHandler.js +4 -4
- package/out/src/generator/emitters/class/enrichResourceTemplate.js +27 -8
- package/out/src/generator/emitters/class/fieldPatternExtractor.js +8 -1
- package/out/src/generator/emitters/class/randomSupportGenerator.js +5 -4
- package/out/src/generator/emitters/class/randomUtilitiesTemplate.js +79 -0
- package/out/src/generator/emitters/class/sliceElementDefaults.js +5 -3
- package/out/src/generator/emitters/client/clientGenerator.js +6 -1
- package/out/src/generator/emitters/interface/backboneSliceTyping.js +13 -2
- 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/processNestedField.js +12 -34
- package/out/src/generator/emitters/prefab/prefabRenderer.js +4 -1
- package/out/src/generator/emitters/prefab/prefabRuntimeAssets.js +23 -0
- package/out/src/generator/emitters/validator/sliceBackboneValidation.js +28 -24
- package/out/src/generator/emitters/validator/sliceDelegation.js +450 -0
- package/out/src/generator/emitters/validator/sliceExtensionValidation.js +22 -22
- package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +55 -318
- package/out/src/generator/emitters/validator/sliceValidatorUtils.js +90 -17
- 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 +91 -62
- package/out/src/generator/emitters/validator/validatorExpressions.js +54 -0
- package/out/src/generator/emitters/validator/validatorFieldBuilders.js +275 -37
- package/out/src/generator/emitters/validator/validatorGenerator.js +168 -87
- package/out/src/generator/emitters/validator/validatorRuntime.js +269 -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/parser/packageParser.js +1 -8
- package/out/src/generator/sdProcessor.js +97 -12
- package/out/src/generator/sdProcessorHelpers.js +1 -1
- package/out/src/generator/sdProcessorRequiredFields.js +95 -0
- package/out/src/main.js +37 -11
- package/package.json +3 -2
- package/parity-matrix.json +18 -18
package/README.md
CHANGED
|
@@ -46,59 +46,42 @@
|
|
|
46
46
|
- **Type-safe extension handling** with proper slicing and nested extension support
|
|
47
47
|
- **Random data builders** for testing and development (when class generation is enabled)
|
|
48
48
|
- **Zero manual mapping**—consume any FHIR package or Implementation Guide directly from registries
|
|
49
|
-
- **Fast and lightweight**—
|
|
50
|
-
- **Type-safe FHIR client** — generated client extends [`@babelfhir-ts/client-r4`](https://www.npmjs.com/package/@babelfhir-ts/client-r4) / [`client-r4b`](https://www.npmjs.com/package/@babelfhir-ts/client-r4b) / [`client-r5`](https://www.npmjs.com/package/@babelfhir-ts/client-r5) with profile-specific methods (e.g., `.
|
|
49
|
+
- **Fast and lightweight**—the CLI pulls no FHIRPath engine of its own; generated packages declare `fhirpath` as a *peer* dependency (`>=4.9.1 <6`), so the host app owns the version
|
|
50
|
+
- **Type-safe FHIR client** — generated client extends [`@babelfhir-ts/client-r4`](https://www.npmjs.com/package/@babelfhir-ts/client-r4) / [`client-r4b`](https://www.npmjs.com/package/@babelfhir-ts/client-r4b) / [`client-r5`](https://www.npmjs.com/package/@babelfhir-ts/client-r5) with profile-specific methods (e.g., `.uSCorePatientProfile()`, `.pASClaim()`) on top of base resource accessors
|
|
51
51
|
- **Install any FHIR profile as a node module**—use `babelfhir-ts install` to add Implementation Guides directly to your project
|
|
52
52
|
|
|
53
53
|
<!-- PARITY-BADGES:START - Do not remove or modify this section -->
|
|
54
54
|
## Continuous Validation
|
|
55
55
|
|
|
56
|
-
Every
|
|
56
|
+
Every push to `develop` and `main` runs the parity pipeline, which validates generated code against real-world FHIR Implementation Guides. (Pull requests run the faster `CI` workflow: typecheck, unit tests, and generated-artifact drift checks.) For each IG, the pipeline:
|
|
57
57
|
|
|
58
58
|
1. Downloads the FHIR package from a registry
|
|
59
59
|
2. Generates TypeScript interfaces, validators, and classes
|
|
60
60
|
3. Compiles the output with `tsc` (zero errors required)
|
|
61
61
|
4. Generates `empty()` and `random()` test resources for every profile
|
|
62
|
-
5. Validates those resources against two external FHIR validators
|
|
63
|
-
|
|
64
|
-
###
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
| Universal | SDC (Structured Data Capture) | `hl7.fhir.uv.sdc@3.0.0` | R4 |
|
|
86
|
-
| Universal | Genomics Reporting | `hl7.fhir.uv.genomics-reporting@3.0.0` | R4 |
|
|
87
|
-
| Universal | CPG (Clinical Practice Guidelines) | `hl7.fhir.uv.cpg@2.0.0` | R4 |
|
|
88
|
-
| DE | ISiK Basis | `de.gematik.isik-basismodul@4.0.3` | R4 |
|
|
89
|
-
| DE | KBV eRezept | `kbv.ita.erp@1.1.1` | R4 |
|
|
90
|
-
| DE | DE Basisprofil | `de.basisprofil.r4@1.5.0` | R4 |
|
|
91
|
-
| DE | ISiK Medikation | `de.gematik.isik-medikation@4.0.1` | R4 |
|
|
92
|
-
| CH | CH Core (Switzerland) | `ch.fhir.ig.ch-core@5.0.0` | R4 |
|
|
93
|
-
| AU | AU Core (Australia) | `hl7.fhir.au.core@1.0.0` | R4 |
|
|
94
|
-
| IHE | PIXm | `ihe.iti.pixm@3.0.4` | R4 |
|
|
95
|
-
| IHE | MHD | `ihe.iti.mhd@4.2.2` | R4 |
|
|
96
|
-
| R5 | AE Research (R5) | `hl7.fhir.uv.ae-research-ig@1.0.1` | R5 |
|
|
97
|
-
| R5 | eMedicinal Product (R5) | `hl7.fhir.uv.emedicinal-product-info@1.0.0` | R5 |
|
|
98
|
-
|
|
99
|
-
### Validation with Firely .NET SDK
|
|
100
|
-
|
|
101
|
-
The first pipeline validates generated resources using the [Firely .NET SDK validator](https://docs.fire.ly/projects/Firely-NET-SDK/). Results are published as live badges:
|
|
62
|
+
5. Validates those resources against two independent external FHIR validators
|
|
63
|
+
|
|
64
|
+
### Validated Implementation Guides (30)
|
|
65
|
+
|
|
66
|
+
Every parity run validates **30 real-world IGs** (28 R4, 2 R5). The list is not duplicated here — it is generated from the parity suite's own source of truth into [`parity-matrix.json`](./parity-matrix.json).
|
|
67
|
+
|
|
68
|
+
Each validated IG is also published as a ready-to-install package — `@max-health-inc/fhir-<name>` — so you do not have to generate it yourself:
|
|
69
|
+
|
|
70
|
+
**[→ Browse the FHIR IG registry](https://github.com/orgs/Max-Health-Inc/packages?ecosystem=npm&q=fhir-)**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# GitHub Packages needs the scope mapped, and a token with read:packages
|
|
74
|
+
echo "@max-health-inc:registry=https://npm.pkg.github.com" >> .npmrc
|
|
75
|
+
npm install @max-health-inc/fhir-us-core
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Published by [Max-Health-Inc/fhir-igs](https://github.com/Max-Health-Inc/fhir-igs), which pins a babelfhir-ts version and republishes an IG only when the IG version, the pinned generator, or the generation flags actually change.
|
|
79
|
+
|
|
80
|
+
### Validation with the Firely SDK Validator
|
|
81
|
+
|
|
82
|
+
The `firely` job validates generated resources using the [Firely SDK Validator](https://docs.fire.ly/projects/Firely-NET-SDK/validation.html) (`Firely.Fhir.Validation`, v3.x) running on the Firely .NET SDK (`Hl7.Fhir`, v6.x) — two separately versioned packages. Results are published as live badges:
|
|
83
|
+
|
|
84
|
+
<details><summary>Per-IG results</summary>
|
|
102
85
|
|
|
103
86
|

|
|
104
87
|

|
|
@@ -129,9 +112,13 @@ The first pipeline validates generated resources using the [Firely .NET SDK vali
|
|
|
129
112
|

|
|
130
113
|

|
|
131
114
|
|
|
115
|
+
</details>
|
|
116
|
+
|
|
132
117
|
### Validation with HL7 Java Validator
|
|
133
118
|
|
|
134
|
-
The
|
|
119
|
+
The `hl7` job validates the same artifacts using the [official HL7 FHIR Validator](https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator), the reference implementation for FHIR conformance checking:
|
|
120
|
+
|
|
121
|
+
<details><summary>Per-IG results</summary>
|
|
135
122
|
|
|
136
123
|

|
|
137
124
|

|
|
@@ -162,6 +149,10 @@ The second pipeline validates using the [official HL7 FHIR Validator](https://co
|
|
|
162
149
|

|
|
163
150
|

|
|
164
151
|
|
|
152
|
+
</details>
|
|
153
|
+
|
|
154
|
+
> External-validator badges cover the 28 R4/R4B IGs. R5 packages are generated and typechecked, but neither external validator publishes R5 badges yet.
|
|
155
|
+
|
|
165
156
|
> 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.
|
|
166
157
|
|
|
167
158
|
📊 **[Full Report](https://max-health-inc.github.io/BabelFHIR-TS/)**
|
|
@@ -192,9 +183,9 @@ babelfhir-ts install hl7.fhir.us.core@8.0.0
|
|
|
192
183
|
```
|
|
193
184
|
|
|
194
185
|
```ts
|
|
195
|
-
import {
|
|
186
|
+
import { USCorePatientProfileClass } from "./output/USCorePatientProfileClass";
|
|
196
187
|
|
|
197
|
-
const patient =
|
|
188
|
+
const patient = USCorePatientProfileClass.random();
|
|
198
189
|
const { errors, warnings } = await patient.validate();
|
|
199
190
|
```
|
|
200
191
|
|
|
@@ -29,6 +29,27 @@ const packageJson = JSON.parse(require('fs').readFileSync(_pkgPath, 'utf8'));
|
|
|
29
29
|
// ─── Package metadata ────────────────────────────────────────────────────────
|
|
30
30
|
export const PACKAGE_VERSION = packageJson.version;
|
|
31
31
|
export const USER_AGENT = `BabelFHIR-TS/${PACKAGE_VERSION}`;
|
|
32
|
+
/**
|
|
33
|
+
* The range this build declares for `name`, from either dependency section.
|
|
34
|
+
*
|
|
35
|
+
* Lets the generated-package manifest derive its ranges from what this repo
|
|
36
|
+
* actually builds and tests against, instead of restating them in a second place
|
|
37
|
+
* that drifts on every dependency bump.
|
|
38
|
+
*/
|
|
39
|
+
export function ownDependencyRange(name) {
|
|
40
|
+
return packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name] ?? '';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The `@types/fhir` range this build depends on.
|
|
44
|
+
*
|
|
45
|
+
* `scripts/generate-fhir-r4-module.js` builds the `fhir-<version>.d.ts` ambient
|
|
46
|
+
* shims from whatever `@types/fhir` is installed here, so the shims can only
|
|
47
|
+
* resolve against a version that actually declares the aliased types. Generated
|
|
48
|
+
* packages therefore derive their own `@types/fhir` range from this one — see
|
|
49
|
+
* EMITTED_RANGES.fhirTypes in generatedPackageDeps.ts — instead of hardcoding a
|
|
50
|
+
* second range that silently drifts on every dependency bump.
|
|
51
|
+
*/
|
|
52
|
+
export const FHIR_TYPES_DEPENDENCY_RANGE = ownDependencyRange('@types/fhir');
|
|
32
53
|
// ─── Network defaults ────────────────────────────────────────────────────────
|
|
33
54
|
/** Default fetch timeout for FHIR registry / StructureDefinition lookups (ms) */
|
|
34
55
|
export const FETCH_TIMEOUT_MS = 15_000;
|
|
@@ -36,6 +57,25 @@ export const FETCH_TIMEOUT_MS = 15_000;
|
|
|
36
57
|
export const TX_TIMEOUT_MS = 30_000;
|
|
37
58
|
/** Max filename length for terminology expansion cache files */
|
|
38
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
|
+
];
|
|
39
79
|
// ─── FHIR StructureDefinition URL prefixes ───────────────────────────────────
|
|
40
80
|
export const FHIR_SD_URL_PREFIX = 'http://hl7.org/fhir/StructureDefinition/';
|
|
41
81
|
export const FHIR_SD_URL_PREFIX_HTTPS = 'https://hl7.org/fhir/StructureDefinition/';
|
|
@@ -9,22 +9,97 @@
|
|
|
9
9
|
* These ranges describe what the *consumer* of a generated package installs,
|
|
10
10
|
* so they are deliberately wider than this repo's own devDependencies.
|
|
11
11
|
*/
|
|
12
|
+
import { FHIR_TYPES_DEPENDENCY_RANGE, ownDependencyRange } from './constants.js';
|
|
13
|
+
/**
|
|
14
|
+
* `@types/fhir` range emitted into generated packages.
|
|
15
|
+
*
|
|
16
|
+
* Derived from this build's own `@types/fhir` dependency, never hardcoded. The
|
|
17
|
+
* emitted `fhir-<version>.d.ts` shim aliases types out of the installed
|
|
18
|
+
* `@types/fhir` (e.g. `export type MoneyQuantity = fhir4.MoneyQuantity`), so a
|
|
19
|
+
* consumer resolving an older version gets TS2694 on a type the shim references
|
|
20
|
+
* but their copy does not declare — `MoneyQuantity` does not exist at all before
|
|
21
|
+
* @types/fhir 0.0.42. This repo's own output typecheck cannot catch that,
|
|
22
|
+
* because `resolveGeneratorTypeRoots()` points `typeRoots` at babelfhir's own
|
|
23
|
+
* always-present `@types` — so the floor has to be correct by construction.
|
|
24
|
+
*
|
|
25
|
+
* A floor rather than an exact pin, and this is the part that used to be wrong:
|
|
26
|
+
* `^0.0.41` looks like a floor but npm's caret on `0.0.x` resolves to exactly
|
|
27
|
+
* 0.0.41. Consumers routinely install several generated packages alongside their
|
|
28
|
+
* own `@types/fhir`, so exact pins across them would conflict or install nested
|
|
29
|
+
* duplicates — two `fhir` global namespaces. Capped below 0.1.0 because
|
|
30
|
+
* `@types/fhir` 1.x/3.x restructure the namespaces entirely.
|
|
31
|
+
*/
|
|
32
|
+
function emittedFhirTypesRange() {
|
|
33
|
+
const floor = FHIR_TYPES_DEPENDENCY_RANGE.replace(/^[^0-9]*/, '');
|
|
34
|
+
if (!/^0\.0\.\d+$/.test(floor)) {
|
|
35
|
+
throw new Error(`Cannot derive the emitted @types/fhir range from "${FHIR_TYPES_DEPENDENCY_RANGE}" — ` +
|
|
36
|
+
`expected a 0.0.x version. Moving off the 0.0.x line needs a new upper bound here.`);
|
|
37
|
+
}
|
|
38
|
+
return `>=${floor} <0.1.0`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `zod` range emitted as a peer for `--schema zod` output.
|
|
42
|
+
*
|
|
43
|
+
* Straight pass-through of this build's own range — no widening. Emitted schemas
|
|
44
|
+
* are generated against the installed zod, so advertising a broader range than
|
|
45
|
+
* was tested is the same class of mistake `^0.0.41` was: a manifest claiming
|
|
46
|
+
* support the artifact beside it does not have.
|
|
47
|
+
*/
|
|
48
|
+
function emittedZodRange() {
|
|
49
|
+
const range = ownDependencyRange('zod');
|
|
50
|
+
if (!range) {
|
|
51
|
+
throw new Error('Cannot derive the emitted zod range — this package declares no zod dependency.');
|
|
52
|
+
}
|
|
53
|
+
return range;
|
|
54
|
+
}
|
|
12
55
|
/**
|
|
13
56
|
* Runtime/peer ranges emitted into generated packages.
|
|
14
57
|
*
|
|
15
|
-
* `fhirpath`
|
|
16
|
-
*
|
|
17
|
-
* `fhirpath/fhir-context/<model>/index.js`, both of which are stable across
|
|
18
|
-
* those majors. The `/index.js` filename is required — fhirpath >= 5 declares
|
|
19
|
-
* an `exports` map, so extensionless deep paths no longer resolve.
|
|
58
|
+
* `fhirpath` and `zod` are peers so the host owns the version; everything else is
|
|
59
|
+
* a direct dependency of the generated package.
|
|
20
60
|
*/
|
|
21
61
|
export const EMITTED_RANGES = {
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
62
|
+
/**
|
|
63
|
+
* FHIR type declarations backing the emitted `.d.ts` files.
|
|
64
|
+
*
|
|
65
|
+
* A getter, not a computed value: emittedFhirTypesRange() throws when the
|
|
66
|
+
* derivation no longer holds, and evaluating that at module load would take
|
|
67
|
+
* down every import of this module — `babelfhir-ts --version` included. As a
|
|
68
|
+
* getter the failure is confined to the code that actually emits a manifest,
|
|
69
|
+
* which is the only place a wrong range does damage.
|
|
70
|
+
*/
|
|
71
|
+
get fhirTypes() {
|
|
72
|
+
return emittedFhirTypesRange();
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* FHIRPath engine used by emitted constraint validators.
|
|
76
|
+
*
|
|
77
|
+
* The floor is 4.9.1, not 3.x. Emitted validators evaluate `constraint.expression`
|
|
78
|
+
* verbatim from the IG, and fhirpath 3.x cannot parse the `as` alias form that
|
|
79
|
+
* IGs use freely — `(value as Quantity).value.exists()` throws
|
|
80
|
+
* `Do not know how to alias as by {"is":"isOp"}`. The emitted `evaluate()` call
|
|
81
|
+
* is wrapped in try/catch only for expressions containing `resolve()`, so on 3.x
|
|
82
|
+
* that surfaces as `validate()` rejecting rather than returning errors. 4.9.1 is
|
|
83
|
+
* the floor this repo set when it unlocked `.as()`/`.ofType()`/`is` (24308d806),
|
|
84
|
+
* develop shipped on 4.10.x, and CI runs 5.x.
|
|
85
|
+
*
|
|
86
|
+
* Capped below 6.0.0: majors do break consumers here. 5.x added an `exports`
|
|
87
|
+
* map, which is why the emitted import carries an explicit
|
|
88
|
+
* `/fhir-context/<model>/index.js` filename.
|
|
89
|
+
*/
|
|
90
|
+
fhirpath: '>=4.9.1 <6.0.0',
|
|
91
|
+
/**
|
|
92
|
+
* Schema runtime for `--schema zod` output.
|
|
93
|
+
*
|
|
94
|
+
* Passed through from this build's own `zod` range rather than restated, so the
|
|
95
|
+
* emitted peer is exactly what the generated schemas were produced and tested
|
|
96
|
+
* against. That range is currently an exact pin (`4.3.6`), which is deliberate:
|
|
97
|
+
* emitted schemas use `z.looseObject` and `z.infer`, and zod has moved those
|
|
98
|
+
* across minors before. A host must therefore land on the same version.
|
|
99
|
+
*/
|
|
100
|
+
get zod() {
|
|
101
|
+
return emittedZodRange();
|
|
102
|
+
},
|
|
28
103
|
/**
|
|
29
104
|
* Prefab UI runtime for `--prefab` output.
|
|
30
105
|
*
|
|
@@ -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:
|
|
@@ -220,9 +252,16 @@ function caseAwareWriteFile(filePath, content) {
|
|
|
220
252
|
}
|
|
221
253
|
/**
|
|
222
254
|
* Extracts the base resource name from a baseDefinition URL.
|
|
255
|
+
*
|
|
256
|
+
* The version suffix has to go first. A canonical may be pinned
|
|
257
|
+
* (`http://hl7.org/fhir/StructureDefinition/Base|4.0.1`, as IPS 2.0.1's logical
|
|
258
|
+
* models write it), and keeping it yielded the base name `Base_4_0_1` after
|
|
259
|
+
* sanitising. That defeated the `baseResource === 'Base'` check that suppresses
|
|
260
|
+
* extending the abstract root, so the emitter produced
|
|
261
|
+
* `export type Document = Base_4_0_1` plus an import of a type nothing generates.
|
|
223
262
|
*/
|
|
224
263
|
export function getBaseResource(baseDefinition) {
|
|
225
|
-
return baseDefinition
|
|
264
|
+
return stripVersionFromCanonicalUrl(baseDefinition ?? '').split('/').pop() || 'Resource';
|
|
226
265
|
}
|
|
227
266
|
/**
|
|
228
267
|
* Sanitizes a string so it can be used as a valid TypeScript identifier.
|
|
@@ -4,7 +4,7 @@ import { primitivePlaceholder, generateCodeDefault, makeCodeableConcept, chooseC
|
|
|
4
4
|
import { generateSliceElement, buildNestedRequirementsObject } from './sliceElementGenerator.js';
|
|
5
5
|
import { COMPLEX_SKELETON_MAP, propKey, buildValueExpression, buildCrossReferencedEntryArray, } from './classGeneratorHelpers.js';
|
|
6
6
|
import { renderClassTemplate } from './classTemplate.js';
|
|
7
|
-
export function generateClass(className, interfaceName, baseResource, requiredFields = [], profileUrl, fieldPatterns = [], forbiddenFields = [], valueSetBindings = {}, fieldOrder = []) {
|
|
7
|
+
export function generateClass(className, interfaceName, baseResource, requiredFields = [], profileUrl, fieldPatterns = [], forbiddenFields = [], valueSetBindings = {}, fieldOrder = [], codeableConceptBindings = []) {
|
|
8
8
|
// Decide whether to emit resourceType. If baseResource points at a known resource or ends with 'Resource'.
|
|
9
9
|
const CORE_RESOURCE_BASES = ctx().coreResourceBases;
|
|
10
10
|
const addResourceType = !!baseResource && CORE_RESOURCE_BASES.has(baseResource);
|
|
@@ -572,5 +572,6 @@ export function generateClass(className, interfaceName, baseResource, requiredFi
|
|
|
572
572
|
profileUrl, constructorMetaProfile, setterMetaProfile,
|
|
573
573
|
randomMetaProfileInit, resourceTypeLine, baseResource,
|
|
574
574
|
fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder,
|
|
575
|
+
codeableConceptBindings,
|
|
575
576
|
});
|
|
576
577
|
}
|
|
@@ -88,7 +88,7 @@ export function buildCrossReferencedEntryArray(sliceElements) {
|
|
|
88
88
|
return `(() => { ${decls.join(' ')} return [${rewritten.join(', ')}]; })()`;
|
|
89
89
|
}
|
|
90
90
|
/** Generate the static pattern/forbidden/order/binding metadata properties for a class. */
|
|
91
|
-
export function generatePatternMetadata(fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder = []) {
|
|
91
|
+
export function generatePatternMetadata(fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder = [], codeableConceptBindings = []) {
|
|
92
92
|
const patternMap = {};
|
|
93
93
|
for (const fp of fieldPatterns) {
|
|
94
94
|
patternMap[fp.fieldName] = fp.pattern;
|
|
@@ -107,6 +107,13 @@ export function generatePatternMetadata(fieldPatterns, forbiddenFields, valueSet
|
|
|
107
107
|
/** ValueSet bindings for coded fields - maps field name to ValueSet URL */
|
|
108
108
|
static readonly valueSetBindings: Record<string, string> = ${JSON.stringify(valueSetBindings, null, 2)};
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Bound fields declared as CodeableConcept rather than a code primitive.
|
|
112
|
+
* enrichResource emits \`{ coding: [...] }\` for these; a bare code string would
|
|
113
|
+
* be rejected as "must be an Object, not a Primitive property".
|
|
114
|
+
*/
|
|
115
|
+
protected static readonly _codeableConceptBindings: string[] = ${JSON.stringify(codeableConceptBindings)};
|
|
116
|
+
|
|
110
117
|
/** Code resolver for ValueSet bindings - returns a random valid code from a ValueSet by URL */
|
|
111
118
|
protected static readonly _codeResolver: ((url: string) => { code: string; system: string; display?: string } | undefined) | undefined = ${hasBindings ? `(() => {
|
|
112
119
|
// Try to load the ValueSetRegistry - it may not exist if no ValueSets were generated
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { COMPLEX_SKELETON_MAP, generatePatternMetadata } from './classGeneratorHelpers.js';
|
|
2
2
|
/** Render the final class file content from pre-computed template parameters. */
|
|
3
3
|
export function renderClassTemplate(params) {
|
|
4
|
-
const { interfaceName, className, requiredInitBlock, enableMetaProfile, profileUrl, constructorMetaProfile, setterMetaProfile, randomMetaProfileInit, resourceTypeLine, baseResource, fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder, } = params;
|
|
4
|
+
const { interfaceName, className, requiredInitBlock, enableMetaProfile, profileUrl, constructorMetaProfile, setterMetaProfile, randomMetaProfileInit, resourceTypeLine, baseResource, fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder, codeableConceptBindings, } = params;
|
|
5
5
|
// Determine which skeleton helpers are actually used in requiredInitBlock
|
|
6
6
|
const usedSkeletons = new Set();
|
|
7
7
|
for (const helper of Object.values(COMPLEX_SKELETON_MAP)) {
|
|
@@ -54,7 +54,7 @@ function safeClone<T>(value: T): T {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export class ${className} {
|
|
57
|
-
${generatePatternMetadata(fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder)}
|
|
57
|
+
${generatePatternMetadata(fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder, codeableConceptBindings)}
|
|
58
58
|
constructor(private resource: ${interfaceName}) {
|
|
59
59
|
${constructorMetaProfile}
|
|
60
60
|
}
|
|
@@ -108,7 +108,7 @@ ${generatePatternMetadata(fieldPatterns, forbiddenFields, valueSetBindings, fiel
|
|
|
108
108
|
// Always include meta.profile with this profile's canonical URL if available & only for resource profiles
|
|
109
109
|
${randomMetaProfileInit}
|
|
110
110
|
// Enrich the base resource with common fields using centralized enrichment logic
|
|
111
|
-
enrichResource(base, '${baseResource || ''}', '${profileUrl || ''}', ${className}._fieldPatterns, ${className}._forbiddenFields, ${className}.valueSetBindings, ${className}._codeResolver, ${className}._fieldOrder);
|
|
111
|
+
enrichResource(base, '${baseResource || ''}', '${profileUrl || ''}', ${className}._fieldPatterns, ${className}._forbiddenFields, ${className}.valueSetBindings, ${className}._codeResolver, ${className}._fieldOrder, ${className}._codeableConceptBindings);
|
|
112
112
|
// Cast through unknown to acknowledge dynamic enrichment
|
|
113
113
|
return { ...(base as unknown as ${interfaceName}), ...overrides };
|
|
114
114
|
}
|
|
@@ -16,6 +16,13 @@ export interface EnrichContext {
|
|
|
16
16
|
codeResolver?: (valueSetUrl: string) => { code: string; system: string; display?: string } | undefined;
|
|
17
17
|
fieldOrder: string[];
|
|
18
18
|
isForbidden: (fieldName: string) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* True when a ValueSet-bound field is declared as CodeableConcept rather than a
|
|
21
|
+
* code primitive. R4 types status as a code, but several R5 product resources
|
|
22
|
+
* type it as CodeableConcept, and writing a bare string there is rejected with
|
|
23
|
+
* "must be an Object, not a Primitive property".
|
|
24
|
+
*/
|
|
25
|
+
isCodeableConceptBinding: (fieldName: string) => boolean;
|
|
19
26
|
resolveBindingCode: (fieldName: string, fallbackSystem?: string, fallbackCode?: string, fallbackDisplay?: string) => { coding: Array<{ system: string; code: string; display?: string }>; text?: string } | undefined;
|
|
20
27
|
resolveBindingCodePrimitive: (fieldName: string, fallbackCode?: string) => string | undefined;
|
|
21
28
|
getPatternValue: (fieldName: string) => unknown | undefined;
|
|
@@ -37,8 +44,11 @@ export function createEnrichContext(
|
|
|
37
44
|
valueSetBindings: Record<string, string>,
|
|
38
45
|
codeResolver: ((valueSetUrl: string) => { code: string; system: string; display?: string } | undefined) | undefined,
|
|
39
46
|
fieldOrder: string[],
|
|
47
|
+
codeableConceptBindings: string[] = [],
|
|
40
48
|
): EnrichContext {
|
|
41
49
|
const isForbidden = (fieldName: string): boolean => forbiddenFields.includes(fieldName);
|
|
50
|
+
const isCodeableConceptBinding = (fieldName: string): boolean =>
|
|
51
|
+
codeableConceptBindings.includes(fieldName);
|
|
42
52
|
|
|
43
53
|
const resolveBindingCode = (fieldName: string, fallbackSystem?: string, fallbackCode?: string, fallbackDisplay?: string) => {
|
|
44
54
|
const bindingUrl = valueSetBindings[fieldName];
|
|
@@ -158,7 +168,7 @@ export function createEnrichContext(
|
|
|
158
168
|
return {
|
|
159
169
|
base, baseResource, profileUrl, fieldPatterns, forbiddenFields,
|
|
160
170
|
valueSetBindings, codeResolver, fieldOrder,
|
|
161
|
-
isForbidden, resolveBindingCode, resolveBindingCodePrimitive,
|
|
171
|
+
isForbidden, isCodeableConceptBinding, resolveBindingCode, resolveBindingCodePrimitive,
|
|
162
172
|
getPatternValue, getRawCodeableConceptPattern, mergeCodingPatterns,
|
|
163
173
|
getCompleteCodePatternValue, getPrimitivePattern, applyNestedPatterns,
|
|
164
174
|
getRefRequirements,
|
|
@@ -500,10 +500,29 @@ export function generateEnrichResourceEnd() {
|
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
-
// Extension value fallback
|
|
503
|
+
// Extension value fallback.
|
|
504
|
+
//
|
|
505
|
+
// Respect the profile's value[x] type constraint: an Extension SD almost always
|
|
506
|
+
// narrows value[x] to one type, and emitting valueString regardless was the
|
|
507
|
+
// largest single source of random()-resource validation failures ("definition
|
|
508
|
+
// allows for the type CodeableConcept but found type String"). Fall back to
|
|
509
|
+
// valueString only when the profile leaves value[x] open or names a type the
|
|
510
|
+
// skeleton factories don't cover.
|
|
504
511
|
if (/Extension/.test(baseResource)) {
|
|
505
512
|
const hasValue = Object.keys(base).some(k => k.startsWith('value'));
|
|
506
|
-
if (!hasValue)
|
|
513
|
+
if (!hasValue) {
|
|
514
|
+
const valueTypes = fieldPatterns?.['_choiceType_value[x]'] as string[] | undefined;
|
|
515
|
+
const constrainedType = valueTypes && valueTypes.length === 1 ? valueTypes[0] : undefined;
|
|
516
|
+
const choice = skeletonChoiceValue('value', constrainedType);
|
|
517
|
+
if (!choice) {
|
|
518
|
+
base['valueString'] = 'value';
|
|
519
|
+
} else if (constrainedType === 'CodeableConcept') {
|
|
520
|
+
// A bound value[x] should carry a code from its ValueSet, not a random one.
|
|
521
|
+
base.valueCodeableConcept = resolveBindingCode('value[x]') ?? choice[1];
|
|
522
|
+
} else {
|
|
523
|
+
base[choice[0]] = choice[1];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
507
526
|
if (!('url' in base)) base.url = profileUrl || 'urn:uuid:extension';
|
|
508
527
|
}
|
|
509
528
|
|
|
@@ -175,8 +175,8 @@ export function generateObservationHandler() {
|
|
|
175
175
|
base.dataAbsentReason = { coding: [{ system: '${TS.DATA_ABSENT}', code: 'unknown' }] };
|
|
176
176
|
} else {
|
|
177
177
|
// Use patterns if available, otherwise derive UCUM unit from observation code
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
const defaultUnit = '1';
|
|
179
|
+
const defaultCode = '1';
|
|
180
180
|
if (unitPattern === undefined && codePattern === undefined) {
|
|
181
181
|
// Map well-known vital sign LOINC codes to correct UCUM units
|
|
182
182
|
const codeObj = base.code as { coding?: Array<{ code?: string }> } | undefined;
|
|
@@ -196,8 +196,8 @@ export function generateObservationHandler() {
|
|
|
196
196
|
};
|
|
197
197
|
if (loincCode && vitalSignUnits[loincCode]) {
|
|
198
198
|
const vs = vitalSignUnits[loincCode];
|
|
199
|
-
defaultUnit
|
|
200
|
-
|
|
199
|
+
// No need to update defaultUnit/defaultCode: this branch uses vs.*
|
|
200
|
+
// directly and only the else branch reads the defaults.
|
|
201
201
|
base.valueQuantity = { value: vs.value, unit: vs.unit, system: '${TS.UCUM}', code: vs.code };
|
|
202
202
|
} else {
|
|
203
203
|
base.valueQuantity = { value: 72, unit: defaultUnit, system: '${TS.UCUM}', code: defaultCode };
|
|
@@ -18,6 +18,9 @@ export type CodeResolver = (valueSetUrl: string) => ResolvedCode | undefined;
|
|
|
18
18
|
* @param forbiddenFields - Array of field names that are forbidden (max=0) in this profile
|
|
19
19
|
* @param valueSetBindings - Map of field names to ValueSet URLs for coded bindings
|
|
20
20
|
* @param codeResolver - Optional function to resolve a ValueSet URL to a random valid code
|
|
21
|
+
* @param fieldOrder - FHIR element ordering for the base resource
|
|
22
|
+
* @param codeableConceptBindings - Bound fields declared as CodeableConcept rather
|
|
23
|
+
* than a code primitive, so a resolved code is emitted as a coding object
|
|
21
24
|
*/
|
|
22
25
|
export function enrichResource(
|
|
23
26
|
base: Record<string, unknown>,
|
|
@@ -27,11 +30,15 @@ export function enrichResource(
|
|
|
27
30
|
forbiddenFields: string[] = [],
|
|
28
31
|
valueSetBindings: Record<string, string> = {},
|
|
29
32
|
codeResolver?: CodeResolver,
|
|
30
|
-
fieldOrder: string[] = []
|
|
33
|
+
fieldOrder: string[] = [],
|
|
34
|
+
codeableConceptBindings: string[] = []
|
|
31
35
|
): void {
|
|
32
36
|
try {
|
|
33
37
|
// Helper to check if a field is forbidden
|
|
34
38
|
const isForbidden = (fieldName: string): boolean => forbiddenFields.includes(fieldName);
|
|
39
|
+
|
|
40
|
+
// Helper to check whether a bound field is a CodeableConcept rather than a code
|
|
41
|
+
const isCodeableConceptBinding = (fieldName: string): boolean => codeableConceptBindings.includes(fieldName);
|
|
35
42
|
|
|
36
43
|
// Helper to resolve a code from ValueSet binding - uses codeResolver if available
|
|
37
44
|
// Returns a CodeableConcept-compatible structure or undefined if not resolvable
|
|
@@ -191,13 +198,25 @@ export function enrichResource(
|
|
|
191
198
|
// Also override generic default 'active' when it's not valid for this resource's ValueSet
|
|
192
199
|
// Skip when status was already set by a fixed pattern above.
|
|
193
200
|
if (statusFixedPattern === undefined && !isForbidden('status')) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
if (isCodeableConceptBinding('status')) {
|
|
202
|
+
// R5 types status as a CodeableConcept on ClinicalUseDefinition,
|
|
203
|
+
// MedicinalProductDefinition, PackagedProductDefinition,
|
|
204
|
+
// RegulatedAuthorization and SubstanceDefinition. Writing the resolved
|
|
205
|
+
// code as a bare string there fails validation with "The property status
|
|
206
|
+
// must be an Object, not a Primitive property", so emit the coding shape.
|
|
207
|
+
const resolvedConcept = resolveBindingCode('status');
|
|
208
|
+
if (resolvedConcept && (!('status' in base) || typeof base.status !== 'object')) {
|
|
209
|
+
base.status = resolvedConcept;
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
const resolved = resolveBindingCodePrimitive('status');
|
|
213
|
+
if (resolved) {
|
|
214
|
+
if (!('status' in base)) {
|
|
215
|
+
base.status = resolved;
|
|
216
|
+
} else if (base.status === 'active' && resolved !== 'active') {
|
|
217
|
+
// Generic 'active' default may not be valid for all resource types (e.g., ChargeItem)
|
|
218
|
+
base.status = resolved;
|
|
219
|
+
}
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
222
|
}
|
|
@@ -373,6 +373,9 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
373
373
|
}))];
|
|
374
374
|
// Collect ValueSet bindings for all fields to expose as static metadata
|
|
375
375
|
const valueSetBindings = {};
|
|
376
|
+
// Record the declared type alongside, so enrichment emits the right JSON shape
|
|
377
|
+
// for a bound field rather than assuming every binding sits on a code primitive.
|
|
378
|
+
const codeableConceptBindingSet = new Set();
|
|
376
379
|
for (const f of candidateRequiredFields) {
|
|
377
380
|
if (f.binding?.uri) {
|
|
378
381
|
// Extract field name from path (e.g., "Observation.status" -> "status")
|
|
@@ -383,9 +386,13 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
383
386
|
if (!valueSetBindings[fieldName]) {
|
|
384
387
|
valueSetBindings[fieldName] = f.binding.uri;
|
|
385
388
|
}
|
|
389
|
+
if ((f.type || f.baseTypeCode) === 'CodeableConcept') {
|
|
390
|
+
codeableConceptBindingSet.add(fieldName);
|
|
391
|
+
}
|
|
386
392
|
}
|
|
387
393
|
}
|
|
388
394
|
}
|
|
395
|
+
const codeableConceptBindings = [...codeableConceptBindingSet];
|
|
389
396
|
// Extract FHIR element ordering from the StructureDefinition snapshot.
|
|
390
397
|
// The snapshot lists elements in the canonical FHIR order defined by the spec.
|
|
391
398
|
// We use this to ensure enrichResource reorders fields correctly, rather than
|
|
@@ -412,5 +419,5 @@ export async function extractFieldPatternsAndMetadata(patternSources, baseResour
|
|
|
412
419
|
fieldOrder.push(fieldName);
|
|
413
420
|
}
|
|
414
421
|
}
|
|
415
|
-
return { fieldPatterns, forbiddenFields, valueSetBindings, fieldOrder };
|
|
422
|
+
return { fieldPatterns, forbiddenFields, valueSetBindings, codeableConceptBindings, fieldOrder };
|
|
416
423
|
}
|