babelfhir-ts 1.3.6 → 1.3.8

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.
Files changed (30) hide show
  1. package/README.md +54 -52
  2. package/out/src/generator/core/cacheConfig.js +35 -11
  3. package/out/src/generator/core/constants.js +12 -0
  4. package/out/src/generator/emitters/class/classTemplate.js +1 -1
  5. package/out/src/generator/emitters/class/enrichContextTemplate.js +168 -0
  6. package/out/src/generator/emitters/class/enrichCtxTransform.js +51 -0
  7. package/out/src/generator/emitters/class/enrichResourceObservationHandler.js +5 -0
  8. package/out/src/generator/emitters/class/randomSupportGenerator.js +135 -3
  9. package/out/src/generator/emitters/class/sliceElementDefaults.js +5 -1
  10. package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +4 -1
  11. package/out/src/generator/emitters/validator/sliceBackboneValidation.js +145 -18
  12. package/out/src/generator/emitters/validator/sliceExtensionValidation.js +174 -7
  13. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +367 -28
  14. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +18 -4
  15. package/out/src/generator/emitters/validator/validatorBindingBuilder.js +8 -2
  16. package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +79 -0
  17. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +77 -8
  18. package/out/src/generator/emitters/validator/validatorGenerator.js +143 -8
  19. package/out/src/generator/emitters/validator/validatorTemplates.js +70 -0
  20. package/out/src/generator/emitters/zod/zodRefinementBuilder.js +47 -18
  21. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +40 -24
  22. package/out/src/generator/generationHelpers.js +63 -3
  23. package/out/src/generator/index.js +100 -37
  24. package/out/src/generator/parser/sdParser.js +8 -2
  25. package/out/src/generator/parser/sliceAggregator.js +5 -1
  26. package/out/src/generator/parser/vsParser.js +54 -3
  27. package/out/src/generator/sdProcessor.js +63 -3
  28. package/out/src/generator/sdProcessorHelpers.js +25 -15
  29. package/out/src/main.js +2 -2
  30. package/package.json +1 -1
package/README.md CHANGED
@@ -61,48 +61,50 @@ Every pull request runs two independent CI pipelines that validate generated cod
61
61
  4. Generates `empty()` and `random()` test resources for every profile
62
62
  5. Validates those resources against two external FHIR validators
63
63
 
64
- ### Tested Implementation Guides (28 packages)
65
-
66
- | Category | Implementation Guide | Package |
67
- |---|---|---|
68
- | US | US Core | `hl7.fhir.us.core@8.0.0` |
69
- | US | QI-Core | `hl7.fhir.us.qicore@6.0.0` |
70
- | US | mCODE | `hl7.fhir.us.mcode@4.0.0` |
71
- | US | SDOH Clinical Care | `hl7.fhir.us.sdoh-clinicalcare@2.2.0` |
72
- | US | NDH (National Directory) | `hl7.fhir.us.ndh@1.0.0` |
73
- | US | CARIN BB | `hl7.fhir.us.carin-bb@2.1.0` |
74
- | US | CQF Measures | `hl7.fhir.us.cqfmeasures@4.0.0` |
75
- | US | Physical Activity | `hl7.fhir.us.physical-activity@1.0.0` |
76
- | DaVinci | PAS | `hl7.fhir.us.davinci-pas@2.0.1` |
77
- | DaVinci | CDex | `hl7.fhir.us.davinci-cdex@2.1.0` |
78
- | DaVinci | PDex | `hl7.fhir.us.davinci-pdex@2.1.0` |
79
- | DaVinci | DTR | `hl7.fhir.us.davinci-dtr@2.1.0` |
80
- | DaVinci | Alerts | `hl7.fhir.us.davinci-alerts@1.0.0` |
81
- | DaVinci | DEQM | `hl7.fhir.us.davinci-deqm@4.0.0` |
82
- | DaVinci | Drug Formulary | `hl7.fhir.us.davinci-drug-formulary@2.1.0` |
83
- | Universal | IPS | `hl7.fhir.uv.ips@2.0.0` |
84
- | Universal | SMART App Launch | `hl7.fhir.uv.smart-app-launch@2.2.0` |
85
- | Universal | SDC (Structured Data Capture) | `hl7.fhir.uv.sdc@3.0.0` |
86
- | Universal | Genomics Reporting | `hl7.fhir.uv.genomics-reporting@3.0.0` |
87
- | Universal | CPG (Clinical Practice Guidelines) | `hl7.fhir.uv.cpg@2.0.0` |
88
- | DE | ISiK Basis | `de.gematik.isik-basismodul@4.0.3` |
89
- | DE | ISiK Medikation | `de.gematik.isik-medikation@4.0.1` |
90
- | DE | KBV eRezept | `kbv.ita.erp@1.1.1` |
91
- | DE | DE Basisprofil | `de.basisprofil.r4@1.5.0` |
92
- | CH | CH Core (Switzerland) | `ch.fhir.ig.ch-core@5.0.0` |
93
- | AU | AU Core (Australia) | `hl7.fhir.au.core@1.0.0` |
94
- | IHE | PIXm | `ihe.iti.pixm@3.0.4` |
95
- | IHE | MHD | `ihe.iti.mhd@4.2.2` |
64
+ ### Tested Implementation Guides (30 packages)
65
+
66
+ | Category | Implementation Guide | Package | FHIR |
67
+ |---|---|---|---|
68
+ | US | US Core | `hl7.fhir.us.core@8.0.0` | R4 |
69
+ | US | QI-Core | `hl7.fhir.us.qicore@6.0.0` | R4 |
70
+ | US | mCODE | `hl7.fhir.us.mcode@4.0.0` | R4 |
71
+ | US | SDOH Clinical Care | `hl7.fhir.us.sdoh-clinicalcare@2.2.0` | R4 |
72
+ | US | NDH (National Directory) | `hl7.fhir.us.ndh@1.0.0` | R4 |
73
+ | US | CARIN BB | `hl7.fhir.us.carin-bb@2.1.0` | R4 |
74
+ | US | CQF Measures | `hl7.fhir.us.cqfmeasures@4.0.0` | R4 |
75
+ | US | Physical Activity | `hl7.fhir.us.physical-activity@1.0.0` | R4 |
76
+ | DaVinci | PAS | `hl7.fhir.us.davinci-pas@2.0.1` | R4 |
77
+ | DaVinci | CDex | `hl7.fhir.us.davinci-cdex@2.1.0` | R4 |
78
+ | DaVinci | PDex | `hl7.fhir.us.davinci-pdex@2.1.0` | R4 |
79
+ | DaVinci | DTR | `hl7.fhir.us.davinci-dtr@2.1.0` | R4 |
80
+ | DaVinci | Alerts | `hl7.fhir.us.davinci-alerts@1.0.0` | R4 |
81
+ | DaVinci | DEQM | `hl7.fhir.us.davinci-deqm@4.0.0` | R4 |
82
+ | DaVinci | Drug Formulary | `hl7.fhir.us.davinci-drug-formulary@2.1.0` | R4 |
83
+ | Universal | IPS | `hl7.fhir.uv.ips@2.0.0` | R4 |
84
+ | Universal | SMART App Launch | `hl7.fhir.uv.smart-app-launch@2.2.0` | R4 |
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 |
96
98
 
97
99
  ### Validation with Firely .NET SDK
98
100
 
99
- The first pipeline validates generated resources using the [Firely .NET SDK validator](https://docs.fire.ly/projects/Firely-NET-SDK/) (Firely.Fhir.Validation.R4 v3.1.1). Results are published as live badges:
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:
100
102
 
101
103
  ![US Core](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-us-core.json)
102
104
  ![QI-Core](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-qicore.json)
103
105
  ![mCODE](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-mcode.json)
104
- ![SDOH](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-sdoh.json)
105
- ![NDH](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-ndh.json)
106
+ ![SDOH Clinical Care](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-sdoh.json)
107
+ ![NDH (National Directory)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-ndh.json)
106
108
  ![CARIN BB](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-carin-bb.json)
107
109
  ![CQF Measures](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-cqfmeasures.json)
108
110
  ![Physical Activity](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-physical-activity.json)
@@ -114,28 +116,28 @@ The first pipeline validates generated resources using the [Firely .NET SDK vali
114
116
  ![DaVinci DEQM](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-davinci-deqm.json)
115
117
  ![DaVinci Drug Formulary](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-davinci-drug-formulary.json)
116
118
  ![IPS](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-ips.json)
117
- ![SMART](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-smart.json)
118
- ![SDC](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-sdc.json)
119
+ ![SMART App Launch](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-smart.json)
120
+ ![SDC (Structured Data Capture)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-sdc.json)
119
121
  ![Genomics Reporting](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-genomics-reporting.json)
120
- ![CPG](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-cpg.json)
122
+ ![CPG (Clinical Practice Guidelines)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-cpg.json)
121
123
  ![ISiK Basis](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-isik-basis.json)
122
- ![ISiK Medikation](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-isik-medikation.json)
123
124
  ![KBV eRezept](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-kbv-erp.json)
124
125
  ![DE Basisprofil](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-de-basisprofil.json)
125
- ![CH Core](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-ch-core.json)
126
- ![AU Core](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-au-core.json)
126
+ ![ISiK Medikation](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-isik-medikation.json)
127
+ ![CH Core (Switzerland)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-ch-core.json)
128
+ ![AU Core (Australia)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-au-core.json)
127
129
  ![PIXm](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-pixm.json)
128
130
  ![MHD](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-mhd.json)
129
131
 
130
132
  ### Validation with HL7 Java Validator
131
133
 
132
- The second pipeline validates using the [official HL7 FHIR Validator](https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator) (v6.9.4), the reference implementation for FHIR conformance checking:
134
+ The second pipeline validates using the [official HL7 FHIR Validator](https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator), the reference implementation for FHIR conformance checking:
133
135
 
134
136
  ![US Core](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-us-core.json)
135
137
  ![QI-Core](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-qicore.json)
136
138
  ![mCODE](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-mcode.json)
137
- ![SDOH](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-sdoh.json)
138
- ![NDH](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-ndh.json)
139
+ ![SDOH Clinical Care](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-sdoh.json)
140
+ ![NDH (National Directory)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-ndh.json)
139
141
  ![CARIN BB](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-carin-bb.json)
140
142
  ![CQF Measures](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-cqfmeasures.json)
141
143
  ![Physical Activity](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-physical-activity.json)
@@ -147,16 +149,16 @@ The second pipeline validates using the [official HL7 FHIR Validator](https://co
147
149
  ![DaVinci DEQM](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-davinci-deqm.json)
148
150
  ![DaVinci Drug Formulary](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-davinci-drug-formulary.json)
149
151
  ![IPS](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-ips.json)
150
- ![SMART](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-smart.json)
151
- ![SDC](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-sdc.json)
152
+ ![SMART App Launch](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-smart.json)
153
+ ![SDC (Structured Data Capture)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-sdc.json)
152
154
  ![Genomics Reporting](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-genomics-reporting.json)
153
- ![CPG](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-cpg.json)
155
+ ![CPG (Clinical Practice Guidelines)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-cpg.json)
154
156
  ![ISiK Basis](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-isik-basis.json)
155
- ![ISiK Medikation](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-isik-medikation.json)
156
157
  ![KBV eRezept](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-kbv-erp.json)
157
158
  ![DE Basisprofil](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-de-basisprofil.json)
158
- ![CH Core](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-ch-core.json)
159
- ![AU Core](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-au-core.json)
159
+ ![ISiK Medikation](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-isik-medikation.json)
160
+ ![CH Core (Switzerland)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-ch-core.json)
161
+ ![AU Core (Australia)](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-au-core.json)
160
162
  ![PIXm](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-pixm.json)
161
163
  ![MHD](https://img.shields.io/endpoint?url=https://max-health-inc.github.io/BabelFHIR-TS/badges/badge-hl7-mhd.json)
162
164
 
@@ -223,8 +225,8 @@ Options:
223
225
  -v, --version Show version number
224
226
  --log <dest> Log destination: console (default) or file
225
227
  --log-level <level> Log verbosity: error, warn, info (default), or debug
226
- --cache-dir <path> Custom cache directory (default: .cache, env: FHIR_CACHE_ROOT)
227
- --no-cache Delete .cache folder after generation
228
+ --cache-dir <path> Custom cache directory (default: ~/.fhir/packages for FHIR packages, .cache for working files)
229
+ --no-cache Delete .cache working folder after generation (does not affect shared ~/.fhir/packages)
228
230
  --no-classes Only generate interfaces and types (skip class generation)
229
231
  --no-client Skip FHIR client generation (client generated by default)
230
232
  --schema <format> Generate schema files alongside outputs (supported: zod)
@@ -4,19 +4,30 @@
4
4
  * This module provides a unified approach to cache management across the project.
5
5
  *
6
6
  * Cache Structure:
7
- * Root cache directory: configurable via environment variable or defaults to .cache
8
- * FHIR packages: cacheRoot/.fhir/packages/packageName@version/
9
- * FHIR resources (StructureDefinitions): cacheRoot/fhir-resources/
10
- * Temporary files: cacheRoot/tmp/
7
+ * FHIR packages: ~/.fhir/packages/ (shared with HL7 Validator, Firely, SUSHI, etc.)
8
+ * Working cache: <cwd>/.cache/ (babelfhir-ts specific artifacts)
9
+ * FHIR resources (StructureDefinitions): .cache/fhir-resources/
10
+ * Terminology expansions: .cache/tx-expansions/
11
+ * Temporary files: .cache/tmp/
11
12
  *
12
13
  * Environment Variables:
13
- * FHIR_CACHE_ROOT: Override the root cache directory (useful for CI)
14
+ * FHIR_CACHE_ROOT: Override both roots (FHIR packages at <root>/.fhir/packages/, working at <root>)
14
15
  * FHIR_CACHE_LOCAL: If set to "true", use package-local cache (in output directory)
15
16
  */
16
17
  import * as path from 'path';
17
18
  import * as fs from 'fs';
19
+ import * as os from 'os';
18
20
  // Module-level state for cache configuration
19
21
  let currentConfig = null;
22
+ /**
23
+ * Get the default FHIR package cache directory per the FHIR Package Cache spec.
24
+ * https://wiki.hl7.org/FHIR_Package_Cache
25
+ * Shared with HL7 Validator, Firely SDK, SUSHI, etc.
26
+ */
27
+ function getDefaultFhirPackagesDir() {
28
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
29
+ return path.join(home, '.fhir', 'packages');
30
+ }
20
31
  /**
21
32
  * Get the default cache root directory based on environment
22
33
  */
@@ -42,21 +53,32 @@ function getDefaultCacheMode() {
42
53
  */
43
54
  export function getCacheConfig() {
44
55
  if (!currentConfig) {
56
+ const rootDir = getDefaultCacheRoot();
45
57
  currentConfig = {
46
- rootDir: getDefaultCacheRoot(),
58
+ rootDir,
47
59
  mode: getDefaultCacheMode(),
60
+ // When FHIR_CACHE_ROOT is set, use it for packages too; otherwise use the standard ~/.fhir/packages
61
+ fhirPackagesDir: process.env.FHIR_CACHE_ROOT
62
+ ? path.join(rootDir, '.fhir', 'packages')
63
+ : getDefaultFhirPackagesDir(),
48
64
  };
49
65
  }
50
66
  return currentConfig;
51
67
  }
52
68
  /**
53
- * Override the cache configuration (useful for testing or CLI options)
69
+ * Override the cache configuration (useful for testing or CLI options).
70
+ * When rootDir is changed, fhirPackagesDir is also updated to <rootDir>/.fhir/packages/
71
+ * unless fhirPackagesDir is explicitly provided.
54
72
  */
55
73
  export function setCacheConfig(config) {
56
74
  const current = getCacheConfig();
75
+ const newRootDir = config.rootDir ?? current.rootDir;
57
76
  currentConfig = {
58
77
  ...current,
59
78
  ...config,
79
+ rootDir: newRootDir,
80
+ // If rootDir changed but fhirPackagesDir not explicitly set, derive from new rootDir
81
+ fhirPackagesDir: config.fhirPackagesDir ?? (config.rootDir ? path.join(newRootDir, '.fhir', 'packages') : current.fhirPackagesDir),
60
82
  };
61
83
  }
62
84
  /**
@@ -66,12 +88,13 @@ export function resetCacheConfig() {
66
88
  currentConfig = null;
67
89
  }
68
90
  /**
69
- * Get the FHIR packages cache directory
70
- * This is where downloaded FHIR packages (tgz files) are stored
91
+ * Get the FHIR packages cache directory.
92
+ * Defaults to ~/.fhir/packages/ (standard FHIR Package Cache shared across tools).
93
+ * When FHIR_CACHE_ROOT or --cache-dir is set, uses <root>/.fhir/packages/ instead.
71
94
  */
72
95
  export function getFhirPackagesCacheDir() {
73
96
  const config = getCacheConfig();
74
- return path.join(config.rootDir, '.fhir', 'packages');
97
+ return config.fhirPackagesDir;
75
98
  }
76
99
  /**
77
100
  * Get the FHIR resources cache directory
@@ -141,7 +164,8 @@ export function clearCacheDir(dir) {
141
164
  }
142
165
  }
143
166
  /**
144
- * Clear all caches
167
+ * Clear all babelfhir-ts working caches (fhir-resources, tmp, tx-expansions).
168
+ * Does NOT delete the shared FHIR package cache (~/.fhir/packages/).
145
169
  */
146
170
  export function clearAllCaches() {
147
171
  const config = getCacheConfig();
@@ -191,6 +191,8 @@ export const VALUESET_DEFAULTS = {
191
191
  'http://hl7.org/fhir/ValueSet/request-resource-types': { code: 'Task' },
192
192
  'http://hl7.org/fhir/ValueSet/resource-types': { code: 'Patient' },
193
193
  'http://hl7.org/fhir/ValueSet/FHIR-version': { code: '4.0.1' },
194
+ // ── MIME type ValueSet (code type, required binding) ──────────────────────
195
+ 'http://hl7.org/fhir/ValueSet/mimetypes': { code: 'application/fhir+json' },
194
196
  // ── Claim/EoB ValueSets (CARIN BB) ────────────────────────────────────────
195
197
  'http://hl7.org/fhir/ValueSet/claim-type': { system: 'http://terminology.hl7.org/CodeSystem/claim-type', code: 'institutional' },
196
198
  'http://hl7.org/fhir/ValueSet/process-priority': { code: 'normal' },
@@ -336,6 +338,10 @@ const consent = (mp) => `{ resourceType: 'Consent', id: randomId()${mp}, status:
336
338
  const list = (mp) => `{ resourceType: 'List', id: randomId()${mp}, status: 'current', mode: 'working', code: { coding: [{ system: '${TS.LOINC}', code: '57024-2' }] }, subject: { reference: 'Patient/' + randomId() }, date: new Date().toISOString() }`;
337
339
  const documentReference = (mp) => `{ resourceType: 'DocumentReference', id: randomId()${mp}, status: 'current', content: [{ attachment: { contentType: 'text/plain', url: 'https://babelfhir.dev/' + randomId() } }] }`;
338
340
  const binary = (mp) => `{ resourceType: 'Binary', id: randomId()${mp}, contentType: 'application/pdf', data: '${DEFAULT_BASE64}' }`;
341
+ const questionnaire = (mp) => `{ resourceType: 'Questionnaire', id: randomId()${mp}, status: 'active', url: 'https://babelfhir.dev/' + randomId() }`;
342
+ const questionnaireResponse = (mp) => `{ resourceType: 'QuestionnaireResponse', id: randomId()${mp}, status: 'completed' }`;
343
+ const operationOutcome = (mp) => `{ resourceType: 'OperationOutcome', id: randomId()${mp}, issue: [{ severity: 'information', code: 'informational', diagnostics: 'Placeholder' }] }`;
344
+ const bundle = (mp) => `{ resourceType: 'Bundle', id: randomId()${mp}, type: 'collection' }`;
339
345
  export const RESOURCE_SKELETONS = {
340
346
  Patient: patient,
341
347
  Practitioner: practitioner,
@@ -355,6 +361,10 @@ export const RESOURCE_SKELETONS = {
355
361
  List: list,
356
362
  DocumentReference: documentReference,
357
363
  Binary: binary,
364
+ Questionnaire: questionnaire,
365
+ QuestionnaireResponse: questionnaireResponse,
366
+ OperationOutcome: operationOutcome,
367
+ Bundle: bundle,
358
368
  };
359
369
  /**
360
370
  * Build a Bundle entry object expression from a skeleton.
@@ -389,6 +399,8 @@ export function matchSkeletonBySliceName(sliceName) {
389
399
  ['device', 'Device'],
390
400
  ['claimresponse', 'ClaimResponse'],
391
401
  ['claim', 'Claim'],
402
+ ['questionnaire', 'Questionnaire'],
403
+ ['operationoutcome', 'OperationOutcome'],
392
404
  ];
393
405
  // practitioner must not match practitionerrole
394
406
  if (lower.includes('practitioner') && !lower.includes('role'))
@@ -29,7 +29,7 @@ export function renderClassTemplate(params) {
29
29
  import { validate${interfaceName}, ${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
- import { ${helperImportList} } from "./RandomSupport.js";
32
+ import { ${helperImportList} } from "./random/index.js";
33
33
  ${hasVsBindings ? `import { createRequire } from 'module';
34
34
  const __require = createRequire(import.meta.url);` : ''}
35
35
 
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Generates the EnrichContext type definition and context factory.
3
+ * Emitted as random/enrichContext.ts — shared by enrichResource, handlers, and fixers.
4
+ */
5
+ export function generateEnrichContext() {
6
+ return `import { randomUUID } from './randomUtilities.js';
7
+
8
+ /** Shared context passed to all enrich resource handlers and fixers. */
9
+ export interface EnrichContext {
10
+ base: Record<string, unknown>;
11
+ baseResource: string;
12
+ profileUrl: string;
13
+ fieldPatterns: Record<string, unknown>;
14
+ forbiddenFields: string[];
15
+ valueSetBindings: Record<string, string>;
16
+ codeResolver?: (valueSetUrl: string) => { code: string; system: string; display?: string } | undefined;
17
+ fieldOrder: string[];
18
+ isForbidden: (fieldName: string) => boolean;
19
+ resolveBindingCode: (fieldName: string, fallbackSystem?: string, fallbackCode?: string, fallbackDisplay?: string) => { coding: Array<{ system: string; code: string; display?: string }>; text?: string } | undefined;
20
+ resolveBindingCodePrimitive: (fieldName: string, fallbackCode?: string) => string | undefined;
21
+ getPatternValue: (fieldName: string) => unknown | undefined;
22
+ getRawCodeableConceptPattern: (fieldName: string) => { coding?: Array<Record<string, unknown>> } | undefined;
23
+ mergeCodingPatterns: (existingCodings: Array<Record<string, unknown>>, patternCodings: Array<Record<string, unknown>>) => void;
24
+ getCompleteCodePatternValue: (fieldName: string) => unknown | undefined;
25
+ getPrimitivePattern: (nestedPath: string) => string | number | boolean | undefined;
26
+ applyNestedPatterns: (targetField: string, target: Record<string, unknown>) => void;
27
+ getRefRequirements: (fieldName: string) => Record<string, boolean> | undefined;
28
+ }
29
+
30
+ /** Build an EnrichContext from the enrichResource parameters. */
31
+ export function createEnrichContext(
32
+ base: Record<string, unknown>,
33
+ baseResource: string,
34
+ profileUrl: string,
35
+ fieldPatterns: Record<string, unknown>,
36
+ forbiddenFields: string[],
37
+ valueSetBindings: Record<string, string>,
38
+ codeResolver: ((valueSetUrl: string) => { code: string; system: string; display?: string } | undefined) | undefined,
39
+ fieldOrder: string[],
40
+ ): EnrichContext {
41
+ const isForbidden = (fieldName: string): boolean => forbiddenFields.includes(fieldName);
42
+
43
+ const resolveBindingCode = (fieldName: string, fallbackSystem?: string, fallbackCode?: string, fallbackDisplay?: string) => {
44
+ const bindingUrl = valueSetBindings[fieldName];
45
+ if (bindingUrl && codeResolver) {
46
+ const resolved = codeResolver(bindingUrl);
47
+ if (resolved) {
48
+ return {
49
+ coding: [{ system: resolved.system, code: resolved.code, display: resolved.display }],
50
+ text: resolved.display || resolved.code
51
+ };
52
+ }
53
+ }
54
+ if (fallbackSystem && fallbackCode) {
55
+ return {
56
+ coding: [{ system: fallbackSystem, code: fallbackCode, display: fallbackDisplay }],
57
+ text: fallbackDisplay || fallbackCode
58
+ };
59
+ }
60
+ return undefined;
61
+ };
62
+
63
+ const resolveBindingCodePrimitive = (fieldName: string, fallbackCode?: string): string | undefined => {
64
+ const bindingUrl = valueSetBindings[fieldName];
65
+ if (bindingUrl && codeResolver) {
66
+ const resolved = codeResolver(bindingUrl);
67
+ if (resolved) return resolved.code;
68
+ }
69
+ return fallbackCode;
70
+ };
71
+
72
+ const getPatternValue = (fieldName: string): unknown | undefined => {
73
+ if (!fieldPatterns || Object.keys(fieldPatterns).length === 0 || !(fieldName in fieldPatterns)) return undefined;
74
+ const pattern = fieldPatterns[fieldName];
75
+ if (!pattern || typeof pattern !== 'object') return undefined;
76
+ const obj = pattern as Record<string, unknown>;
77
+ if ('coding' in obj && Array.isArray(obj.coding) && obj.coding.length > 0) {
78
+ const complete = (obj.coding as Array<Record<string, unknown>>)
79
+ .filter(c => c.system && c.code)
80
+ .map(c => ({ ...c }));
81
+ return complete.length > 0 ? { coding: complete } : undefined;
82
+ }
83
+ return undefined;
84
+ };
85
+
86
+ const getRawCodeableConceptPattern = (fieldName: string): { coding?: Array<Record<string, unknown>> } | undefined => {
87
+ if (!fieldPatterns || Object.keys(fieldPatterns).length === 0 || !(fieldName in fieldPatterns)) return undefined;
88
+ const pattern = fieldPatterns[fieldName];
89
+ if (!pattern || typeof pattern !== 'object') return undefined;
90
+ const obj = pattern as Record<string, unknown>;
91
+ if ('coding' in obj && Array.isArray(obj.coding) && obj.coding.length > 0) {
92
+ return { coding: obj.coding as Array<Record<string, unknown>> };
93
+ }
94
+ return undefined;
95
+ };
96
+
97
+ const mergeCodingPatterns = (existingCodings: Array<Record<string, unknown>>, patternCodings: Array<Record<string, unknown>>) => {
98
+ const patternBySystem = new Map<unknown, Record<string, unknown>>();
99
+ for (const patternCoding of patternCodings) {
100
+ patternBySystem.set(patternCoding.system, patternCoding);
101
+ }
102
+ for (const existing of existingCodings) {
103
+ const patternCoding = patternBySystem.get(existing.system);
104
+ if (patternCoding) {
105
+ for (const [key, value] of Object.entries(patternCoding)) {
106
+ if (key !== 'system' && !(key in existing)) {
107
+ existing[key] = value;
108
+ }
109
+ }
110
+ }
111
+ }
112
+ };
113
+
114
+ const getCompleteCodePatternValue = (fieldName: string): unknown | undefined => {
115
+ if (!fieldPatterns || Object.keys(fieldPatterns).length === 0 || !(fieldName in fieldPatterns)) return undefined;
116
+ const pattern = fieldPatterns[fieldName];
117
+ if (!pattern || typeof pattern !== 'object') return undefined;
118
+ const obj = pattern as Record<string, unknown>;
119
+ if ('coding' in obj && Array.isArray(obj.coding) && obj.coding.length > 0) {
120
+ const firstCoding = obj.coding[0] as Record<string, unknown>;
121
+ if (firstCoding && 'system' in firstCoding && 'code' in firstCoding) {
122
+ return { coding: obj.coding };
123
+ }
124
+ }
125
+ return undefined;
126
+ };
127
+
128
+ const getPrimitivePattern = (nestedPath: string): string | number | boolean | undefined => {
129
+ if (!fieldPatterns || !(nestedPath in fieldPatterns)) return undefined;
130
+ const value = fieldPatterns[nestedPath];
131
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
132
+ return undefined;
133
+ };
134
+
135
+ const applyNestedPatterns = (targetField: string, target: Record<string, unknown>): void => {
136
+ if (!fieldPatterns) return;
137
+ for (const key of Object.keys(fieldPatterns)) {
138
+ if (key.startsWith(targetField + '.')) {
139
+ const nestedKey = key.slice(targetField.length + 1);
140
+ const patternValue = fieldPatterns[key];
141
+ if (typeof patternValue === 'string' || typeof patternValue === 'number' || typeof patternValue === 'boolean') {
142
+ target[nestedKey] = patternValue;
143
+ }
144
+ }
145
+ }
146
+ };
147
+
148
+ const getRefRequirements = (fieldName: string): Record<string, boolean> | undefined => {
149
+ if (!fieldPatterns) return undefined;
150
+ const reqKey = '_refReq_' + fieldName;
151
+ const reqs = fieldPatterns[reqKey];
152
+ if (reqs && typeof reqs === 'object' && !Array.isArray(reqs)) {
153
+ return reqs as Record<string, boolean>;
154
+ }
155
+ return undefined;
156
+ };
157
+
158
+ return {
159
+ base, baseResource, profileUrl, fieldPatterns, forbiddenFields,
160
+ valueSetBindings, codeResolver, fieldOrder,
161
+ isForbidden, resolveBindingCode, resolveBindingCodePrimitive,
162
+ getPatternValue, getRawCodeableConceptPattern, mergeCodingPatterns,
163
+ getCompleteCodePatternValue, getPrimitivePattern, applyNestedPatterns,
164
+ getRefRequirements,
165
+ };
166
+ }
167
+ `;
168
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Transforms raw enrichResource template output to use EnrichContext (ctx) parameter.
3
+ * Replaces bare references to closure variables/functions with ctx.* equivalents.
4
+ */
5
+ const CTX_FUNCTIONS = [
6
+ 'isForbidden',
7
+ 'resolveBindingCode',
8
+ 'resolveBindingCodePrimitive',
9
+ 'getPatternValue',
10
+ 'getRawCodeableConceptPattern',
11
+ 'mergeCodingPatterns',
12
+ 'getCompleteCodePatternValue',
13
+ 'getPrimitivePattern',
14
+ 'applyNestedPatterns',
15
+ 'getRefRequirements',
16
+ ];
17
+ const CTX_VARS = [
18
+ 'baseResource',
19
+ 'profileUrl',
20
+ 'fieldPatterns',
21
+ 'forbiddenFields',
22
+ 'valueSetBindings',
23
+ 'codeResolver',
24
+ 'fieldOrder',
25
+ ];
26
+ /**
27
+ * Transform a raw enrichResource handler/fixer code block so that all
28
+ * bare references to closure identifiers become ctx.* accesses.
29
+ *
30
+ * Rules:
31
+ * - Function calls: `isForbidden(` → `ctx.isForbidden(`
32
+ * - Variable reads: standalone `baseResource` → `ctx.baseResource`
33
+ * - `base.` / `base[` / ` base ` → `ctx.base.` / `ctx.base[` etc.
34
+ * - Does NOT touch occurrences inside string literals or comments
35
+ */
36
+ export function ctxify(code) {
37
+ let result = code;
38
+ // 1. Replace function calls — word-boundary before, '(' after
39
+ for (const fn of CTX_FUNCTIONS) {
40
+ result = result.replace(new RegExp(`(?<!\\.)\\b${fn}\\(`, 'g'), `ctx.${fn}(`);
41
+ }
42
+ // 2. Replace variable identifiers — word-boundary both sides, not already prefixed
43
+ for (const v of CTX_VARS) {
44
+ result = result.replace(new RegExp(`(?<!\\.)\\b${v}\\b`, 'g'), `ctx.${v}`);
45
+ }
46
+ // 3. Replace `base` the parameter — must not be inside a compound word or already prefixed
47
+ // Match: standalone `base` followed by `.`, `[`, `)`, `,`, `;`, whitespace, or end-of-line
48
+ // Negative lookbehind: not preceded by `.` or a word char (e.g., "database")
49
+ result = result.replace(/(?<![.\w])base(?=[.[\]);,\s}])/g, 'ctx.base');
50
+ return result;
51
+ }
@@ -36,6 +36,11 @@ export function generateObservationHandler() {
36
36
  // (from resolveSliceCode random selection). If so, replace it with the required pattern code.
37
37
  const sameSystemIdx = patternCoding.code ? existingCode.coding.findIndex(c => c.system === patternCoding.system && c.code !== patternCoding.code) : -1;
38
38
  if (sameSystemIdx >= 0) {
39
+ // Remove stale display from the random coding when the pattern
40
+ // overrides the code but doesn't specify its own display.
41
+ if (!('display' in patternCoding)) {
42
+ delete existingCode.coding[sameSystemIdx].display;
43
+ }
39
44
  existingCode.coding[sameSystemIdx] = { ...existingCode.coding[sameSystemIdx], ...patternCoding };
40
45
  } else {
41
46
  existingCode.coding.push(patternCoding);