babelfhir-ts 1.0.26 → 1.0.28
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/LICENSE +15 -0
- package/README.md +21 -1
- package/out/generator/importManager.js +252 -0
- package/out/generator/index.js +2 -1
- package/out/generator/interfaceGenerator.js +81 -188
- package/out/generator/sdParser.js +14 -1
- package/out/generator/utils.js +6 -2
- package/out/generator/validatorGenerator.js +41 -6
- package/out/generator/valueSetGenerator.js +1 -1
- package/package.json +22 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Maximilian Nussbaumer
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# BabelFHIR-TS
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/babelfhir-ts)
|
|
4
|
+
[](https://opensource.org/licenses/ISC)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
3
8
|
**BabelFHIR-TS** transforms FHIR® StructureDefinitions into production-ready TypeScript code with full type safety and built-in validation. Unlike generic FHIR type definitions, BabelFHIR-TS generates **profile-aware** interfaces that understand your Implementation Guide's constraints, extensions, and slicing rules.
|
|
4
9
|
|
|
5
10
|
### What you get
|
|
@@ -49,7 +54,7 @@ Download and process a package directly from a registry (defaults to `https://pa
|
|
|
49
54
|
babelfhir-ts --package hl7.fhir.us.core@8.0.0
|
|
50
55
|
```
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
Download, pocess and install a processed package into your current project:
|
|
53
58
|
|
|
54
59
|
```bash
|
|
55
60
|
babelfhir-ts install hl7.fhir.us.core@8.0.0
|
|
@@ -188,3 +193,18 @@ We continuously improve the generator based on real-world IG usage, and your fee
|
|
|
188
193
|
## License
|
|
189
194
|
|
|
190
195
|
ISC © Maximilian Nussbaumer
|
|
196
|
+
|
|
197
|
+
## Contributing
|
|
198
|
+
|
|
199
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute to this project.
|
|
200
|
+
|
|
201
|
+
## Security
|
|
202
|
+
|
|
203
|
+
For security issues, please see [SECURITY.md](SECURITY.md) for our security policy and how to report vulnerabilities.
|
|
204
|
+
|
|
205
|
+
## Links
|
|
206
|
+
|
|
207
|
+
- [npm package](https://www.npmjs.com/package/babelfhir-ts)
|
|
208
|
+
- [GitHub repository](https://github.com/quotentiroler/BabelFHIR-ts)
|
|
209
|
+
- [Issue tracker](https://github.com/quotentiroler/BabelFHIR-ts/issues)
|
|
210
|
+
- [Changelog](CHANGELOG.md)
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import fhirInterfaceNames from "./fhirInterfaces.json" with { type: 'json' };
|
|
2
|
+
import { sanitizeIdentifier } from './utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Manages TypeScript import statements for generated FHIR interfaces.
|
|
5
|
+
* Handles three types of imports:
|
|
6
|
+
* 1. FHIR base types from '@types/fhir' (via 'fhir/r4')
|
|
7
|
+
* 2. Custom local types (other generated profiles in the same package)
|
|
8
|
+
* 3. External profiled types (to be fetched/generated separately)
|
|
9
|
+
* 4. Path-based imports (custom relative paths, e.g., ValueSets in subdirectories)
|
|
10
|
+
*/
|
|
11
|
+
export class ImportManager {
|
|
12
|
+
fhirImports = new Set();
|
|
13
|
+
customImports = new Set();
|
|
14
|
+
pathBasedImports = new Map(); // typeName -> relativePath
|
|
15
|
+
localInterfaceNames;
|
|
16
|
+
existingProfiles;
|
|
17
|
+
externalProfiles = new Map();
|
|
18
|
+
constructor(localInterfaceNames, existingProfiles) {
|
|
19
|
+
this.localInterfaceNames = localInterfaceNames;
|
|
20
|
+
this.existingProfiles = existingProfiles;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Adds a type to the appropriate import category.
|
|
24
|
+
*
|
|
25
|
+
* @param typeName - The TypeScript type name to import
|
|
26
|
+
* @param profileUrl - Optional profile URL for external profiles
|
|
27
|
+
* @param baseType - Optional base type for external profiles
|
|
28
|
+
* @param isFhirBase - Whether the base type is a FHIR type
|
|
29
|
+
*/
|
|
30
|
+
addType(typeName, profileUrl, baseType, isFhirBase = false) {
|
|
31
|
+
if (!typeName)
|
|
32
|
+
return;
|
|
33
|
+
// Check if it's a nested interface of the current profile (defined locally in this file)
|
|
34
|
+
if (this.localInterfaceNames.has(typeName)) {
|
|
35
|
+
// Don't add to imports - defined in the same file
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Check if it's a known FHIR type from @types/fhir
|
|
39
|
+
if (this.isFhirType(typeName)) {
|
|
40
|
+
this.fhirImports.add(typeName);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Check if it exists in our generated profiles/types
|
|
44
|
+
if (this.existingProfiles?.has(typeName)) {
|
|
45
|
+
this.customImports.add(typeName);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Check if this is a primitive type that shouldn't be fetched
|
|
49
|
+
if (this.isPrimitiveLikeType(typeName)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Add to custom imports (will be generated or fetched)
|
|
53
|
+
this.customImports.add(typeName);
|
|
54
|
+
// Track as external profile if we have profile metadata
|
|
55
|
+
if (profileUrl || baseType) {
|
|
56
|
+
this.registerExternalProfile(typeName, profileUrl, baseType, isFhirBase);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Registers an external profile that needs to be fetched or generated.
|
|
61
|
+
*/
|
|
62
|
+
registerExternalProfile(rawTypeName, profileUrl, baseType, isFhirBase = false) {
|
|
63
|
+
if (!rawTypeName)
|
|
64
|
+
return;
|
|
65
|
+
const sanitizedTypeName = sanitizeIdentifier(rawTypeName);
|
|
66
|
+
const resolvedProfileUrl = profileUrl && profileUrl.length > 0
|
|
67
|
+
? profileUrl
|
|
68
|
+
: `http://hl7.org/fhir/StructureDefinition/${sanitizedTypeName}`;
|
|
69
|
+
let meta = this.externalProfiles.get(sanitizedTypeName);
|
|
70
|
+
if (!meta) {
|
|
71
|
+
meta = { profileUrl: resolvedProfileUrl, baseTypes: new Map() };
|
|
72
|
+
this.externalProfiles.set(sanitizedTypeName, meta);
|
|
73
|
+
}
|
|
74
|
+
else if (profileUrl && profileUrl.length > 0 &&
|
|
75
|
+
meta.profileUrl.startsWith('http://hl7.org/fhir/StructureDefinition/')) {
|
|
76
|
+
// Update with more specific profile URL
|
|
77
|
+
meta.profileUrl = profileUrl;
|
|
78
|
+
}
|
|
79
|
+
// Track base type relationship
|
|
80
|
+
if (baseType && baseType !== 'any') {
|
|
81
|
+
const sanitizedBase = sanitizeIdentifier(baseType);
|
|
82
|
+
const existing = meta.baseTypes.get(sanitizedBase);
|
|
83
|
+
if (!existing) {
|
|
84
|
+
meta.baseTypes.set(sanitizedBase, { isFhir: isFhirBase });
|
|
85
|
+
}
|
|
86
|
+
else if (isFhirBase && !existing.isFhir) {
|
|
87
|
+
existing.isFhir = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Gets all registered external profile references.
|
|
93
|
+
*/
|
|
94
|
+
getExternalProfiles() {
|
|
95
|
+
return this.externalProfiles;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Generates import statement lines for FHIR and custom types.
|
|
99
|
+
*/
|
|
100
|
+
generateImportStatements() {
|
|
101
|
+
const statements = [];
|
|
102
|
+
// FHIR imports from 'fhir/r4'
|
|
103
|
+
if (this.fhirImports.size > 0) {
|
|
104
|
+
const sortedFhirImports = Array.from(this.fhirImports).sort();
|
|
105
|
+
statements.push(`import { ${sortedFhirImports.join(', ')} } from "fhir/r4";`);
|
|
106
|
+
}
|
|
107
|
+
// Path-based imports (e.g., ValueSets from subdirectories)
|
|
108
|
+
const sortedPathBasedImports = Array.from(this.pathBasedImports.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
109
|
+
for (const [typeName, relativePath] of sortedPathBasedImports) {
|
|
110
|
+
statements.push(`import { ${typeName} } from "${relativePath}";`);
|
|
111
|
+
}
|
|
112
|
+
// Custom local imports
|
|
113
|
+
const sortedCustomImports = Array.from(this.customImports).sort();
|
|
114
|
+
for (const type of sortedCustomImports) {
|
|
115
|
+
statements.push(`import { ${type} } from "./${type}";`);
|
|
116
|
+
}
|
|
117
|
+
return statements.join('\n');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Prunes unused imports from generated code.
|
|
121
|
+
* Removes import statements for types that aren't actually used.
|
|
122
|
+
*/
|
|
123
|
+
pruneUnusedImports(output) {
|
|
124
|
+
// Prune unused FHIR imports
|
|
125
|
+
output = output.replace(/import \{([^}]+)\} from "fhir\/r4";?/g, (full, inside) => {
|
|
126
|
+
const used = [];
|
|
127
|
+
const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
|
|
128
|
+
for (const name of specifiers) {
|
|
129
|
+
const re = new RegExp(`\\b${name}\\b`);
|
|
130
|
+
if (re.test(output.replace(full, ''))) {
|
|
131
|
+
used.push(name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return used.length ? `import { ${used.join(', ')} } from "fhir/r4";` : '';
|
|
135
|
+
});
|
|
136
|
+
// Prune unused custom imports (both path-based and standard custom imports)
|
|
137
|
+
output = output.replace(/import \{([^}]+)\} from "\.\/(.+?)";?/g, (full, inside, path) => {
|
|
138
|
+
const used = [];
|
|
139
|
+
const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
|
|
140
|
+
for (const name of specifiers) {
|
|
141
|
+
const re = new RegExp(`\\b${name}\\b`);
|
|
142
|
+
if (re.test(output.replace(full, ''))) {
|
|
143
|
+
used.push(name);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return used.length ? `import { ${used.join(', ')} } from "./${path}";` : '';
|
|
147
|
+
});
|
|
148
|
+
// Remove now-empty import lines and collapse multiple blank lines
|
|
149
|
+
output = output.replace(/^\s*import \{\s*\}.*$/mg, '').replace(/\n{3,}/g, '\n\n');
|
|
150
|
+
return output;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Reorganizes imports to prefer local types over FHIR types where appropriate.
|
|
154
|
+
* Moves locally-generated types from fhir/r4 imports to local imports.
|
|
155
|
+
*/
|
|
156
|
+
reorganizeImports(output) {
|
|
157
|
+
if (!this.existingProfiles || this.existingProfiles.size === 0) {
|
|
158
|
+
return output;
|
|
159
|
+
}
|
|
160
|
+
// Extract ALL types from fhir/r4 imports (there may be multiple import lines)
|
|
161
|
+
const fhirImportRegex = /import \{([^}]+)\} from "fhir\/r4";/g;
|
|
162
|
+
const allFhirTypes = [];
|
|
163
|
+
let match;
|
|
164
|
+
while ((match = fhirImportRegex.exec(output)) !== null) {
|
|
165
|
+
const types = match[1].split(',').map(t => t.trim()).filter(Boolean);
|
|
166
|
+
allFhirTypes.push(...types);
|
|
167
|
+
}
|
|
168
|
+
if (allFhirTypes.length === 0) {
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
const localTypes = [];
|
|
172
|
+
const remainingFhirTypes = [];
|
|
173
|
+
for (const typeName of allFhirTypes) {
|
|
174
|
+
if (this.existingProfiles.has(typeName)) {
|
|
175
|
+
// Never move FhirResource to local imports; it must stay from fhir/r4
|
|
176
|
+
if (typeName !== 'FhirResource') {
|
|
177
|
+
localTypes.push(typeName);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
remainingFhirTypes.push(typeName);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
remainingFhirTypes.push(typeName);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Remove ALL old fhir/r4 import lines
|
|
188
|
+
output = output.replace(/import \{[^}]+\} from "fhir\/r4";\n?/g, '');
|
|
189
|
+
// Add back the imports at the top
|
|
190
|
+
let newImports = '';
|
|
191
|
+
if (remainingFhirTypes.length > 0) {
|
|
192
|
+
newImports += `import { ${remainingFhirTypes.join(', ')} } from "fhir/r4";\n`;
|
|
193
|
+
}
|
|
194
|
+
for (const localType of localTypes) {
|
|
195
|
+
newImports += `import { ${localType} } from "./${localType}";\n`;
|
|
196
|
+
}
|
|
197
|
+
// Insert new imports after any existing custom imports or at the top
|
|
198
|
+
if (newImports) {
|
|
199
|
+
const customImportMatch = output.match(/^(import \{[^}]+\} from "\.\/[^"]+";?\n)+/m);
|
|
200
|
+
if (customImportMatch) {
|
|
201
|
+
// Insert after existing custom imports
|
|
202
|
+
output = output.replace(customImportMatch[0], customImportMatch[0] + newImports);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// Insert at the very top
|
|
206
|
+
output = newImports + output;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return output;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Checks if a type is a known FHIR type from @types/fhir.
|
|
213
|
+
*/
|
|
214
|
+
isFhirType(type) {
|
|
215
|
+
return type === 'FhirResource' || fhirInterfaceNames.includes(type);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Checks if a type name looks like a primitive that shouldn't be imported.
|
|
219
|
+
*/
|
|
220
|
+
isPrimitiveLikeType(typeName) {
|
|
221
|
+
const primitiveTypeLowerCase = typeName.toLowerCase();
|
|
222
|
+
return [
|
|
223
|
+
'integer', 'unsignedint', 'positiveint', 'decimal', 'boolean', 'string',
|
|
224
|
+
'base64binary', 'instant', 'date', 'datetime', 'time', 'code', 'oid',
|
|
225
|
+
'id', 'markdown', 'uri', 'url', 'canonical', 'uuid'
|
|
226
|
+
].includes(primitiveTypeLowerCase);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Adds a specific FHIR type to imports.
|
|
230
|
+
*/
|
|
231
|
+
addFhirType(typeName) {
|
|
232
|
+
if (this.isFhirType(typeName)) {
|
|
233
|
+
this.fhirImports.add(typeName);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Adds a specific custom local type to imports.
|
|
238
|
+
*/
|
|
239
|
+
addCustomType(typeName) {
|
|
240
|
+
if (!this.localInterfaceNames.has(typeName)) {
|
|
241
|
+
this.customImports.add(typeName);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Adds an import with an explicit relative path (e.g., for ValueSets in subdirectories).
|
|
246
|
+
* @param typeName - The type name to import
|
|
247
|
+
* @param relativePath - The relative path without extension (e.g., "./valuesets/ValueSet-Example")
|
|
248
|
+
*/
|
|
249
|
+
addPathBasedImport(typeName, relativePath) {
|
|
250
|
+
this.pathBasedImports.set(typeName, relativePath);
|
|
251
|
+
}
|
|
252
|
+
}
|
package/out/generator/index.js
CHANGED
|
@@ -518,6 +518,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
518
518
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
519
519
|
// Load ValueSets from the directory
|
|
520
520
|
const valueSets = readValueSetsFromDir(inputDir);
|
|
521
|
+
const valueSetCodesMap = readValueSetCodesFromDir(inputDir);
|
|
521
522
|
console.log(`Loaded ${valueSets.size} ValueSets from ${inputDir} (${Array.from(valueSets.values()).filter(vs => vs.isSmall).length} suitable for union types)`);
|
|
522
523
|
// Generate TypeScript files for ValueSets
|
|
523
524
|
const valueSetOutputDir = path.join(outputDir, 'valuesets');
|
|
@@ -617,7 +618,7 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
617
618
|
const full = path.join(inputDir, jf);
|
|
618
619
|
try {
|
|
619
620
|
const raw = JSON.parse(fs.readFileSync(full, 'utf-8'));
|
|
620
|
-
await processStructureDefinition(raw, { outputDir, fhirSourceHint: full, valueSets, existingStructureDefinitions, profileIdToName, flags });
|
|
621
|
+
await processStructureDefinition(raw, { outputDir, fhirSourceHint: full, valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, flags });
|
|
621
622
|
}
|
|
622
623
|
catch (err) {
|
|
623
624
|
console.error(`Failed processing JSON ${jf}:`, err);
|
|
@@ -1,72 +1,27 @@
|
|
|
1
1
|
import fhirInterfaceNames from "./fhirInterfaces.json" with { type: 'json' };
|
|
2
2
|
import { capitalize, sanitizeIdentifier, toPascalCase } from './utils.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getUniformSystem } from './vsParser.js';
|
|
4
|
+
import { sanitizeValueSetName } from './valueSetGenerator.js';
|
|
4
5
|
import { logger } from '../logger.js';
|
|
6
|
+
import { ImportManager } from './importManager.js';
|
|
5
7
|
export function generateInterfaces(interfaceName, newFields, baseResource, baseFields = [], valueSets, resourceType, existingProfiles, fhirChildTypeMap) {
|
|
6
8
|
const debug = (...args) => { if (process.env.DEBUG_FHIR_GEN === 'true')
|
|
7
9
|
console.log('[gen:interfaces]', ...args); };
|
|
8
|
-
const imports = new Set();
|
|
9
|
-
const customImports = new Set();
|
|
10
10
|
const interfaces = [];
|
|
11
11
|
const localInterfaceNames = new Set();
|
|
12
12
|
const writtenLines = new Set();
|
|
13
13
|
// Track minimal alias extension interfaces generated in this file to avoid duplicates
|
|
14
14
|
const generatedAliasTypes = new Set();
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return;
|
|
20
|
-
const sanitizedTypeName = sanitizeIdentifier(rawTypeName);
|
|
21
|
-
const resolvedProfileUrl = profileUrl && profileUrl.length > 0
|
|
22
|
-
? profileUrl
|
|
23
|
-
: `http://hl7.org/fhir/StructureDefinition/${sanitizedTypeName}`;
|
|
24
|
-
let meta = referencedExternalProfiles.get(sanitizedTypeName);
|
|
25
|
-
if (!meta) {
|
|
26
|
-
meta = { profileUrl: resolvedProfileUrl, baseTypes: new Map() };
|
|
27
|
-
referencedExternalProfiles.set(sanitizedTypeName, meta);
|
|
28
|
-
}
|
|
29
|
-
else if (profileUrl && profileUrl.length > 0 &&
|
|
30
|
-
meta.profileUrl.startsWith('http://hl7.org/fhir/StructureDefinition/')) {
|
|
31
|
-
meta.profileUrl = profileUrl;
|
|
32
|
-
}
|
|
33
|
-
if (!baseType || baseType === 'any')
|
|
34
|
-
return;
|
|
35
|
-
const sanitizedBase = sanitizeIdentifier(baseType);
|
|
36
|
-
const existing = meta.baseTypes.get(sanitizedBase);
|
|
37
|
-
if (!existing) {
|
|
38
|
-
meta.baseTypes.set(sanitizedBase, { isFhir: isFhirBase });
|
|
39
|
-
}
|
|
40
|
-
else if (isFhirBase && !existing.isFhir) {
|
|
41
|
-
existing.isFhir = true;
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
// Helper to add a type import, checking if it's locally generated first
|
|
45
|
-
const addTypeImport = (typeName) => {
|
|
15
|
+
// Initialize ImportManager to handle all import statements
|
|
16
|
+
const importManager = new ImportManager(localInterfaceNames, existingProfiles);
|
|
17
|
+
// Helper to add a type import using ImportManager
|
|
18
|
+
const addTypeImport = (typeName, profileUrl, baseType, isFhirBase) => {
|
|
46
19
|
// If the type is a nested interface of the current profile, don't import it (defined in same file)
|
|
47
20
|
const isNestedOfThisProfile = typeName.startsWith(interfaceName);
|
|
48
21
|
if (isNestedOfThisProfile && localInterfaceNames.has(typeName)) {
|
|
49
22
|
return; // don't add to imports - defined locally in this file
|
|
50
23
|
}
|
|
51
|
-
|
|
52
|
-
if (existingProfiles?.has(typeName) || localInterfaceNames.has(typeName)) {
|
|
53
|
-
customImports.add(typeName);
|
|
54
|
-
}
|
|
55
|
-
else if (isFhirType(typeName)) {
|
|
56
|
-
imports.add(typeName);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
// Check if this is a primitive type that shouldn't be fetched
|
|
60
|
-
const primitiveTypeLowerCase = typeName.toLowerCase();
|
|
61
|
-
const isPrimitiveLikeType = ['integer', 'unsignedint', 'positiveint', 'decimal', 'boolean', 'string',
|
|
62
|
-
'base64binary', 'instant', 'date', 'datetime', 'time', 'code', 'oid', 'id', 'markdown',
|
|
63
|
-
'uri', 'url', 'canonical', 'uuid'].includes(primitiveTypeLowerCase);
|
|
64
|
-
customImports.add(typeName);
|
|
65
|
-
// Track as external profile to be fetched if not already known and not a primitive type
|
|
66
|
-
if (!isPrimitiveLikeType) {
|
|
67
|
-
registerExternalProfile(typeName);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
24
|
+
importManager.addType(typeName, profileUrl, baseType, isFhirBase);
|
|
70
25
|
};
|
|
71
26
|
// Helper to find exact interface block index (avoid prefix collisions like Foo and FooBar)
|
|
72
27
|
const findInterfaceIndex = (name) => {
|
|
@@ -87,28 +42,57 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
87
42
|
baseFieldByLastSegment.set(seg, bf);
|
|
88
43
|
}
|
|
89
44
|
// Helper to apply ValueSet binding constraints to CodeableConcept types
|
|
90
|
-
const applyBindingConstraint = (fieldType, field) => {
|
|
91
|
-
// Only apply to CodeableConcept types
|
|
92
|
-
if (fieldType !== 'CodeableConcept' || !
|
|
45
|
+
const applyBindingConstraint = (fieldType, field, allFields) => {
|
|
46
|
+
// Only apply to CodeableConcept types
|
|
47
|
+
if (fieldType !== 'CodeableConcept' || !valueSets) {
|
|
93
48
|
return fieldType;
|
|
94
49
|
}
|
|
95
|
-
|
|
50
|
+
// Check if this field or any of its slices have bindings
|
|
51
|
+
let binding = field.binding;
|
|
52
|
+
let isSlicedField = false;
|
|
53
|
+
// If this is a base field (no sliceName), check if any slices have more specific bindings
|
|
54
|
+
if (!field.sliceName) {
|
|
55
|
+
const slicesForThisField = allFields.filter(f => f.name === field.name &&
|
|
56
|
+
f.sliceName &&
|
|
57
|
+
f.binding?.uri);
|
|
58
|
+
// Use the first slice binding if available (slices typically have more specific constraints)
|
|
59
|
+
if (slicesForThisField.length > 0 && slicesForThisField[0].binding) {
|
|
60
|
+
binding = slicesForThisField[0].binding;
|
|
61
|
+
isSlicedField = true;
|
|
62
|
+
debug(`Using binding from slice ${slicesForThisField[0].sliceName} for ${field.name}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!binding) {
|
|
66
|
+
return fieldType;
|
|
67
|
+
}
|
|
68
|
+
const valueSet = valueSets.get(binding.uri);
|
|
96
69
|
if (!valueSet || !valueSet.isSmall) {
|
|
97
70
|
// ValueSet not found or too large for union type - just use CodeableConcept
|
|
98
|
-
debug(`Skipping binding constraint for ${field.name}: ValueSet ${
|
|
71
|
+
debug(`Skipping binding constraint for ${field.name}: ValueSet ${binding.uri} ${!valueSet ? 'not found' : 'too large'}`);
|
|
99
72
|
return fieldType;
|
|
100
73
|
}
|
|
101
|
-
//
|
|
102
|
-
const
|
|
74
|
+
// Import the ValueSet module
|
|
75
|
+
const sanitizedVSName = sanitizeValueSetName(valueSet.name);
|
|
76
|
+
const vsTypeName = `${sanitizedVSName}Code`;
|
|
77
|
+
// Add import for the ValueSet type (from valuesets subdirectory)
|
|
78
|
+
importManager.addPathBasedImport(vsTypeName, './valuesets/ValueSet-' + sanitizedVSName);
|
|
79
|
+
// Generate constrained type using the imported ValueSet Code type
|
|
103
80
|
const system = getUniformSystem(valueSet);
|
|
81
|
+
let constrainedType;
|
|
104
82
|
if (system && valueSet.concepts.length > 0) {
|
|
105
|
-
// All codes from same system - generate strict constraint
|
|
106
|
-
|
|
83
|
+
// All codes from same system - generate strict constraint with system
|
|
84
|
+
constrainedType = `CodeableConcept & { coding: Array<{ code: ${vsTypeName}; system: "${system}" }> }`;
|
|
107
85
|
}
|
|
108
86
|
else {
|
|
109
87
|
// Mixed systems or no uniform system - just constrain the code
|
|
110
|
-
|
|
88
|
+
constrainedType = `CodeableConcept & { coding: Array<{ code: ${vsTypeName} }> }`;
|
|
111
89
|
}
|
|
90
|
+
// If this is a sliced field (binding comes from a slice), create a union type
|
|
91
|
+
// to allow both constrained and unconstrained elements (open slicing)
|
|
92
|
+
if (isSlicedField) {
|
|
93
|
+
return `(${constrainedType} | CodeableConcept)`;
|
|
94
|
+
}
|
|
95
|
+
return constrainedType;
|
|
112
96
|
};
|
|
113
97
|
function processFields(fields, parentInterfaceName, parentFieldType, isProcessingSlice = false) {
|
|
114
98
|
const interfaceLines = [];
|
|
@@ -551,8 +535,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
551
535
|
aliasTypes.push(alias);
|
|
552
536
|
}
|
|
553
537
|
}
|
|
554
|
-
|
|
555
|
-
imports.add('Extension');
|
|
538
|
+
importManager.addFhirType('Extension');
|
|
556
539
|
const union = ['Extension', ...aliasTypes].join(' | ');
|
|
557
540
|
// extension on elements is always an optional array
|
|
558
541
|
const extLine = `extension?: (${union})[];`;
|
|
@@ -666,7 +649,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
666
649
|
// Special-case: Bundle.entry.resource should be FhirResource
|
|
667
650
|
if (fieldName === 'resource' && (baseResource === 'Bundle' || resourceType === 'Bundle')) {
|
|
668
651
|
resolvedType = 'FhirResource';
|
|
669
|
-
|
|
652
|
+
importManager.addFhirType('FhirResource');
|
|
670
653
|
}
|
|
671
654
|
// Final primitive normalization for nested branch as well
|
|
672
655
|
resolvedType = mapTypeToTS(resolvedType);
|
|
@@ -737,7 +720,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
737
720
|
const profileUrl = field.profileUrls?.[0] || (field.type && field.type.includes('http') ? field.type : undefined);
|
|
738
721
|
const baseTypeName = baseFld?.type;
|
|
739
722
|
const baseIsFhir = baseTypeName ? isFhirType(baseTypeName) : false;
|
|
740
|
-
|
|
723
|
+
addTypeImport(resolvedType, profileUrl, baseTypeName, baseIsFhir);
|
|
741
724
|
}
|
|
742
725
|
if (!isPrimitiveType(resolvedType)) {
|
|
743
726
|
if (isFhirType(resolvedType)) {
|
|
@@ -771,7 +754,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
771
754
|
}
|
|
772
755
|
}
|
|
773
756
|
else {
|
|
774
|
-
|
|
757
|
+
importManager.addCustomType(sanitizeIdentifier(resolvedType));
|
|
775
758
|
}
|
|
776
759
|
}
|
|
777
760
|
}
|
|
@@ -1023,7 +1006,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1023
1006
|
// Special-case: Bundle.entry.resource should be FhirResource, not Resource
|
|
1024
1007
|
if (fieldName === 'resource' && (baseResource === 'Bundle' || resourceType === 'Bundle')) {
|
|
1025
1008
|
fieldType = 'FhirResource';
|
|
1026
|
-
|
|
1009
|
+
importManager.addFhirType('FhirResource');
|
|
1027
1010
|
}
|
|
1028
1011
|
// Normalize canonical URLs to local type names
|
|
1029
1012
|
if (!isPrimitiveType(fieldType) && !isFhirType(fieldType)) {
|
|
@@ -1044,8 +1027,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1044
1027
|
aliasTypes.push(alias);
|
|
1045
1028
|
}
|
|
1046
1029
|
}
|
|
1047
|
-
|
|
1048
|
-
imports.add('Extension');
|
|
1030
|
+
importManager.addFhirType('Extension');
|
|
1049
1031
|
const union = ['Extension', ...aliasTypes].join(' | ');
|
|
1050
1032
|
fieldType = `(${union})[]`;
|
|
1051
1033
|
// We will set isArray=false to avoid adding [] twice in the final line
|
|
@@ -1070,17 +1052,16 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1070
1052
|
// Track this as an external profile that needs a type alias file
|
|
1071
1053
|
const profileUrl = sl.profileUrls?.[0] || sl.type || '';
|
|
1072
1054
|
if (profileUrl && !isFhirType(normalizedType)) {
|
|
1073
|
-
|
|
1055
|
+
addTypeImport(normalizedType, profileUrl, 'Identifier', true);
|
|
1074
1056
|
}
|
|
1075
1057
|
// Add to custom imports if not a FHIR type
|
|
1076
1058
|
if (!isFhirType(normalizedType)) {
|
|
1077
|
-
|
|
1059
|
+
importManager.addCustomType(normalizedType);
|
|
1078
1060
|
}
|
|
1079
1061
|
}
|
|
1080
1062
|
}
|
|
1081
1063
|
if (profileTypes.length > 0) {
|
|
1082
|
-
|
|
1083
|
-
imports.add('Identifier');
|
|
1064
|
+
importManager.addFhirType('Identifier');
|
|
1084
1065
|
const union = ['Identifier', ...profileTypes].join(' | ');
|
|
1085
1066
|
fieldType = `(${union})[]`;
|
|
1086
1067
|
// We will set isArray=false to avoid adding [] twice in the final line
|
|
@@ -1115,8 +1096,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1115
1096
|
aliasTypes.push(alias);
|
|
1116
1097
|
}
|
|
1117
1098
|
}
|
|
1118
|
-
|
|
1119
|
-
imports.add('Extension');
|
|
1099
|
+
importManager.addFhirType('Extension');
|
|
1120
1100
|
const union = ['Extension', ...aliasTypes].join(' | ');
|
|
1121
1101
|
const extLine = `extension?: (${union})[];`;
|
|
1122
1102
|
// Determine parent type to extend
|
|
@@ -1135,7 +1115,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1135
1115
|
if (!parentT || isPrimitiveLike(parentT)) {
|
|
1136
1116
|
const elementIface = `${parentInterfaceName}${capitalize(fieldName)}Element`;
|
|
1137
1117
|
// Ensure Element import
|
|
1138
|
-
|
|
1118
|
+
importManager.addFhirType('Element');
|
|
1139
1119
|
if (!interfaces.some(i => i.startsWith(`export interface ${elementIface}`))) {
|
|
1140
1120
|
interfaces.push(`export interface ${elementIface} extends Element {\n ${extLine}\n}`);
|
|
1141
1121
|
}
|
|
@@ -1243,7 +1223,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1243
1223
|
}
|
|
1244
1224
|
}
|
|
1245
1225
|
// Apply ValueSet binding constraints for CodeableConcept types
|
|
1246
|
-
fieldType = applyBindingConstraint(fieldType, field);
|
|
1226
|
+
fieldType = applyBindingConstraint(fieldType, field, fields);
|
|
1247
1227
|
// Preserve arrayness from base when known; if base metadata is missing and field name is a known repeating element (e.g., 'coverage' on Account),
|
|
1248
1228
|
// default to array to avoid incorrect narrowing.
|
|
1249
1229
|
// Skip adding [] if fieldType already contains array syntax (e.g., from identifier/extension union types)
|
|
@@ -1493,20 +1473,20 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1493
1473
|
if (!isFhirType(sanitizedBase)) {
|
|
1494
1474
|
// Check if this profile exists in our generated files
|
|
1495
1475
|
if (existingProfiles?.has(sanitizedBase)) {
|
|
1496
|
-
|
|
1476
|
+
importManager.addCustomType(sanitizedBase);
|
|
1497
1477
|
}
|
|
1498
1478
|
else if (resourceType && isFhirType(resourceType)) {
|
|
1499
1479
|
// Fallback to the actual FHIR resource type if base profile doesn't exist
|
|
1500
|
-
|
|
1480
|
+
importManager.addFhirType(resourceType);
|
|
1501
1481
|
// Update baseResource to use the fallback for extends clause
|
|
1502
1482
|
baseResource = resourceType;
|
|
1503
1483
|
}
|
|
1504
1484
|
else {
|
|
1505
|
-
|
|
1485
|
+
importManager.addCustomType(sanitizedBase);
|
|
1506
1486
|
}
|
|
1507
1487
|
}
|
|
1508
1488
|
else {
|
|
1509
|
-
|
|
1489
|
+
importManager.addFhirType(sanitizedBase);
|
|
1510
1490
|
}
|
|
1511
1491
|
}
|
|
1512
1492
|
processFields(newFields, interfaceName);
|
|
@@ -1540,8 +1520,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1540
1520
|
interfaces.unshift(`export interface ${alias} extends Extension { url: '${url}' }`);
|
|
1541
1521
|
generatedAliasTypes.add(alias);
|
|
1542
1522
|
}
|
|
1543
|
-
|
|
1544
|
-
imports.add('Extension');
|
|
1523
|
+
importManager.addFhirType('Extension');
|
|
1545
1524
|
};
|
|
1546
1525
|
// Helper to append a line to an existing interface body; if target doesn't exist and it's the root, synthesize it
|
|
1547
1526
|
const appendToInterface = (name, line) => {
|
|
@@ -1551,10 +1530,10 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1551
1530
|
const sanitizedBase = sanitizeIdentifier(baseResource);
|
|
1552
1531
|
// Ensure import of the base resource type
|
|
1553
1532
|
if (!isFhirType(sanitizedBase)) {
|
|
1554
|
-
|
|
1533
|
+
importManager.addCustomType(sanitizedBase);
|
|
1555
1534
|
}
|
|
1556
1535
|
else {
|
|
1557
|
-
|
|
1536
|
+
importManager.addFhirType(sanitizedBase);
|
|
1558
1537
|
}
|
|
1559
1538
|
interfaces.push(`export interface ${rootName} extends ${sanitizedBase} {}`);
|
|
1560
1539
|
}
|
|
@@ -1623,7 +1602,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1623
1602
|
if (isPrim) {
|
|
1624
1603
|
// Create Element sidecar with extension union
|
|
1625
1604
|
const elementIface = `${rootName}${capitalize(seg)}Element`;
|
|
1626
|
-
|
|
1605
|
+
importManager.addFhirType('Element');
|
|
1627
1606
|
if (!interfaces.some(i => i.startsWith(`export interface ${elementIface}`))) {
|
|
1628
1607
|
interfaces.push(`export interface ${elementIface} extends Element {\n extension?: (${union})[];\n}`);
|
|
1629
1608
|
}
|
|
@@ -1674,7 +1653,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1674
1653
|
});
|
|
1675
1654
|
}
|
|
1676
1655
|
if (extendT)
|
|
1677
|
-
|
|
1656
|
+
addTypeImport(extendT);
|
|
1678
1657
|
const extendsClause = extendT ? ` extends ${extendT}` : '';
|
|
1679
1658
|
if (!interfaces.some(i => i.startsWith(`export interface ${nestedName}`))) {
|
|
1680
1659
|
interfaces.push(`export interface ${nestedName}${extendsClause} {\n extension?: (${union})[];\n}`);
|
|
@@ -1688,7 +1667,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1688
1667
|
appendToInterface(rootName, `${seg}${opt}: ${nestedName}${isArr};`);
|
|
1689
1668
|
// Also record import for known FHIR backbone
|
|
1690
1669
|
if (extendT && isFhirType(extendT))
|
|
1691
|
-
|
|
1670
|
+
addTypeImport(extendT);
|
|
1692
1671
|
}
|
|
1693
1672
|
}
|
|
1694
1673
|
else if (segs.length === 2) {
|
|
@@ -1706,7 +1685,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1706
1685
|
if (!interfaces.some(i => i.startsWith(`export interface ${lvl1Name}`))) {
|
|
1707
1686
|
const extendsClause1 = extendT1 ? ` extends ${extendT1}` : '';
|
|
1708
1687
|
if (extendT1 && isFhirType(extendT1))
|
|
1709
|
-
|
|
1688
|
+
addTypeImport(extendT1);
|
|
1710
1689
|
interfaces.push(`export interface ${lvl1Name}${extendsClause1} {}`);
|
|
1711
1690
|
appendToInterface(rootName, `${seg1}: ${lvl1Name};`);
|
|
1712
1691
|
}
|
|
@@ -1728,7 +1707,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1728
1707
|
}
|
|
1729
1708
|
}
|
|
1730
1709
|
if (extendT2 && isFhirType(extendT2))
|
|
1731
|
-
|
|
1710
|
+
addTypeImport(extendT2);
|
|
1732
1711
|
const extendsClause2 = extendT2 ? ` extends ${extendT2}` : '';
|
|
1733
1712
|
if (!interfaces.some(i => i.startsWith(`export interface ${lvl2Name}`))) {
|
|
1734
1713
|
interfaces.push(`export interface ${lvl2Name}${extendsClause2} {\n extension?: (${union})[];\n}`);
|
|
@@ -1758,10 +1737,10 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1758
1737
|
if (baseResource && !isBaseAbs) {
|
|
1759
1738
|
const sanitizedBase = sanitizedBaseRes;
|
|
1760
1739
|
if (!isFhirType(sanitizedBase)) {
|
|
1761
|
-
|
|
1740
|
+
importManager.addCustomType(sanitizedBase);
|
|
1762
1741
|
}
|
|
1763
1742
|
else {
|
|
1764
|
-
|
|
1743
|
+
importManager.addFhirType(sanitizedBase);
|
|
1765
1744
|
}
|
|
1766
1745
|
interfaces.push(`export type ${interfaceName} = ${sanitizedBase};`);
|
|
1767
1746
|
}
|
|
@@ -1791,8 +1770,7 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1791
1770
|
aliases.push(alias);
|
|
1792
1771
|
}
|
|
1793
1772
|
}
|
|
1794
|
-
|
|
1795
|
-
imports.add('Extension');
|
|
1773
|
+
importManager.addFhirType('Extension');
|
|
1796
1774
|
const union = ['Extension', ...aliases].join(' | ');
|
|
1797
1775
|
const idx = findInterfaceIndex(interfaceName);
|
|
1798
1776
|
if (idx >= 0 && !/\bextension\?\s*:\s*\(/.test(interfaces[idx])) {
|
|
@@ -1800,97 +1778,12 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1800
1778
|
}
|
|
1801
1779
|
}
|
|
1802
1780
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
// Ensure HumanName import if referenced but not imported (flattened case)
|
|
1810
|
-
if (/\bHumanName\b/.test(output) && !/import \{[^}]*HumanName/.test(output)) {
|
|
1811
|
-
output = `import { HumanName } from "fhir/r4";\n` + output;
|
|
1812
|
-
}
|
|
1813
|
-
// Minimal post-processing retained only for import pruning.
|
|
1814
|
-
// Prune unused imported identifiers (both FHIR and custom) to satisfy lint rules.
|
|
1815
|
-
output = output.replace(/import \{([^}]+)\} from "fhir\/r4";?/g, (full, inside) => {
|
|
1816
|
-
const used = [];
|
|
1817
|
-
const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1818
|
-
for (const name of specifiers) {
|
|
1819
|
-
const re = new RegExp(`\\b${name}\\b`);
|
|
1820
|
-
if (re.test(output.replace(full, '')))
|
|
1821
|
-
used.push(name);
|
|
1822
|
-
}
|
|
1823
|
-
return used.length ? `import { ${used.join(', ')} } from "fhir/r4";` : '';
|
|
1824
|
-
});
|
|
1825
|
-
output = output.replace(/import \{([^}]+)\} from "\.\/(.+?)";?/g, (full, inside, path) => {
|
|
1826
|
-
const used = [];
|
|
1827
|
-
const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1828
|
-
for (const name of specifiers) {
|
|
1829
|
-
const re = new RegExp(`\\b${name}\\b`);
|
|
1830
|
-
if (re.test(output.replace(full, '')))
|
|
1831
|
-
used.push(name);
|
|
1832
|
-
}
|
|
1833
|
-
return used.length ? `import { ${used.join(', ')} } from "./${path}";` : '';
|
|
1834
|
-
});
|
|
1835
|
-
// Remove now-empty import lines and collapse multiple blank lines.
|
|
1836
|
-
output = output.replace(/^\s*import \{\s*\}.*$/mg, '').replace(/\n{3,}/g, '\n\n');
|
|
1837
|
-
// Post-processing: Move locally-generated types from fhir/r4 imports to local imports
|
|
1838
|
-
if (existingProfiles && existingProfiles.size > 0) {
|
|
1839
|
-
// Extract ALL types from fhir/r4 imports (there may be multiple import lines)
|
|
1840
|
-
const fhirImportRegex = /import \{([^}]+)\} from "fhir\/r4";/g;
|
|
1841
|
-
const allFhirTypes = [];
|
|
1842
|
-
let match;
|
|
1843
|
-
logger.log('[DEBUG post-processing]', { interfaceName, hasSimpleQuantity: existingProfiles.has('SimpleQuantity'), existingProfilesSize: existingProfiles.size });
|
|
1844
|
-
while ((match = fhirImportRegex.exec(output)) !== null) {
|
|
1845
|
-
const types = match[1].split(',').map(t => t.trim()).filter(Boolean);
|
|
1846
|
-
logger.log('[DEBUG found fhir/r4 import]', { types });
|
|
1847
|
-
allFhirTypes.push(...types);
|
|
1848
|
-
}
|
|
1849
|
-
if (allFhirTypes.length > 0) {
|
|
1850
|
-
const localTypes = [];
|
|
1851
|
-
const remainingFhirTypes = [];
|
|
1852
|
-
logger.log('[DEBUG separating types]', { allFhirTypes });
|
|
1853
|
-
for (const typeName of allFhirTypes) {
|
|
1854
|
-
if (existingProfiles.has(typeName)) {
|
|
1855
|
-
logger.log('[DEBUG local type]', { typeName });
|
|
1856
|
-
// Never move FhirResource to local imports; it must stay from fhir/r4
|
|
1857
|
-
if (typeName !== 'FhirResource') {
|
|
1858
|
-
localTypes.push(typeName);
|
|
1859
|
-
}
|
|
1860
|
-
else {
|
|
1861
|
-
remainingFhirTypes.push(typeName);
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
else {
|
|
1865
|
-
logger.log('[DEBUG fhir type]', { typeName });
|
|
1866
|
-
remainingFhirTypes.push(typeName);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
logger.log('[DEBUG separated]', { localTypes, remainingFhirTypes });
|
|
1870
|
-
// Remove ALL old fhir/r4 import lines
|
|
1871
|
-
output = output.replace(/import \{[^}]+\} from "fhir\/r4";\n?/g, '');
|
|
1872
|
-
// Add back the imports at the top
|
|
1873
|
-
let newImports = '';
|
|
1874
|
-
if (remainingFhirTypes.length > 0) {
|
|
1875
|
-
newImports += `import { ${remainingFhirTypes.join(', ')} } from "fhir/r4";\n`;
|
|
1876
|
-
}
|
|
1877
|
-
for (const localType of localTypes) {
|
|
1878
|
-
newImports += `import { ${localType} } from "./${localType}";\n`;
|
|
1879
|
-
}
|
|
1880
|
-
// Insert new imports after any existing custom imports or at the top
|
|
1881
|
-
if (newImports) {
|
|
1882
|
-
const customImportMatch = output.match(/^(import \{[^}]+\} from "\.\/[^"]+";?\n)+/m);
|
|
1883
|
-
if (customImportMatch) {
|
|
1884
|
-
// Insert after existing custom imports
|
|
1885
|
-
output = output.replace(customImportMatch[0], customImportMatch[0] + newImports);
|
|
1886
|
-
}
|
|
1887
|
-
else {
|
|
1888
|
-
// Insert at the very top
|
|
1889
|
-
output = newImports + output;
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1781
|
+
// Generate import statements using ImportManager
|
|
1782
|
+
const importStatements = importManager.generateImportStatements();
|
|
1783
|
+
let output = `${importStatements}\n${interfaces.join("\n\n")}`;
|
|
1784
|
+
// Post-processing: prune unused imports and reorganize local types
|
|
1785
|
+
output = importManager.pruneUnusedImports(output);
|
|
1786
|
+
output = importManager.reorganizeImports(output);
|
|
1894
1787
|
// Final logging to see all interfaces
|
|
1895
1788
|
if (interfaceName.includes('ConditionDeBasis02')) {
|
|
1896
1789
|
logger.log('[DEBUG FINAL INTERFACES]', {
|
|
@@ -1901,6 +1794,6 @@ export function generateInterfaces(interfaceName, newFields, baseResource, baseF
|
|
|
1901
1794
|
}
|
|
1902
1795
|
return {
|
|
1903
1796
|
content: output,
|
|
1904
|
-
referencedExternalProfiles
|
|
1797
|
+
referencedExternalProfiles: importManager.getExternalProfiles()
|
|
1905
1798
|
};
|
|
1906
1799
|
}
|
|
@@ -249,6 +249,15 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
249
249
|
console.warn(`StructureDefinition ${structureDefinition.id || "unknown"} has no elements.`);
|
|
250
250
|
return { newFields: [], oldFields: baseFields };
|
|
251
251
|
}
|
|
252
|
+
// Build a map from snapshot for enriching differential elements with binding information
|
|
253
|
+
const snapshotBindings = new Map();
|
|
254
|
+
if (structureDefinition.differential && structureDefinition.snapshot) {
|
|
255
|
+
for (const snapElem of structureDefinition.snapshot.element) {
|
|
256
|
+
if (snapElem.binding) {
|
|
257
|
+
snapshotBindings.set(snapElem.path, snapElem.binding);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
252
261
|
const newFields = elements.map((element) => {
|
|
253
262
|
const name = element.path;
|
|
254
263
|
// Use the profile name if it exists, otherwise fallback to the type code
|
|
@@ -327,7 +336,11 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
327
336
|
.filter((constraint) => constraint !== null);
|
|
328
337
|
// ValueSet binding sampling (deterministic): capture binding.valueSet and choose a code from local map if available
|
|
329
338
|
let bindingMeta = undefined;
|
|
330
|
-
|
|
339
|
+
// First try element's own binding, then fall back to snapshot binding if available
|
|
340
|
+
let bindingUri = element.binding?.valueSet;
|
|
341
|
+
if (!bindingUri && snapshotBindings.has(element.path)) {
|
|
342
|
+
bindingUri = snapshotBindings.get(element.path)?.valueSet;
|
|
343
|
+
}
|
|
331
344
|
// Extract sample code from pattern if present (e.g., patternCodeableConcept or patternCoding)
|
|
332
345
|
let patternCode;
|
|
333
346
|
if (patternValue) {
|
package/out/generator/utils.js
CHANGED
|
@@ -60,13 +60,17 @@ export function alignFields(newFields, oldFields) {
|
|
|
60
60
|
}
|
|
61
61
|
export function mergeFields(newFields, oldFields) {
|
|
62
62
|
const fieldMap = new Map();
|
|
63
|
+
// Create unique key for field that includes sliceName if present
|
|
64
|
+
const getFieldKey = (field) => {
|
|
65
|
+
return field.sliceName ? `${field.name}:${field.sliceName}` : field.name;
|
|
66
|
+
};
|
|
63
67
|
// Add oldFields to the map
|
|
64
68
|
for (const field of oldFields) {
|
|
65
|
-
fieldMap.set(field
|
|
69
|
+
fieldMap.set(getFieldKey(field), field);
|
|
66
70
|
}
|
|
67
71
|
// Overwrite or add newFields to the map
|
|
68
72
|
for (const field of newFields) {
|
|
69
|
-
fieldMap.set(field
|
|
73
|
+
fieldMap.set(getFieldKey(field), field);
|
|
70
74
|
}
|
|
71
75
|
// Return the merged fields as an array
|
|
72
76
|
return Array.from(fieldMap.values());
|
|
@@ -45,6 +45,43 @@ export function generateValidateProfileFunction(interfaceName, fields // Array o
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
|
+
// Add slice cardinality validation for sliced fields with bindings
|
|
49
|
+
const sliceValidations = [];
|
|
50
|
+
const slicesByBase = new Map();
|
|
51
|
+
// Group slices by their base field name
|
|
52
|
+
fields.forEach(field => {
|
|
53
|
+
if (field.sliceName && field.binding?.uri) {
|
|
54
|
+
const baseName = field.name;
|
|
55
|
+
if (!slicesByBase.has(baseName)) {
|
|
56
|
+
slicesByBase.set(baseName, []);
|
|
57
|
+
}
|
|
58
|
+
slicesByBase.get(baseName).push(field);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// Generate validation for each sliced field
|
|
62
|
+
slicesByBase.forEach((slices, baseName) => {
|
|
63
|
+
slices.forEach(slice => {
|
|
64
|
+
const min = typeof slice.min === "number" ? slice.min : (slice.isOptional ? 0 : 1);
|
|
65
|
+
if (min > 0 && slice.binding?.codes && slice.binding.codes.length > 0) {
|
|
66
|
+
const relPath = baseName.replace(/^[^.]+\./, "");
|
|
67
|
+
const codes = slice.binding.codes.map(c => `"${c.code}"`).join(", ");
|
|
68
|
+
// Escape double quotes in codes list for use in error message string
|
|
69
|
+
const escapedCodes = slice.binding.codes.map(c => c.code).join(", ");
|
|
70
|
+
const minText = min === 1 ? "at least one" : `at least ${min}`;
|
|
71
|
+
const sliceLabel = slice.sliceName || "slice";
|
|
72
|
+
// Sanitize slice name for use as JavaScript variable name
|
|
73
|
+
const varName = sliceLabel.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
74
|
+
sliceValidations.push(`
|
|
75
|
+
// Slice cardinality check for ${baseName}:${sliceLabel}
|
|
76
|
+
const ${varName}Count = resource.${relPath}?.filter(item =>
|
|
77
|
+
item.coding?.some(coding => [${codes}].includes(coding.code as string))
|
|
78
|
+
).length || 0;
|
|
79
|
+
if (${varName}Count < ${min}) {
|
|
80
|
+
errors.push("Slice '${sliceLabel}' on ${relPath}: must have ${minText} element(s) with codes: ${escapedCodes}");
|
|
81
|
+
}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
48
85
|
// Filter out constraints that require terminology or reference resolution which our current
|
|
49
86
|
// synchronous evaluator does not support in parity mode (memberOf, resolve).
|
|
50
87
|
const filteredConstraints = uniqueConstraints.filter(c => !/(\bmemberOf\b|\bresolve\b)/.test(c.expression || ""));
|
|
@@ -61,20 +98,18 @@ export function generateValidateProfileFunction(interfaceName, fields // Array o
|
|
|
61
98
|
}`;
|
|
62
99
|
})
|
|
63
100
|
.join("\n");
|
|
64
|
-
const hasConstraints = filteredConstraints.length > 0;
|
|
101
|
+
const hasConstraints = filteredConstraints.length > 0 || sliceValidations.length > 0;
|
|
65
102
|
// Always emit a uniform signature validateX(resource: X) so callers don't branch on arity.
|
|
66
103
|
// Only import fhirpath and run evaluation logic when there are constraints.
|
|
67
104
|
if (!hasConstraints) {
|
|
68
105
|
return `
|
|
69
106
|
export async function validate${interfaceName}(resource: ${interfaceName}): Promise<{ errors: string[], warnings: string[] }> {\n // Touch the parameter so eslint no-unused-vars doesn't flag it when there are no constraints.\n void resource;\n return { errors: [], warnings: [] };\n}`;
|
|
70
107
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
export async function validate${interfaceName}(resource: ${interfaceName}): Promise<{ errors: string[], warnings: string[] }> {
|
|
108
|
+
const fhirpathImport = filteredConstraints.length > 0 ? 'import fhirpath from "fhirpath";\n\n' : '';
|
|
109
|
+
return `${fhirpathImport}export async function validate${interfaceName}(resource: ${interfaceName}): Promise<{ errors: string[], warnings: string[] }> {
|
|
75
110
|
const errors: string[] = [];
|
|
76
111
|
const warnings: string[] = [];
|
|
77
|
-
${validationLogic}
|
|
112
|
+
${validationLogic}${sliceValidations.join('')}
|
|
78
113
|
return { errors, warnings };
|
|
79
114
|
}`;
|
|
80
115
|
}
|
|
@@ -56,7 +56,7 @@ export function generateValueSetTypeScript(valueSet) {
|
|
|
56
56
|
/**
|
|
57
57
|
* Sanitize ValueSet name for use as TypeScript identifier
|
|
58
58
|
*/
|
|
59
|
-
function sanitizeValueSetName(name) {
|
|
59
|
+
export function sanitizeValueSetName(name) {
|
|
60
60
|
// Remove common prefixes
|
|
61
61
|
let sanitized = name.replace(/^ValueSet[-_]?/i, '');
|
|
62
62
|
// Split on non-alphanumeric and capitalize each part
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4 StructureDefinitions (profiles) directly inside package archives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "out/main.js",
|
|
@@ -36,10 +36,30 @@
|
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"fhir",
|
|
39
|
-
"typescript"
|
|
39
|
+
"typescript",
|
|
40
|
+
"code-generation",
|
|
41
|
+
"healthcare",
|
|
42
|
+
"hl7",
|
|
43
|
+
"fhir-profiles",
|
|
44
|
+
"implementation-guide",
|
|
45
|
+
"validation",
|
|
46
|
+
"fhir-r4",
|
|
47
|
+
"fhir-to-typescript"
|
|
40
48
|
],
|
|
41
49
|
"author": "Maximilian Nussbaumer",
|
|
42
50
|
"license": "ISC",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/quotentiroler/BabelFHIR-ts.git"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/quotentiroler/BabelFHIR-ts/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/quotentiroler/BabelFHIR-ts#readme",
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.0.0",
|
|
61
|
+
"npm": ">=9.0.0"
|
|
62
|
+
},
|
|
43
63
|
"devDependencies": {
|
|
44
64
|
"@eslint/js": "^9.24.0",
|
|
45
65
|
"@types/node": "^22.14.1",
|