babelfhir-ts 1.0.30 → 1.0.32
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 +19 -10
- package/out/generator/index.js +8 -11
- package/out/generator/validatorGenerator.js +3 -3
- package/package.json +4 -5
- package/out/generator/testGenerator.js +0 -71
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ Download and process a package directly from a registry (defaults to `https://pa
|
|
|
58
58
|
babelfhir-ts --package hl7.fhir.us.core@8.0.0
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Download,
|
|
61
|
+
Download, process and install a processed package into your current project:
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
babelfhir-ts install hl7.fhir.us.core@8.0.0
|
|
@@ -132,7 +132,7 @@ babelfhir-ts [options] [<input> [output]]
|
|
|
132
132
|
|
|
133
133
|
- **Directory** – scan all `.tgz`, `.zip`, or `.json` files inside the folder
|
|
134
134
|
- **Archive** – process a FHIR NPM package in `.tgz` or `.zip` format
|
|
135
|
-
- **StructureDefinition JSON** – generate code for a single profile
|
|
135
|
+
- **StructureDefinition JSON** – generate code for a single profile definition
|
|
136
136
|
|
|
137
137
|
## Scripts for contributors
|
|
138
138
|
|
|
@@ -141,7 +141,7 @@ babelfhir-ts [options] [<input> [output]]
|
|
|
141
141
|
| `npm run generate` | Execute the CLI against the local `input/` folder and refresh `output/`. |
|
|
142
142
|
| `npm run generate:check` | End-to-end check: generate, type-check, and lint the emitted output. |
|
|
143
143
|
| `npm test` | Type-check and run all Vitest suites (coverage enabled). |
|
|
144
|
-
| `npm
|
|
144
|
+
| `npm test validatorParity` | Run validator parity tests against Firely .NET SDK validator. |
|
|
145
145
|
|
|
146
146
|
## Caching notes
|
|
147
147
|
|
|
@@ -165,12 +165,21 @@ BabelFHIR-TS is a code generation tool that parses FHIR StructureDefinitions and
|
|
|
165
165
|
|
|
166
166
|
### Validation Scope
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
- **
|
|
168
|
+
The generated `validate()` methods DO check:
|
|
169
|
+
- **FHIRPath constraints** from StructureDefinition invariants
|
|
170
|
+
- **Cardinality rules** (min/max occurrences)
|
|
171
|
+
- **Required fields** from profiles
|
|
172
|
+
- **Pattern constraints** (patternCodeableConcept, patternCoding)
|
|
173
|
+
- **Slice validation** for common patterns (coding arrays, BackboneElement slices)
|
|
174
|
+
- **Data type correctness** (string, number, boolean, etc.)
|
|
175
|
+
|
|
176
|
+
The generated validators DO NOT check:
|
|
177
|
+
- **Terminology validation** (ValueSet expansion, code system membership) - requires terminology server
|
|
178
|
+
- **Reference resolution** (checking that referenced resources exist) - requires FHIR server
|
|
179
|
+
- **Complex discriminator types** (type, profile, exists, position) - only pattern/value discriminators supported
|
|
180
|
+
- **Cross-resource business rules** - application-specific logic
|
|
181
|
+
|
|
182
|
+
For comprehensive conformance testing, use the official [HL7 FHIR Validator](https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator) or [Firely .NET SDK](https://fire.ly/products/firely-net-sdk/).
|
|
174
183
|
|
|
175
184
|
### TypeScript Limitations
|
|
176
185
|
|
|
@@ -188,7 +197,7 @@ BabelFHIR-TS is a code generation tool that parses FHIR StructureDefinitions and
|
|
|
188
197
|
|
|
189
198
|
Use `random()` for development, testing, and prototyping, but always validate production data on the FHIR server side.
|
|
190
199
|
|
|
191
|
-
- **`validate()` parity**: The generated validation methods execute FHIRPath expressions and check constraints from StructureDefinitions, but are **only guaranteed to match** the Firely .NET SDK validator for scenarios covered by our test suite (see GitHub actions). Edge cases, complex slicing patterns, or profiles not in our test pipeline may produce different results. For production conformance testing, use the [HL7 FHIR Validator](https://
|
|
200
|
+
- **`validate()` parity**: The generated validation methods execute FHIRPath expressions and check constraints from StructureDefinitions, but are **only guaranteed to match** the Firely .NET SDK validator for scenarios covered by our test suite (see GitHub actions). Edge cases, complex slicing patterns, or profiles not in our test pipeline may produce different results. For production conformance testing, use the official [HL7 FHIR Validator](https://confluence.hl7.org/display/FHIR/Using+the+FHIR+Validator) or [Firely .NET SDK](https://fire.ly/products/firely-net-sdk/) as the source of truth.
|
|
192
201
|
|
|
193
202
|
### FHIR Version Support
|
|
194
203
|
|
package/out/generator/index.js
CHANGED
|
@@ -7,7 +7,6 @@ import { extractPackage, readStructureDefinitionsFromDir, createPackageFromDir,
|
|
|
7
7
|
import { fetchStructureDefinitions, parseStructureDefinition, fetchStructureDefinition, registerLocalStructureDefinitions, buildFhirChildTypeMap } from './sdParser.js';
|
|
8
8
|
import { writeInterfaceAndValidatorToFile, mergeFields, alignFields, ensureDirectoryExists, writeToFile, getBaseResource, downloadFile, sanitizeIdentifier, toPascalCase } from './utils.js';
|
|
9
9
|
import { generateValidateProfileFunction } from './validatorGenerator.js';
|
|
10
|
-
import { loadExamplesFromPackage, generateTestFile } from './testGenerator.js';
|
|
11
10
|
import { generateValueSetTypeScript, generateValueSetRegistry } from './valueSetGenerator.js';
|
|
12
11
|
import { generateRandomSupportContent } from './randomSupportGenerator.js';
|
|
13
12
|
import { logger } from '../logger.js';
|
|
@@ -177,7 +176,7 @@ async function generateIndexFile(dir) {
|
|
|
177
176
|
}
|
|
178
177
|
// Reusable processor for a single StructureDefinition
|
|
179
178
|
async function processStructureDefinition(sd, ctx) {
|
|
180
|
-
const { outputDir, fhirSourceHint, valueSetCodesMap, valueSets,
|
|
179
|
+
const { outputDir, fhirSourceHint, valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags, fhirChildTypeMap } = ctx;
|
|
181
180
|
const rawInterfaceName = sd.name || sd.id || 'UnnamedInterface';
|
|
182
181
|
let interfaceName = toPascalCase(rawInterfaceName);
|
|
183
182
|
// Check if the interface name conflicts with a FHIR base type
|
|
@@ -234,7 +233,7 @@ async function processStructureDefinition(sd, ctx) {
|
|
|
234
233
|
if (externalSD) {
|
|
235
234
|
try {
|
|
236
235
|
logger.log(`Processing external profile as StructureDefinition: ${normalizedTypeName}`);
|
|
237
|
-
const actualInterfaceName = await processStructureDefinition(externalSD, { outputDir, fhirSourceHint: profileInfo.profileUrl, valueSetCodesMap, valueSets,
|
|
236
|
+
const actualInterfaceName = await processStructureDefinition(externalSD, { outputDir, fhirSourceHint: profileInfo.profileUrl, valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags, fhirChildTypeMap });
|
|
238
237
|
// If the actual generated interface name differs from what we expected,
|
|
239
238
|
// add a mapping so other profiles can find it
|
|
240
239
|
if (actualInterfaceName !== normalizedTypeName) {
|
|
@@ -402,10 +401,6 @@ async function processStructureDefinition(sd, ctx) {
|
|
|
402
401
|
// Only generate class files if --no-classes flag is not set
|
|
403
402
|
if (!flags?.noClasses) {
|
|
404
403
|
writeToFile(path.join(outputDir, `${interfaceName}Class.ts`), classContent);
|
|
405
|
-
if (examplesMap && examplesMap[interfaceName]) {
|
|
406
|
-
const { valid, invalid } = examplesMap[interfaceName];
|
|
407
|
-
generateTestFile(outputDir, interfaceName, valid, invalid);
|
|
408
|
-
}
|
|
409
404
|
}
|
|
410
405
|
console.log(`Generated artifacts for ${interfaceName}`);
|
|
411
406
|
return interfaceName;
|
|
@@ -482,9 +477,8 @@ export async function generate(fhirSource, outputDir, flags) {
|
|
|
482
477
|
}
|
|
483
478
|
}
|
|
484
479
|
logger.log(`Profile registries pre-populated: ${profileUrlToName.size} URLs, ${profileIdToName.size} IDs`);
|
|
485
|
-
const examples = loadExamplesFromPackage(fhirSource);
|
|
486
480
|
for (const sd of structureDefinitions) {
|
|
487
|
-
await processStructureDefinition(sd, { outputDir, fhirSourceHint: fhirSource, valueSetCodesMap, valueSets,
|
|
481
|
+
await processStructureDefinition(sd, { outputDir, fhirSourceHint: fhirSource, valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags, fhirChildTypeMap });
|
|
488
482
|
}
|
|
489
483
|
}
|
|
490
484
|
/**
|
|
@@ -585,9 +579,8 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
585
579
|
logger.log(`Profile registries pre-populated: ${profileUrlToName.size} URLs, ${profileIdToName.size} IDs`);
|
|
586
580
|
// Register all local StructureDefinitions for resolution before HTTP fetches
|
|
587
581
|
registerLocalStructureDefinitions(structureDefinitions);
|
|
588
|
-
const examples = loadExamplesFromPackage(extractedRoot);
|
|
589
582
|
for (const sd of structureDefinitions) {
|
|
590
|
-
await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets,
|
|
583
|
+
await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags });
|
|
591
584
|
}
|
|
592
585
|
// Create package.json in generated folder
|
|
593
586
|
const originalPackageJsonPath = path.join(extractedRoot, 'package', 'package.json');
|
|
@@ -637,6 +630,10 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
637
630
|
// Compile TypeScript to JavaScript
|
|
638
631
|
logger.log('Compiling TypeScript to JavaScript...');
|
|
639
632
|
await compileTypeScriptToJS(outputDir);
|
|
633
|
+
// Copy package.json to root of extractedRoot so it's at the tarball root
|
|
634
|
+
const rootPackageJsonPath = path.join(extractedRoot, 'package.json');
|
|
635
|
+
fs.copyFileSync(generatedPackageJsonPath, rootPackageJsonPath);
|
|
636
|
+
logger.log('Copied package.json to tarball root');
|
|
640
637
|
const finalArchive = outArchivePath || deriveOutputArchiveName(packageArchivePath);
|
|
641
638
|
await createPackageFromDir(extractedRoot, finalArchive);
|
|
642
639
|
return finalArchive;
|
|
@@ -674,10 +674,10 @@ export function generateValidateProfileFunction(interfaceName, fields // Array o
|
|
|
674
674
|
// Only import fhirpath and run evaluation logic when there are constraints.
|
|
675
675
|
if (!hasConstraints) {
|
|
676
676
|
return `
|
|
677
|
-
export async function validate${interfaceName}(resource: ${interfaceName}): Promise<{ errors: string[]
|
|
677
|
+
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}`;
|
|
678
678
|
}
|
|
679
|
-
const fhirpathImport = filteredConstraints.length > 0 ? 'import fhirpath from "fhirpath";\nimport fhirpath_r4_model from "fhirpath/fhir-context/r4";\n\n' : '';
|
|
680
|
-
return `${fhirpathImport}export async function validate${interfaceName}(resource: ${interfaceName}): Promise<{ errors: string[]
|
|
679
|
+
const fhirpathImport = filteredConstraints.length > 0 ? 'import fhirpath from "fhirpath";\nimport fhirpath_r4_model from "fhirpath/fhir-context/r4/index.js";\n\n' : '';
|
|
680
|
+
return `${fhirpathImport}export async function validate${interfaceName}(resource: ${interfaceName}): Promise<{ errors: string[]; warnings: string[] }> {
|
|
681
681
|
const errors: string[] = [];
|
|
682
682
|
const warnings: string[] = [];
|
|
683
683
|
${validationLogic}${patternValidations.join('')}${fixedValueValidations.join('')}${sliceValidations.join('')}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
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",
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"generate": "tsx src/main.ts",
|
|
32
32
|
"generate:check": "npm run generate && npm run typecheck && npm run typecheck:output && npm run lint:output",
|
|
33
33
|
"generate:fhir-module": "node scripts/generate-fhir-r4-module.js",
|
|
34
|
-
"download-validator": "tsx scripts/download-validator.ts",
|
|
35
34
|
"prepublishOnly": "npm run build"
|
|
36
35
|
},
|
|
37
36
|
"keywords": [
|
|
@@ -50,12 +49,12 @@
|
|
|
50
49
|
"license": "ISC",
|
|
51
50
|
"repository": {
|
|
52
51
|
"type": "git",
|
|
53
|
-
"url": "https://github.com/quotentiroler/BabelFHIR-
|
|
52
|
+
"url": "https://github.com/quotentiroler/BabelFHIR-TS.git"
|
|
54
53
|
},
|
|
55
54
|
"bugs": {
|
|
56
|
-
"url": "https://github.com/quotentiroler/BabelFHIR-
|
|
55
|
+
"url": "https://github.com/quotentiroler/BabelFHIR-TS/issues"
|
|
57
56
|
},
|
|
58
|
-
"homepage": "https://github.com/quotentiroler/BabelFHIR-
|
|
57
|
+
"homepage": "https://github.com/quotentiroler/BabelFHIR-TS#readme",
|
|
59
58
|
"engines": {
|
|
60
59
|
"node": ">=18.0.0",
|
|
61
60
|
"npm": ">=9.0.0"
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import { logger } from "../logger.js";
|
|
4
|
-
export function loadExamplesFromPackage(packagePath) {
|
|
5
|
-
const examplesDir = path.join(packagePath, "examples");
|
|
6
|
-
const resourceMap = {};
|
|
7
|
-
if (!fs.existsSync(examplesDir)) {
|
|
8
|
-
console.warn(`No examples directory found in package: ${examplesDir}`);
|
|
9
|
-
return resourceMap;
|
|
10
|
-
}
|
|
11
|
-
const files = fs.readdirSync(examplesDir);
|
|
12
|
-
files.forEach((file) => {
|
|
13
|
-
const match = file.match(/^(Example|Invalid)(\w+)\.json$/);
|
|
14
|
-
if (match) {
|
|
15
|
-
const [, type, resourceName] = match;
|
|
16
|
-
if (!resourceMap[resourceName]) {
|
|
17
|
-
resourceMap[resourceName] = { valid: [], invalid: [] };
|
|
18
|
-
}
|
|
19
|
-
if (type === "Example") {
|
|
20
|
-
resourceMap[resourceName].valid.push(path.join(examplesDir, file));
|
|
21
|
-
}
|
|
22
|
-
else if (type === "Invalid") {
|
|
23
|
-
resourceMap[resourceName].invalid.push(path.join(examplesDir, file));
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
return resourceMap;
|
|
28
|
-
}
|
|
29
|
-
export function generateTestFile(outputDir, resourceName, validExamples, invalidExamples) {
|
|
30
|
-
const className = `${resourceName}Class`;
|
|
31
|
-
const testFileName = `${resourceName}.test.ts`;
|
|
32
|
-
const testFilePath = path.join(outputDir, testFileName);
|
|
33
|
-
const testContent = `
|
|
34
|
-
import { describe, it, expect } from "vitest";
|
|
35
|
-
import { ${className} } from "../../generated/${className}";
|
|
36
|
-
import fs from "fs";
|
|
37
|
-
import path from "path";
|
|
38
|
-
|
|
39
|
-
function loadExample(filePath: string) {
|
|
40
|
-
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
41
|
-
return JSON.parse(fileContent);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe("${className} Validation", () => {
|
|
45
|
-
${validExamples
|
|
46
|
-
.map((examplePath) => `
|
|
47
|
-
it("should validate ${path.basename(examplePath)} without errors", async () => {
|
|
48
|
-
const resource = loadExample("${examplePath.replace(/\\/g, "/")}");
|
|
49
|
-
const instance = new ${className}(resource);
|
|
50
|
-
const errors = await instance.validate();
|
|
51
|
-
expect(errors.errors).toEqual([]);
|
|
52
|
-
});
|
|
53
|
-
`)
|
|
54
|
-
.join("\n")}
|
|
55
|
-
|
|
56
|
-
${invalidExamples
|
|
57
|
-
.map((examplePath) => `
|
|
58
|
-
it("should validate ${path.basename(examplePath)} with errors", async () => {
|
|
59
|
-
const resource = loadExample("${examplePath.replace(/\\/g, "/")}");
|
|
60
|
-
const instance = new ${className}(resource);
|
|
61
|
-
const { errors } = await instance.validate();
|
|
62
|
-
expect(errors.length).toBeGreaterThan(0);
|
|
63
|
-
});
|
|
64
|
-
`)
|
|
65
|
-
.join("\n")}
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
`;
|
|
69
|
-
fs.writeFileSync(testFilePath, testContent);
|
|
70
|
-
logger.log(`Generated test file: ${testFilePath}`);
|
|
71
|
-
}
|