Skip to content

Commit 0bb6c52

Browse files
authored
Merge pull request #176 from atomic-ehr/feat/phase1-patch-helpers
API/TypeSchema/CLI: declarative package-defect handling with presets (#128)
2 parents 714ca47 + 13ee4a1 commit 0bb6c52

23 files changed

Lines changed: 532 additions & 435 deletions

File tree

‎CLAUDE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ Reference example: `examples/typescript-r4-us-core/profile-r4-bodyweight.test.ts
160160
- Handles profiles and extensions (US Core in development)
161161
- Caches parsed schemas for performance
162162
- Multi-package dependency resolution via Canonical Manager
163+
- Package defects are fixed declaratively at the loader: `canonicalManager.patches` repairs raw package data and `excludeCanonical` drops known-broken canonicals at the index (helpers from `@atomic-ehr/fhir-canonical-manager/patch`; shipped `builtinPatches` in `src/api/builtin-patches.ts` apply to every builder-constructed loader unless `builtinPatches: false`) — never add ad-hoc workarounds in transformer code
163164

164165
### TypeSchema Format
165166
- Intermediate representation between FHIR and target languages

‎README.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,18 @@ The JSON `typescript` options also accept `moduleSpecifierStyle` (`"extensionles
153153

154154
All terminology fields are optional. When supplied, `enabled` must be a boolean, `packages` must be an array of strings, and `packageVerification` must contain string values. Unknown nested keys and invalid values are reported with their full configuration paths.
155155

156+
### Fixing defective packages
157+
158+
Real FHIR packages ship defects — missing dependencies, typo'd names and canonicals, bindings to unavailable ValueSets, incomplete CodeSystems. Fixes are declared, not hand-coded:
159+
160+
- **Canonical exclusions** — drop a known-broken canonical at the package index, before codegen sees it: `canonicalManager: { patches: { indexEntry: [excludeCanonical({ package, url, reason })] } }`. Codegen ships `builtinPatches` (`src/api/builtin-patches.ts` — generation-breaking content in HL7's own packages, e.g. `hl7.fhir.r5.core` profiles that break R4-compatible generation); they apply to every loader the builder constructs, on top of your own patches, and `builtinPatches: false` is the explicit opt-out. Index exclusion removes the canonical from resolution too, so exclude whole derivation chains together — and it is applied at scan time, so drop the loader cache after changing the list. A hand-built CanonicalManager owns its wiring: apply `builtinPatches.indexEntry` explicitly (see the ccda example).
161+
- **Custom patches** — per-phase handlers built from the helpers on the `@atomic-ehr/fhir-canonical-manager/patch` subpath (`ensureDependency`, `renamePackage`, `replaceText`, `ensureCodes`, scoped by `inPackage`/`inResource`), passed as `new APIBuilder({ canonicalManager: { patches: {...} } })`. In the CLI config, `"options": { "forceDependencies": {...} }` pins declared dependency versions across the closure.
162+
- **Broken package index** — `canonicalManager: { packageIndex: "recover" }` heals a corrupt `.index.json` by falling back to a directory scan (always with a warning); `"regenerate"` ignores the shipped index entirely.
163+
164+
Loader settings (`registry`, `packageIndex`, `dropCache`, `patches`) live under the builder's `canonicalManager` option — they configure the CanonicalManager package loader, not the generator. The old flat options are deprecated and warn.
165+
166+
Applied fixes surface in the generation report's "Input fixes" section.
167+
156168
### Usage Examples
157169

158170
See the [examples/](examples/) directory for working demonstrations:

‎bun.lock‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎examples/on-the-fly/ccda/generate.ts‎

Lines changed: 35 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,50 @@
11
// Run this script using Bun CLI with:
22
// bun run scripts/generate-fhir-types.ts
33

4-
import { CanonicalManager, type PreprocessContext } from "@atomic-ehr/fhir-canonical-manager";
4+
import { CanonicalManager } from "@atomic-ehr/fhir-canonical-manager";
5+
import { ensureCodes, inPackage, inResource, replaceText } from "@atomic-ehr/fhir-canonical-manager/patch";
56
import { registerFromManager } from "@root/typeschema/register";
67
import { APIBuilder, prettyReport } from "../../../src/api/builder";
7-
8-
const preprocessPackage = (ctx: PreprocessContext): PreprocessContext => {
9-
if (ctx.kind !== "resource") return ctx;
10-
if (ctx.package.name === "hl7.cda.uv.core") {
11-
let str = JSON.stringify(ctx.resource);
12-
str = str.replaceAll(
13-
"http://hl7.org/cda/stds/core/StructureDefinition/IVL_TS",
14-
"http://hl7.org/cda/stds/core/StructureDefinition/IVL-TS",
15-
);
16-
return { ...ctx, resource: JSON.parse(str) };
17-
}
18-
// CarePlanAct profile binds moodCode to an external NLM ValueSet that
19-
// isn't available in any loaded package. Reuse the base Act binding.
20-
if (ctx.package.name === "hl7.cda.us.ccda") {
21-
const res = ctx.resource as { url?: string };
22-
if (res.url === "http://hl7.org/cda/us/ccda/StructureDefinition/CarePlanAct") {
23-
let str = JSON.stringify(ctx.resource);
24-
str = str.replaceAll(
25-
"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.37",
26-
"http://terminology.hl7.org/ValueSet/v3-xDocumentActMood",
27-
);
28-
return { ...ctx, resource: JSON.parse(str) };
29-
}
30-
}
31-
// The bundle-type CodeSystem is missing codes "bundle" and
32-
// "subscription-notification" used by BatchBundle and
33-
// SubscriptionNotificationBundle profiles. Patch all instances since
34-
// resolveAny may pick any package's copy of the CodeSystem.
35-
const res = ctx.resource as { url?: string; concept?: { code: string }[] };
36-
if (res.url === "http://hl7.org/fhir/bundle-type" && res.concept) {
37-
const existing = new Set(res.concept.map((c) => c.code));
38-
const missing = ["bundle", "subscription-notification"].filter((c) => !existing.has(c));
39-
if (missing.length > 0) {
40-
return {
41-
...ctx,
42-
resource: {
43-
...ctx.resource,
44-
concept: [...res.concept, ...missing.map((code) => ({ code }))],
45-
},
46-
};
47-
}
48-
}
49-
return ctx;
50-
};
8+
import { builtinPatches } from "../../../src/api/builtin-patches";
519

5210
if (require.main === module) {
5311
console.log("📦 Generating CCDA Types...");
5412

5513
const manager = CanonicalManager({
5614
packages: [],
5715
workingDir: ".codegen-cache/canonical-manager-cache",
58-
preprocessPackage,
16+
patches: {
17+
// The builder injects builtinPatches only into loaders it constructs itself.
18+
// This manager is built by hand (we need the register up front to select the
19+
// CDA logical models for promoteLogical), and CM patches are constructor-time
20+
// configuration that cannot be attached afterwards — so the shipped input
21+
// fixes are applied explicitly here.
22+
indexEntry: builtinPatches.indexEntry,
23+
fhirResource: [
24+
// IVL_TS is a typo'd canonical in hl7.cda.uv.core (should be IVL-TS).
25+
inPackage("hl7.cda.uv.core", [
26+
replaceText(
27+
"http://hl7.org/cda/stds/core/StructureDefinition/IVL_TS",
28+
"http://hl7.org/cda/stds/core/StructureDefinition/IVL-TS",
29+
),
30+
]),
31+
// CarePlanAct binds moodCode to an external NLM ValueSet absent from every loaded
32+
// package; reuse the base Act binding.
33+
inPackage("hl7.cda.us.ccda", [
34+
inResource("http://hl7.org/cda/us/ccda/StructureDefinition/CarePlanAct", [
35+
replaceText(
36+
"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.37",
37+
"http://terminology.hl7.org/ValueSet/v3-xDocumentActMood",
38+
),
39+
]),
40+
]),
41+
// The resolved (R4) bundle-type CodeSystem lacks codes pinned by the R5
42+
// bundle profiles; patch every copy (resolveAny may pick any).
43+
// "subscription-notification" is a real R5 code; "bundle" is an R5 spec typo
44+
// for "batch" — FIXME: repair batch-bundle's patternCode instead (output-changing).
45+
ensureCodes("http://hl7.org/fhir/bundle-type", ["bundle", "subscription-notification"]),
46+
],
47+
},
5948
});
6049

6150
// Initialize manager with packages to discover CDA resources

‎examples/on-the-fly/kbv-condition-diagnosis/generate.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ if (require.main === module) {
1111
console.log("Generating KBV Condition Diagnosis types...");
1212

1313
const builder = new APIBuilder({
14-
registry: "https://packages.simplifier.net",
15-
ignorePackageIndex: true,
14+
canonicalManager: {
15+
registry: "https://packages.simplifier.net",
16+
packageIndex: "regenerate",
17+
},
1618
})
1719
.fromPackage("kbv.basis", "1.9.0")
1820
.throwException()

‎examples/on-the-fly/kbv-r4/generate.ts‎

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,19 @@
1-
import type { PreprocessContext } from "@atomic-ehr/fhir-canonical-manager";
1+
import { ensureDependency, inPackage } from "@atomic-ehr/fhir-canonical-manager/patch";
22
import { APIBuilder, prettyReport } from "../../../src/api/builder";
33

4-
const preprocessPackage = (ctx: PreprocessContext): PreprocessContext => {
5-
if (ctx.kind !== "package") return ctx;
6-
const json = ctx.packageJson;
7-
const name = json.name as string;
8-
9-
// de.basisprofil.r4 doesn't declare hl7.fhir.r4.core as a dependency
10-
if (name === "de.basisprofil.r4") {
11-
const deps = (json.dependencies as Record<string, string>) || {};
12-
if (!deps["hl7.fhir.r4.core"]) {
13-
return {
14-
...ctx,
15-
kind: "package",
16-
packageJson: {
17-
...json,
18-
dependencies: { ...deps, "hl7.fhir.r4.core": "4.0.1" },
19-
},
20-
};
21-
}
22-
}
23-
24-
return ctx;
25-
};
26-
274
if (require.main === module) {
285
console.log("Generating KBV R4 types...");
296

307
const builder = new APIBuilder({
31-
preprocessPackage,
32-
registry: "https://packages.simplifier.net",
33-
ignorePackageIndex: true,
8+
canonicalManager: {
9+
registry: "https://packages.simplifier.net",
10+
// de.basisprofil.r4 ships a broken .index.json; heal it with a directory scan.
11+
packageIndex: "recover",
12+
// de.basisprofil.r4 references core types without declaring hl7.fhir.r4.core.
13+
patches: {
14+
packageJson: [inPackage("de.basisprofil.r4", [ensureDependency({ "hl7.fhir.r4.core": "4.0.1" })])],
15+
},
16+
},
3417
})
3518
.fromPackage("kbv.ita.for", "1.3.1")
3619
.fromPackage("de.basisprofil.r4", "1.6.0-ballot2")

‎examples/on-the-fly/norge-r4/generate.ts‎

Lines changed: 31 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,40 @@
1-
import type { PreprocessContext } from "@atomic-ehr/fhir-canonical-manager";
1+
import { ensureDependency, inResource, renamePackage, replaceText } from "@atomic-ehr/fhir-canonical-manager/patch";
22
import { APIBuilder, prettyReport } from "../../../src/api/builder";
33

4-
// Fix known package name typos (in-memory transformation)
5-
const packageNameFixes: Record<string, string> = {
6-
"simplifier.core.r4.rResources": "simplifier.core.r4.resources",
7-
};
8-
9-
// Packages that need hl7.fhir.r4.core dependency injected
10-
const needsCoreDependency = (name: string): boolean => {
11-
return (
12-
name.startsWith("simplifier.core.r4.") ||
13-
name === "simplifier.core.r4" ||
14-
name.startsWith("hl7.fhir.no.") ||
15-
name.startsWith("ehelse.fhir.no.") ||
16-
name.startsWith("nhn.fhir.no.") ||
17-
name.startsWith("sfm.")
18-
);
19-
};
20-
21-
const preprocessPackage = (ctx: PreprocessContext): PreprocessContext => {
22-
// GdRelatedPerson widens patient reference to include Person, but the
23-
// base R4 RelatedPerson.patient only allows Patient. Drop the Person targets.
24-
if (ctx.kind === "resource") {
25-
const res = ctx.resource as { url?: string };
26-
if (res.url === "http://ehelse.no/fhir/StructureDefinition/gd-RelatedPerson") {
27-
let str = JSON.stringify(ctx.resource);
28-
str = str.replaceAll(
29-
"http://hl7.org/fhir/StructureDefinition/Person",
30-
"http://hl7.org/fhir/StructureDefinition/Patient",
31-
);
32-
str = str.replaceAll(
33-
"http://hl7.no/fhir/StructureDefinition/no-basis-Person",
34-
"http://hl7.org/fhir/StructureDefinition/Patient",
35-
);
36-
str = str.replaceAll(
37-
"http://ehelse.no/fhir/StructureDefinition/gd-Person",
38-
"http://hl7.org/fhir/StructureDefinition/Patient",
39-
);
40-
return { ...ctx, resource: JSON.parse(str) };
41-
}
42-
return ctx;
43-
}
44-
let json = ctx.packageJson;
45-
const name = json.name as string;
46-
47-
// Fix package name typos
48-
const fixedName = packageNameFixes[name];
49-
if (fixedName) {
50-
console.log(`Fixed package name: ${name} -> ${fixedName}`);
51-
json = { ...json, name: fixedName };
52-
}
53-
54-
// Add missing core dependency to packages that don't properly declare it
55-
if (needsCoreDependency(name)) {
56-
const deps = (json.dependencies as Record<string, string>) || {};
57-
if (!deps["hl7.fhir.r4.core"]) {
58-
console.log(`Injecting hl7.fhir.r4.core dependency into ${name}`);
59-
json = {
60-
...json,
61-
dependencies: { ...deps, "hl7.fhir.r4.core": "4.0.1" },
62-
};
63-
}
64-
}
65-
66-
return { ...ctx, kind: "package", packageJson: json };
67-
};
68-
694
if (require.main === module) {
705
console.log("Generating Norge R4 types...");
716

727
const builder = new APIBuilder({
73-
preprocessPackage,
74-
registry: "https://packages.simplifier.net",
8+
canonicalManager: {
9+
registry: "https://packages.simplifier.net",
10+
patches: {
11+
packageJson: [
12+
// Many Norge packages reference core types without declaring the dependency;
13+
// make every package (except core itself) depend on core.
14+
ensureDependency({ "hl7.fhir.r4.core": "4.0.1" }),
15+
// Fix known package name typo.
16+
renamePackage("simplifier.core.r4.rResources", "simplifier.core.r4.resources"),
17+
],
18+
// gd-RelatedPerson widens patient to include Person, but base R4 RelatedPerson.patient
19+
// only allows Patient — narrow the Person targets back to Patient.
20+
fhirResource: [
21+
inResource("http://ehelse.no/fhir/StructureDefinition/gd-RelatedPerson", [
22+
replaceText(
23+
"http://hl7.org/fhir/StructureDefinition/Person",
24+
"http://hl7.org/fhir/StructureDefinition/Patient",
25+
),
26+
replaceText(
27+
"http://hl7.no/fhir/StructureDefinition/no-basis-Person",
28+
"http://hl7.org/fhir/StructureDefinition/Patient",
29+
),
30+
replaceText(
31+
"http://ehelse.no/fhir/StructureDefinition/gd-Person",
32+
"http://hl7.org/fhir/StructureDefinition/Patient",
33+
),
34+
]),
35+
],
36+
},
37+
},
7538
})
7639
.fromPackage("hl7.fhir.r4.core", "4.0.1")
7740
.fromPackage("ehelse.fhir.no.grunndata", "2.3.5")

‎examples/typescript-custom-packages/generate.ts‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as Path from "node:path";
22
import { fileURLToPath } from "node:url";
3+
import { excludeCanonical } from "@atomic-ehr/fhir-canonical-manager/patch";
34
import { APIBuilder, prettyReport } from "../../src/api";
45

56
const __dirname = Path.dirname(fileURLToPath(import.meta.url));
@@ -36,7 +37,28 @@ console.log(prettyReport(localReport));
3637
if (!localReport.success) process.exit(1);
3738

3839
// 2. Remote .tgz package by URL (.fromPackageRef) — SQL-on-FHIR → ./sql-on-fhir-types
39-
const sqlReport = await new APIBuilder()
40+
const sofReport = await new APIBuilder({
41+
// Instead of the shipped builtin patches, declare the known-broken R5 canonicals by hand —
42+
// this demonstrates (and continuously exercises) the fully manual loader configuration.
43+
// Index patches apply at scan time, so a cached closure needs a cache drop to pick them up.
44+
builtinPatches: false,
45+
canonicalManager: {
46+
patches: {
47+
indexEntry: [
48+
excludeCanonical({
49+
package: { name: "hl7.fhir.r5.core", version: "5.0.0" },
50+
url: "http://hl7.org/fhir/StructureDefinition/shareablecodesystem",
51+
reason: "Broken CodeSystem.concept.concept content (ElementReference).",
52+
}),
53+
excludeCanonical({
54+
package: { name: "hl7.fhir.r5.core", version: "5.0.0" },
55+
url: "http://hl7.org/fhir/StructureDefinition/publishablecodesystem",
56+
reason: "Uses R5-only base types not available in R4 generation.",
57+
}),
58+
],
59+
},
60+
},
61+
})
4062
.throwException()
4163
.typescript({ withDebugComment: false, generateProfile: false })
4264
// The IG references R5 core resources (e.g. ViewDefinition's base chain reaches Library)
@@ -55,7 +77,7 @@ const sqlReport = await new APIBuilder()
5577
.cleanOutput(true)
5678
.generate();
5779

58-
console.log(prettyReport(sqlReport));
59-
if (!sqlReport.success) process.exit(1);
80+
console.log(prettyReport(sofReport));
81+
if (!sofReport.success) process.exit(1);
6082

6183
console.log("✅ FHIR types generated successfully!");

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
},
4848
"homepage": "https://github.com/atomic-ehr/codegen#readme",
4949
"dependencies": {
50-
"@atomic-ehr/fhir-canonical-manager": "0.0.24",
50+
"@atomic-ehr/fhir-canonical-manager": "0.0.26",
5151
"@atomic-ehr/fhirschema": "0.0.14",
5252
"brace-expansion": "^5.0.9",
5353
"mustache": "^4.2.0",

0 commit comments

Comments
 (0)