From 534e83e23a15625beb340bf92e68a582023b9e9a Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 4 May 2026 15:01:52 +0200 Subject: [PATCH 1/4] TS: align typescript-us-core example with the tutorial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The companion blog post now demonstrates three codegen features the example wasn't exercising: - generate.ts: pass mkCodegenLogger with suppressTags (silences known noisy categories: #fieldTypeNotFound, #duplicateSchema, #duplicateCanonical, #largeValueSet) and replace the bespoke console.log success/failure prints with prettyReport(report) - profile-bp.test.ts: add a "filter a mixed collection with profile is()" demo that exercises USCoreBloodPressureProfile.is and USCorePatientProfile.is over a mixed unknown[], then hands survivors to from() for typed reads — mirrors the read-back pattern from the tutorial's Step 5 --- examples/typescript-us-core/generate.ts | 18 +++--- .../typescript-us-core/profile-bp.test.ts | 59 ++++++++++++++++++- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/examples/typescript-us-core/generate.ts b/examples/typescript-us-core/generate.ts index 306223426..7180bbf8e 100644 --- a/examples/typescript-us-core/generate.ts +++ b/examples/typescript-us-core/generate.ts @@ -1,12 +1,14 @@ // Run this script using Bun CLI with: // bun run scripts/generate-fhir-types.ts -import { APIBuilder } from "../../src/api/builder"; +import { APIBuilder, mkCodegenLogger, prettyReport } from "../../src/api"; if (require.main === module) { - console.log("📦 Generating US Core Types..."); + const logger = mkCodegenLogger({ + suppressTags: ["#fieldTypeNotFound", "#duplicateSchema", "#duplicateCanonical", "#largeValueSet"], + }); - const builder = new APIBuilder() + const builder = new APIBuilder({ logger }) .throwException() .fromPackage("hl7.fhir.us.core", "8.0.1") .typeSchema({ @@ -31,13 +33,7 @@ if (require.main === module) { .cleanOutput(true); const report = await builder.generate(); + console.log(prettyReport(report)); - // console.log(report); - - if (report.success) { - console.log("✅ FHIR US Core types generated successfully!"); - } else { - console.error("❌ FHIR US Core types generation failed."); - process.exit(1); - } + if (!report.success) process.exit(1); } diff --git a/examples/typescript-us-core/profile-bp.test.ts b/examples/typescript-us-core/profile-bp.test.ts index 9824d5550..651740082 100644 --- a/examples/typescript-us-core/profile-bp.test.ts +++ b/examples/typescript-us-core/profile-bp.test.ts @@ -4,7 +4,8 @@ import { describe, expect, test } from "bun:test"; import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; -import { USCoreBloodPressureProfile } from "./fhir-types/hl7-fhir-us-core/profiles"; +import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient"; +import { USCoreBloodPressureProfile, USCorePatientProfile } from "./fhir-types/hl7-fhir-us-core/profiles"; describe("demo: create a US Core blood pressure observation", () => { test("build a valid BP resource step by step", () => { @@ -121,6 +122,62 @@ describe("demo: read a US Core blood pressure observation from JSON", () => { }); }); +describe("demo: filter a mixed collection with profile is()", () => { + test("is() narrows by resourceType + meta.profile, no validation, no instance constructed", () => { + // A mixed bag: a US Core patient, a US Core BP observation, a non-profiled observation, and junk + const patient: Patient = { + resourceType: "Patient", + meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"] }, + identifier: [{ system: "http://hospital.example.org/mrn", value: "MRN-001" }], + name: [{ family: "Lovelace", given: ["Ada"] }], + }; + const bp: Observation = { + resourceType: "Observation", + meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure"] }, + status: "final", + category: [ + { + coding: [ + { code: "vital-signs", system: "http://terminology.hl7.org/CodeSystem/observation-category" }, + ], + }, + ], + code: { coding: [{ code: "85354-9", system: "http://loinc.org" }] }, + subject: { reference: "Patient/pt-1" }, + effectiveDateTime: "2024-06-15", + component: [ + { + code: { coding: [{ code: "8480-6", system: "http://loinc.org" }] }, + valueQuantity: { value: 120, unit: "mmHg" }, + }, + { + code: { coding: [{ code: "8462-4", system: "http://loinc.org" }] }, + valueQuantity: { value: 80, unit: "mmHg" }, + }, + ], + }; + const otherObs: Observation = { + resourceType: "Observation", + status: "final", + code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] }, + }; + const resources: unknown[] = [patient, bp, otherObs, { resourceType: "Patient" }, null, "not a resource"]; + + // Profile is() filters by resourceType AND meta.profile.includes(canonicalUrl) + const bps = resources.filter(USCoreBloodPressureProfile.is); + expect(bps).toEqual([bp]); + + // Same guard works for the Patient profile against the same input + const patients = resources.filter(USCorePatientProfile.is); + expect(patients).toEqual([patient]); + + // Hand survivors to from() to use typed getters + const wrapped = bps.map((o) => USCoreBloodPressureProfile.from(o)); + expect(wrapped[0]!.getSystolic()!.value).toBe(120); + expect(wrapped[0]!.getDiastolic()!.value).toBe(80); + }); +}); + describe("category auto-population", () => { test("custom category is preserved, VSCat is appended", () => { const profile = USCoreBloodPressureProfile.create({ From 73c99cce80eaa611eb78568aa3632604bf897a29 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 4 May 2026 15:13:58 +0200 Subject: [PATCH 2/4] TS: us-core example now actually demonstrates Bundle - generate.ts: add Bundle to the tree-shake config so the example can import the generic Bundle/BundleEntry types - profile-bp.test.ts: rewrite the is() demo to filter a typed Bundle (matching the article's Step 5), with a separate assertion against an unknown[] showing the guard also works without Bundle - README: add is() to the profile API list, fix validate()'s described return shape (object with errors and warnings, not a list), and mention prettyReport(report) under "Generating Types" - fhir-types/: regenerated to pick up Bundle.ts and the routine index/Resource/type-tree updates that come with adding a canonical --- examples/typescript-us-core/README.md | 5 +- .../typescript-us-core/fhir-types/README.md | 3 - .../fhir-types/hl7-fhir-r4-core/Bundle.ts | 68 +++++++++++++++++++ .../fhir-types/hl7-fhir-r4-core/Resource.ts | 2 +- .../fhir-types/hl7-fhir-r4-core/index.ts | 2 + .../fhir-types/type-tree.yaml | 4 ++ examples/typescript-us-core/generate.ts | 3 + .../typescript-us-core/profile-bp.test.ts | 27 +++++--- 8 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Bundle.ts diff --git a/examples/typescript-us-core/README.md b/examples/typescript-us-core/README.md index db8f257a2..7771a652b 100644 --- a/examples/typescript-us-core/README.md +++ b/examples/typescript-us-core/README.md @@ -8,7 +8,7 @@ US Core FHIR profile generation with type-safe profile wrapper classes. bun run examples/typescript-us-core/generate.ts ``` -Edit [`generate.ts`](generate.ts) to customize which profiles to include. Tree shaking keeps only the specified profiles and their transitive dependencies. Output goes to `./fhir-types/`. +`generate.ts` calls `prettyReport(report)` to print a grouped summary on stdout — files-per-generator, line counts, errors, warnings, duration, status. Edit the script to customize which profiles to include; tree shaking keeps only the specified profiles and their transitive dependencies. Output goes to `./fhir-types/`. ## Profile Class API @@ -17,7 +17,8 @@ Each profile class provides: - **`from(resource)`** -- validate the resource conforms to the profile (meta.profile + required fields), throw on errors - **`apply(resource)`** -- attach meta.profile without validation, useful for incremental construction - **`create(args)`** -- build a new resource from typed input, auto-sets fixed values -- **`validate()`** -- check required fields, return error list +- **`is(value)`** -- non-throwing type guard (resourceType + meta.profile.includes(canonicalUrl)); drop into `.filter()` on any collection +- **`validate()`** -- return `{ errors, warnings }`; errors block (required fields, slice cardinality), warnings cover must-support gaps - **`toResource()`** -- get the underlying FHIR resource Generated accessors depend on what the profile defines: diff --git a/examples/typescript-us-core/fhir-types/README.md b/examples/typescript-us-core/fhir-types/README.md index cc230de3e..3eeeacca2 100644 --- a/examples/typescript-us-core/fhir-types/README.md +++ b/examples/typescript-us-core/fhir-types/README.md @@ -2219,7 +2219,6 @@ - `urn:fhir:binding:BodyStructureQualifier` - `urn:fhir:binding:BodyTempUnits` - `urn:fhir:binding:BodyWeightUnits` -- `urn:fhir:binding:BundleType` - `urn:fhir:binding:CapabilityStatementKind` - `urn:fhir:binding:CarePlanActivityKind` - `urn:fhir:binding:CarePlanActivityOutcome` @@ -2534,7 +2533,6 @@ - `urn:fhir:binding:GuidanceResponseStatus` - `urn:fhir:binding:GuidePageGeneration` - `urn:fhir:binding:GuideParameterCode` -- `urn:fhir:binding:HTTPVerb` - `urn:fhir:binding:HandlingConditionSet` - `urn:fhir:binding:HumanRefSeqNCBIBuildId` - `urn:fhir:binding:IANATimezone` @@ -2842,7 +2840,6 @@ - `urn:fhir:binding:SPDXLicense` - `urn:fhir:binding:Safety` - `urn:fhir:binding:SearchComparator` -- `urn:fhir:binding:SearchEntryMode` - `urn:fhir:binding:SearchModifierCode` - `urn:fhir:binding:SearchParamType` - `urn:fhir:binding:SearchProcessingModeType` diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Bundle.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Bundle.ts new file mode 100644 index 000000000..4683d0708 --- /dev/null +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Bundle.ts @@ -0,0 +1,68 @@ +// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { BackboneElement } from "../hl7-fhir-r4-core/BackboneElement"; +import type { Identifier } from "../hl7-fhir-r4-core/Identifier"; +import type { Resource } from "../hl7-fhir-r4-core/Resource"; +import type { Signature } from "../hl7-fhir-r4-core/Signature"; + +import type { Element } from "../hl7-fhir-r4-core/Element"; +export type { BackboneElement } from "../hl7-fhir-r4-core/BackboneElement"; +export type { Identifier } from "../hl7-fhir-r4-core/Identifier"; +export type { Signature } from "../hl7-fhir-r4-core/Signature"; + +export interface BundleEntry extends BackboneElement { + fullUrl?: string; + link?: BundleLink[]; + request?: BundleEntryRequest; + resource?: T1; + response?: BundleEntryResponse; + search?: BundleEntrySearch; +} + +export interface BundleEntryRequest extends BackboneElement { + ifMatch?: string; + ifModifiedSince?: string; + ifNoneExist?: string; + ifNoneMatch?: string; + method: ("GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH"); + url: string; +} + +export interface BundleEntryResponse extends BackboneElement { + etag?: string; + lastModified?: string; + location?: string; + outcome?: T; + status: string; +} + +export interface BundleEntrySearch extends BackboneElement { + mode?: ("match" | "include" | "outcome"); + score?: number; +} + +export interface BundleLink extends BackboneElement { + relation: string; + url: string; +} + +// CanonicalURL: http://hl7.org/fhir/StructureDefinition/Bundle (pkg: hl7.fhir.r4.core#4.0.1) +export interface Bundle extends Resource { + resourceType: "Bundle"; + + entry?: BundleEntry[]; + identifier?: Identifier; + link?: BundleLink[]; + signature?: Signature; + timestamp?: string; + _timestamp?: Element; + total?: number; + _total?: Element; + type: ("document" | "message" | "transaction" | "transaction-response" | "batch" | "batch-response" | "history" | "searchset" | "collection"); + _type?: Element; +} +export const isBundle = (resource: unknown): resource is Bundle => { + return resource !== null && typeof resource === "object" && (resource as {resourceType: string}).resourceType === "Bundle"; +} diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Resource.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Resource.ts index 56b489b7c..b79403fc2 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Resource.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/Resource.ts @@ -9,7 +9,7 @@ export type { Meta } from "../hl7-fhir-r4-core/Meta"; // CanonicalURL: http://hl7.org/fhir/StructureDefinition/Resource (pkg: hl7.fhir.r4.core#4.0.1) export interface Resource { - resourceType: "DomainResource" | "Observation" | "Patient" | "Resource"; + resourceType: "Bundle" | "DomainResource" | "Observation" | "Patient" | "Resource"; id?: string; _id?: Element; diff --git a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/index.ts b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/index.ts index 82c1509ab..a837c5f70 100644 --- a/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/index.ts +++ b/examples/typescript-us-core/fhir-types/hl7-fhir-r4-core/index.ts @@ -4,6 +4,8 @@ export type { Age } from "./Age"; export type { Annotation } from "./Annotation"; export type { Attachment } from "./Attachment"; export type { BackboneElement } from "./BackboneElement"; +export type { Bundle, BundleEntry, BundleEntryRequest, BundleEntryResponse, BundleEntrySearch, BundleLink } from "./Bundle"; +export { isBundle } from "./Bundle"; export type { CodeableConcept } from "./CodeableConcept"; export type { Coding } from "./Coding"; export type { ContactDetail } from "./ContactDetail"; diff --git a/examples/typescript-us-core/fhir-types/type-tree.yaml b/examples/typescript-us-core/fhir-types/type-tree.yaml index 7142c3d98..d4aa50176 100644 --- a/examples/typescript-us-core/fhir-types/type-tree.yaml +++ b/examples/typescript-us-core/fhir-types/type-tree.yaml @@ -80,6 +80,7 @@ hl7.fhir.r4.core: http://hl7.org/fhir/StructureDefinition/TriggerDefinition: {} http://hl7.org/fhir/StructureDefinition/UsageContext: {} resource: + http://hl7.org/fhir/StructureDefinition/Bundle: {} http://hl7.org/fhir/StructureDefinition/DomainResource: {} http://hl7.org/fhir/StructureDefinition/Observation: {} http://hl7.org/fhir/StructureDefinition/Patient: {} @@ -102,6 +103,7 @@ shared: urn:fhir:binding:AddressUse: {} urn:fhir:binding:AdministrativeGender: {} urn:fhir:binding:BodySite: {} + urn:fhir:binding:BundleType: {} urn:fhir:binding:ContactPointSystem: {} urn:fhir:binding:ContactPointUse: {} urn:fhir:binding:ContactRelationship: {} @@ -113,6 +115,7 @@ shared: urn:fhir:binding:ExpressionLanguage: {} urn:fhir:binding:FHIRAllTypes: {} urn:fhir:binding:FHIRResourceTypeExt: {} + urn:fhir:binding:HTTPVerb: {} urn:fhir:binding:IdentifierType: {} urn:fhir:binding:IdentifierUse: {} urn:fhir:binding:Language: {} @@ -135,6 +138,7 @@ shared: urn:fhir:binding:QuantityComparator: {} urn:fhir:binding:RelatedArtifactType: {} urn:fhir:binding:RouteOfAdministration: {} + urn:fhir:binding:SearchEntryMode: {} urn:fhir:binding:SecurityLabels: {} urn:fhir:binding:SignatureType: {} urn:fhir:binding:SortDirection: {} diff --git a/examples/typescript-us-core/generate.ts b/examples/typescript-us-core/generate.ts index 7180bbf8e..ab09f1b35 100644 --- a/examples/typescript-us-core/generate.ts +++ b/examples/typescript-us-core/generate.ts @@ -18,6 +18,9 @@ if (require.main === module) { "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure": {}, "http://hl7.org/fhir/us/core/StructureDefinition/us-core-body-weight": {}, }, + "hl7.fhir.r4.core": { + "http://hl7.org/fhir/StructureDefinition/Bundle": {}, + }, }, }) .typescript({ diff --git a/examples/typescript-us-core/profile-bp.test.ts b/examples/typescript-us-core/profile-bp.test.ts index 651740082..30c68abf1 100644 --- a/examples/typescript-us-core/profile-bp.test.ts +++ b/examples/typescript-us-core/profile-bp.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, test } from "bun:test"; +import type { Bundle } from "./fhir-types/hl7-fhir-r4-core/Bundle"; import type { Observation } from "./fhir-types/hl7-fhir-r4-core/Observation"; import type { Patient } from "./fhir-types/hl7-fhir-r4-core/Patient"; import { USCoreBloodPressureProfile, USCorePatientProfile } from "./fhir-types/hl7-fhir-us-core/profiles"; @@ -122,9 +123,8 @@ describe("demo: read a US Core blood pressure observation from JSON", () => { }); }); -describe("demo: filter a mixed collection with profile is()", () => { - test("is() narrows by resourceType + meta.profile, no validation, no instance constructed", () => { - // A mixed bag: a US Core patient, a US Core BP observation, a non-profiled observation, and junk +describe("demo: filter Bundle with profile is()", () => { + test("is() narrows by resourceType + meta.profile across a typed bundle and a raw collection", () => { const patient: Patient = { resourceType: "Patient", meta: { profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"] }, @@ -161,17 +161,26 @@ describe("demo: filter a mixed collection with profile is()", () => { status: "final", code: { coding: [{ code: "29463-7", system: "http://loinc.org" }] }, }; - const resources: unknown[] = [patient, bp, otherObs, { resourceType: "Patient" }, null, "not a resource"]; - // Profile is() filters by resourceType AND meta.profile.includes(canonicalUrl) - const bps = resources.filter(USCoreBloodPressureProfile.is); + // Bundle propagation narrows entry[].resource to Patient | Observation at the type level + const bundle: Bundle = { + resourceType: "Bundle", + type: "transaction", + entry: [{ resource: patient }, { resource: bp }, { resource: otherObs }], + }; + + // is() adds runtime narrowing on top of Bundle: filters by resourceType + meta.profile + const bps = (bundle.entry ?? []).map((e) => e.resource).filter(USCoreBloodPressureProfile.is); expect(bps).toEqual([bp]); - // Same guard works for the Patient profile against the same input - const patients = resources.filter(USCorePatientProfile.is); + const patients = (bundle.entry ?? []).map((e) => e.resource).filter(USCorePatientProfile.is); expect(patients).toEqual([patient]); - // Hand survivors to from() to use typed getters + // Same guard also works on a plain unknown[] — no Bundle required + const mixed: unknown[] = [patient, bp, otherObs, { resourceType: "Patient" }, null, "not a resource"]; + expect(mixed.filter(USCoreBloodPressureProfile.is)).toEqual([bp]); + + // Hand survivors to from() for typed reads const wrapped = bps.map((o) => USCoreBloodPressureProfile.from(o)); expect(wrapped[0]!.getSystolic()!.value).toBe(120); expect(wrapped[0]!.getDiastolic()!.value).toBe(80); From 38e52ae6b62df3c320e2948b6fb0488e9a4a8420 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 4 May 2026 15:17:17 +0200 Subject: [PATCH 3/4] docs: shorten profile API method bullets in us-core README Each bullet is now a fragment instead of a full sentence. Same information, ~half the words. Removes secondary clauses ("useful for incremental construction", "(required fields, slice cardinality)") that the linked tests already document concretely. --- examples/typescript-us-core/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/typescript-us-core/README.md b/examples/typescript-us-core/README.md index 7771a652b..51209d64d 100644 --- a/examples/typescript-us-core/README.md +++ b/examples/typescript-us-core/README.md @@ -14,12 +14,12 @@ bun run examples/typescript-us-core/generate.ts Each profile class provides: -- **`from(resource)`** -- validate the resource conforms to the profile (meta.profile + required fields), throw on errors -- **`apply(resource)`** -- attach meta.profile without validation, useful for incremental construction -- **`create(args)`** -- build a new resource from typed input, auto-sets fixed values -- **`is(value)`** -- non-throwing type guard (resourceType + meta.profile.includes(canonicalUrl)); drop into `.filter()` on any collection -- **`validate()`** -- return `{ errors, warnings }`; errors block (required fields, slice cardinality), warnings cover must-support gaps -- **`toResource()`** -- get the underlying FHIR resource +- **`from(resource)`** -- validate against the profile, throw on errors +- **`apply(resource)`** -- attach meta.profile, no validation +- **`create(args)`** -- build a new resource, auto-set fixed values +- **`is(value)`** -- non-throwing type guard for `.filter()` +- **`validate()`** -- return `{ errors, warnings }` +- **`toResource()`** -- the underlying FHIR resource Generated accessors depend on what the profile defines: From eb5bf7484285588042cccdf2729c05b11005e2c5 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Mon, 4 May 2026 15:38:27 +0200 Subject: [PATCH 4/4] docs: shorten Generating Types paragraph in us-core README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trim the prettyReport field enumeration to "a grouped summary of what was emitted" — readers running the script will see the structure. Tighten the customize/tree-shake sentence and the output line. --- examples/typescript-us-core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/typescript-us-core/README.md b/examples/typescript-us-core/README.md index 51209d64d..98e45f1fd 100644 --- a/examples/typescript-us-core/README.md +++ b/examples/typescript-us-core/README.md @@ -8,7 +8,7 @@ US Core FHIR profile generation with type-safe profile wrapper classes. bun run examples/typescript-us-core/generate.ts ``` -`generate.ts` calls `prettyReport(report)` to print a grouped summary on stdout — files-per-generator, line counts, errors, warnings, duration, status. Edit the script to customize which profiles to include; tree shaking keeps only the specified profiles and their transitive dependencies. Output goes to `./fhir-types/`. +`generate.ts` calls `prettyReport(report)` to print a grouped summary of what was emitted. Edit the script to add/remove profiles; tree shaking keeps only what you list. Output: `./fhir-types/`. ## Profile Class API