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.
- package/README.md +54 -52
- package/out/src/generator/core/cacheConfig.js +35 -11
- package/out/src/generator/core/constants.js +12 -0
- package/out/src/generator/emitters/class/classTemplate.js +1 -1
- package/out/src/generator/emitters/class/enrichContextTemplate.js +168 -0
- package/out/src/generator/emitters/class/enrichCtxTransform.js +51 -0
- package/out/src/generator/emitters/class/enrichResourceObservationHandler.js +5 -0
- package/out/src/generator/emitters/class/randomSupportGenerator.js +135 -3
- package/out/src/generator/emitters/class/sliceElementDefaults.js +5 -1
- package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +4 -1
- package/out/src/generator/emitters/validator/sliceBackboneValidation.js +145 -18
- package/out/src/generator/emitters/validator/sliceExtensionValidation.js +174 -7
- package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +367 -28
- package/out/src/generator/emitters/validator/sliceValidatorUtils.js +18 -4
- package/out/src/generator/emitters/validator/validatorBindingBuilder.js +8 -2
- package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +79 -0
- package/out/src/generator/emitters/validator/validatorFieldBuilders.js +77 -8
- package/out/src/generator/emitters/validator/validatorGenerator.js +143 -8
- package/out/src/generator/emitters/validator/validatorTemplates.js +70 -0
- package/out/src/generator/emitters/zod/zodRefinementBuilder.js +47 -18
- package/out/src/generator/emitters/zod/zodSchemaGenerator.js +40 -24
- package/out/src/generator/generationHelpers.js +63 -3
- package/out/src/generator/index.js +100 -37
- package/out/src/generator/parser/sdParser.js +8 -2
- package/out/src/generator/parser/sliceAggregator.js +5 -1
- package/out/src/generator/parser/vsParser.js +54 -3
- package/out/src/generator/sdProcessor.js +63 -3
- package/out/src/generator/sdProcessorHelpers.js +25 -15
- package/out/src/main.js +2 -2
- 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 (
|
|
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 |
|
|
90
|
-
| DE |
|
|
91
|
-
| DE |
|
|
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/)
|
|
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
|

|
|
102
104
|

|
|
103
105
|

|
|
104
|
-

|
|
105
|
-

|
|
106
|
+

|
|
107
|
+

|
|
106
108
|

|
|
107
109
|

|
|
108
110
|

|
|
@@ -114,28 +116,28 @@ The first pipeline validates generated resources using the [Firely .NET SDK vali
|
|
|
114
116
|

|
|
115
117
|

|
|
116
118
|

|
|
117
|
-

|
|
118
|
-

|
|
119
|
+

|
|
120
|
+

|
|
119
121
|

|
|
120
|
-

|
|
122
|
+

|
|
121
123
|

|
|
122
|
-

|
|
123
124
|

|
|
124
125
|

|
|
125
|
-

|
|
127
|
+

|
|
128
|
+

|
|
127
129
|

|
|
128
130
|

|
|
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)
|
|
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
|

|
|
135
137
|

|
|
136
138
|

|
|
137
|
-

|
|
138
|
-

|
|
139
|
+

|
|
140
|
+

|
|
139
141
|

|
|
140
142
|

|
|
141
143
|

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

|
|
148
150
|

|
|
149
151
|

|
|
150
|
-

|
|
151
|
-

|
|
152
|
+

|
|
153
|
+

|
|
152
154
|

|
|
153
|
-

|
|
155
|
+

|
|
154
156
|

|
|
155
|
-

|
|
156
157
|

|
|
157
158
|

|
|
158
|
-

|
|
160
|
+

|
|
161
|
+

|
|
160
162
|

|
|
161
163
|

|
|
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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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 "./
|
|
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);
|