Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 3 additions & 25 deletions examples/on-the-fly/kbv-r4/generate.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,12 @@
import type { PreprocessContext } from "@atomic-ehr/fhir-canonical-manager";
import { APIBuilder, prettyReport } from "../../../src/api/builder";

const preprocessPackage = (ctx: PreprocessContext): PreprocessContext => {
if (ctx.kind !== "package") return ctx;
const json = ctx.packageJson;
const name = json.name as string;

// de.basisprofil.r4 doesn't declare hl7.fhir.r4.core as a dependency
if (name === "de.basisprofil.r4") {
const deps = (json.dependencies as Record<string, string>) || {};
if (!deps["hl7.fhir.r4.core"]) {
return {
...ctx,
kind: "package",
packageJson: {
...json,
dependencies: { ...deps, "hl7.fhir.r4.core": "4.0.1" },
},
};
}
}

return ctx;
};
import { injectDependency, inPackage } from "../../../src/api/patches";

if (require.main === module) {
console.log("Generating KBV R4 types...");

const builder = new APIBuilder({
preprocessPackage,
// de.basisprofil.r4 references core types without declaring hl7.fhir.r4.core.
patches: { packageJson: [inPackage("de.basisprofil.r4", [injectDependency({ "hl7.fhir.r4.core": "4.0.1" })])] },
registry: "https://packages.simplifier.net",
ignorePackageIndex: true,
})
Expand Down
99 changes: 34 additions & 65 deletions examples/on-the-fly/norge-r4/generate.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,45 @@
import type { PreprocessContext } from "@atomic-ehr/fhir-canonical-manager";
import { APIBuilder, prettyReport } from "../../../src/api/builder";
import {
injectDependency,
inPackage,
inResource,
renamePackage,
renameReferenceTarget,
} from "../../../src/api/patches";

// Fix known package name typos (in-memory transformation)
const packageNameFixes: Record<string, string> = {
"simplifier.core.r4.rResources": "simplifier.core.r4.resources",
};
const CORE_PACKAGE = "hl7.fhir.r4.core";

// Packages that need hl7.fhir.r4.core dependency injected
const needsCoreDependency = (name: string): boolean => {
return (
name.startsWith("simplifier.core.r4.") ||
name === "simplifier.core.r4" ||
name.startsWith("hl7.fhir.no.") ||
name.startsWith("ehelse.fhir.no.") ||
name.startsWith("nhn.fhir.no.") ||
name.startsWith("sfm.")
);
};

const preprocessPackage = (ctx: PreprocessContext): PreprocessContext => {
// GdRelatedPerson widens patient reference to include Person, but the
// base R4 RelatedPerson.patient only allows Patient. Drop the Person targets.
if (ctx.kind === "resource") {
const res = ctx.resource as { url?: string };
if (res.url === "http://ehelse.no/fhir/StructureDefinition/gd-RelatedPerson") {
let str = JSON.stringify(ctx.resource);
str = str.replaceAll(
"http://hl7.org/fhir/StructureDefinition/Person",
"http://hl7.org/fhir/StructureDefinition/Patient",
);
str = str.replaceAll(
"http://hl7.no/fhir/StructureDefinition/no-basis-Person",
"http://hl7.org/fhir/StructureDefinition/Patient",
);
str = str.replaceAll(
"http://ehelse.no/fhir/StructureDefinition/gd-Person",
"http://hl7.org/fhir/StructureDefinition/Patient",
);
return { ...ctx, resource: JSON.parse(str) };
}
return ctx;
}
let json = ctx.packageJson;
const name = json.name as string;

// Fix package name typos
const fixedName = packageNameFixes[name];
if (fixedName) {
console.log(`Fixed package name: ${name} -> ${fixedName}`);
json = { ...json, name: fixedName };
}

// Add missing core dependency to packages that don't properly declare it
if (needsCoreDependency(name)) {
const deps = (json.dependencies as Record<string, string>) || {};
if (!deps["hl7.fhir.r4.core"]) {
console.log(`Injecting hl7.fhir.r4.core dependency into ${name}`);
json = {
...json,
dependencies: { ...deps, "hl7.fhir.r4.core": "4.0.1" },
};
}
}

return { ...ctx, kind: "package", packageJson: json };
};
// True for every package except core itself — injectDependency is a no-op when the
// dependency is already declared, so this only needs to keep core from depending on itself.
const needsCoreDependency = (name: string): boolean => name !== CORE_PACKAGE;

if (require.main === module) {
console.log("Generating Norge R4 types...");

const builder = new APIBuilder({
preprocessPackage,
patches: {
packageJson: [
// Many Norge packages reference core types without declaring the dependency;
// inject it wherever it's missing.
inPackage((pkg) => needsCoreDependency(pkg.name), [injectDependency({ [CORE_PACKAGE]: "4.0.1" })]),
// Fix known package name typo.
renamePackage({ "simplifier.core.r4.rResources": "simplifier.core.r4.resources" }),
],
// gd-RelatedPerson widens patient to include Person, but base R4 RelatedPerson.patient
// only allows Patient — narrow the Person targets back to Patient.
fhirResource: [
inResource("http://ehelse.no/fhir/StructureDefinition/gd-RelatedPerson", [
renameReferenceTarget({
"http://hl7.org/fhir/StructureDefinition/Person":
"http://hl7.org/fhir/StructureDefinition/Patient",
"http://hl7.no/fhir/StructureDefinition/no-basis-Person":
"http://hl7.org/fhir/StructureDefinition/Patient",
"http://ehelse.no/fhir/StructureDefinition/gd-Person":
"http://hl7.org/fhir/StructureDefinition/Patient",
}),
]),
],
},
registry: "https://packages.simplifier.net",
})
.fromPackage("hl7.fhir.r4.core", "4.0.1")
Expand Down
72 changes: 26 additions & 46 deletions examples/typescript-ccda/generate.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,41 @@
// Run this script using Bun CLI with:
// bun run scripts/generate-fhir-types.ts

import { CanonicalManager, type PreprocessContext } from "@atomic-ehr/fhir-canonical-manager";
import { CanonicalManager } from "@atomic-ehr/fhir-canonical-manager";
import { registerFromManager } from "@root/typeschema/register";
import { APIBuilder, prettyReport } from "../../src/api/builder";

const preprocessPackage = (ctx: PreprocessContext): PreprocessContext => {
if (ctx.kind !== "resource") return ctx;
if (ctx.package.name === "hl7.cda.uv.core") {
let str = JSON.stringify(ctx.resource);
str = str.replaceAll(
"http://hl7.org/cda/stds/core/StructureDefinition/IVL_TS",
"http://hl7.org/cda/stds/core/StructureDefinition/IVL-TS",
);
return { ...ctx, resource: JSON.parse(str) };
}
// CarePlanAct profile binds moodCode to an external NLM ValueSet that
// isn't available in any loaded package. Reuse the base Act binding.
if (ctx.package.name === "hl7.cda.us.ccda") {
const res = ctx.resource as { url?: string };
if (res.url === "http://hl7.org/cda/us/ccda/StructureDefinition/CarePlanAct") {
let str = JSON.stringify(ctx.resource);
str = str.replaceAll(
"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.37",
"http://terminology.hl7.org/ValueSet/v3-xDocumentActMood",
);
return { ...ctx, resource: JSON.parse(str) };
}
}
// The bundle-type CodeSystem is missing codes "bundle" and
// "subscription-notification" used by BatchBundle and
// SubscriptionNotificationBundle profiles. Patch all instances since
// resolveAny may pick any package's copy of the CodeSystem.
const res = ctx.resource as { url?: string; concept?: { code: string }[] };
if (res.url === "http://hl7.org/fhir/bundle-type" && res.concept) {
const existing = new Set(res.concept.map((c) => c.code));
const missing = ["bundle", "subscription-notification"].filter((c) => !existing.has(c));
if (missing.length > 0) {
return {
...ctx,
resource: {
...ctx.resource,
concept: [...res.concept, ...missing.map((code) => ({ code }))],
},
};
}
}
return ctx;
};
import { inPackage, inResource, patchCodeSystem, renameCanonical, swapBinding } from "../../src/api/patches";

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

const manager = CanonicalManager({
packages: [],
workingDir: ".codegen-cache/canonical-manager-cache",
preprocessPackage,
patches: {
fhirResource: [
// IVL_TS is a typo'd canonical in hl7.cda.uv.core (should be IVL-TS).
inPackage("hl7.cda.uv.core", [
renameCanonical({
"http://hl7.org/cda/stds/core/StructureDefinition/IVL_TS":
"http://hl7.org/cda/stds/core/StructureDefinition/IVL-TS",
}),
]),
// CarePlanAct binds moodCode to an external NLM ValueSet absent from every loaded
// package; reuse the base Act binding.
inPackage("hl7.cda.us.ccda", [
inResource("http://hl7.org/cda/us/ccda/StructureDefinition/CarePlanAct", [
swapBinding({
"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1267.37":
"http://terminology.hl7.org/ValueSet/v3-xDocumentActMood",
}),
]),
]),
// The bundle-type CodeSystem omits codes used by BatchBundle /
// SubscriptionNotificationBundle; patch every copy (resolveAny may pick any).
patchCodeSystem("http://hl7.org/fhir/bundle-type", ["bundle", "subscription-notification"]),
],
},
});

// Initialize manager with packages to discover CDA resources
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
},
"homepage": "https://github.com/atomic-ehr/codegen#readme",
"dependencies": {
"@atomic-ehr/fhir-canonical-manager": "0.0.24",
"@atomic-ehr/fhir-canonical-manager": "0.0.24-canary.20260608091810.e54bf2f",
"@atomic-ehr/fhirschema": "0.0.11",
"mustache": "^4.2.0",
"picocolors": "^1.1.1",
Expand Down
16 changes: 16 additions & 0 deletions src/api/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as Path from "node:path";
import {
CanonicalManager,
type LocalPackageConfig,
type Patches,
type PreprocessContext,
type TgzPackageConfig,
} from "@atomic-ehr/fhir-canonical-manager";
Expand All @@ -32,6 +33,13 @@ import * as Mustache from "./writer-generator/mustache";
import { TypeScript, type TypeScriptOptions } from "./writer-generator/typescript/writer";
import type { FileBuffer, FileSystemWriter, FileSystemWriterOptions, WriterOptions } from "./writer-generator/writer";

/**
* Per-phase patch handlers for the `APIBuilder` `patches` option. Each phase is a list of
* handlers (typically `inPackage`/`inResource` combinators), passed straight to the
* CanonicalManager `Patches` config.
*/
export type PatchesInput = Partial<Patches>;

/**
* Configuration options for the API builder
*/
Expand Down Expand Up @@ -171,6 +179,8 @@ export class APIBuilder {
userOpts: Partial<APIBuilderOptions> & {
manager?: ReturnType<typeof CanonicalManager>;
register?: Register;
/** Per-phase patch handlers passed to the CanonicalManager (package-defect fixes). */
patches?: PatchesInput;
preprocessPackage?: (context: PreprocessContext) => PreprocessContext;
ignorePackageIndex?: boolean;
logger?: CodegenLogManager;
Expand Down Expand Up @@ -213,10 +223,16 @@ export class APIBuilder {
workingDir: ".codegen-cache/canonical-manager-cache",
registry: userOpts.registry,
dropCache: userOpts.dropCanonicalManagerCache,
patches: userOpts.patches,
preprocessPackage: userOpts.preprocessPackage,
ignorePackageIndex: userOpts.ignorePackageIndex,
});
this.logger = userOpts.logger ?? mkLogger({ prefix: "api" });
// `patches` only apply to a CM that this builder constructs; an injected
// manager/register owns its own patch wiring.
if (userOpts.patches && (userOpts.manager || userOpts.register)) {
this.logger.warn("`patches` is ignored when a prebuilt `manager`/`register` is provided.");
}
this.options = opts;
}

Expand Down
Loading
Loading