Skip to content

Commit 640a4a5

Browse files
Merge pull request #150 from Max-Health-Inc/develop
🚀 Promote develop → main
2 parents 6b1a63b + c800240 commit 640a4a5

32 files changed

Lines changed: 916 additions & 174 deletions

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,34 @@ jobs:
4646
- name: Report newer IG releases
4747
run: npm run check:ig-versions
4848
continue-on-error: true
49+
50+
# The published packages under packages/ were only compiled at release time, so a
51+
# type error in scripts/client-codegen/template reached npm before anything ran
52+
# tsc over it. The drift check above proves the copies match the template; this
53+
# proves the template itself compiles and its tests pass.
54+
packages:
55+
runs-on: ubuntu-latest
56+
strategy:
57+
fail-fast: false
58+
matrix:
59+
package: [client-r4, client-r4b, client-r5, dicomweb, oauth-crypto, smart-auth, zod]
60+
steps:
61+
- uses: actions/checkout@v7
62+
63+
- uses: actions/setup-node@v7
64+
with:
65+
node-version: '22'
66+
cache: 'npm'
67+
68+
# The packages resolve @max-health-inc/config (their vitest preset) from the
69+
# repo root, so the root install has to happen before the package's own tests.
70+
- run: npm ci
71+
72+
- run: npm ci
73+
working-directory: packages/${{ matrix.package }}
74+
75+
- run: npm run build
76+
working-directory: packages/${{ matrix.package }}
77+
78+
- run: npm run test --if-present
79+
working-directory: packages/${{ matrix.package }}

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "babelfhir-ts",
3-
"version": "1.6.1",
3+
"version": "1.6.2-dev.20260808230507.48274f8c",
44
"description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B/R5 StructureDefinitions (profiles) directly inside package archives.",
55
"type": "module",
66
"main": "out/src/main.js",

packages/client-r4/README.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,45 @@ const patient = await client.patient().read('example');
7272
### Bundle Parsing
7373

7474
```ts
75-
import { BundleParser } from '@babelfhir-ts/client-r4';
75+
import { BundleParser, hasId } from '@babelfhir-ts/client-r4';
7676

77-
const patients = BundleParser.getResourcesByType<fhir4.Patient>(bundle, 'Patient');
78-
const first = BundleParser.getFirstResourceByType<fhir4.Patient>(bundle, 'Patient');
77+
// A searchset keeps the type the reader already knew.
78+
const searchset = await client.read().patient().search({ name: 'Smith' }); // Bundle<WithId<fhir4.Patient>>
79+
const all = BundleParser.getAllResources(searchset); // WithId<fhir4.Patient>[]
80+
81+
// On a mixed bundle the return type follows the resourceType argument — no type argument, no cast.
82+
const patients = BundleParser.getResourcesByType(bundle, 'Patient'); // fhir4.Patient[]
83+
const first = BundleParser.getFirstResourceByType(bundle, 'Observation'); // fhir4.Observation | undefined
84+
85+
// `id` is optional in FHIR, so narrow to it explicitly where it is required.
86+
const identified = patients.filter(hasId); // WithId<fhir4.Patient>[]
87+
88+
// References resolve to the bundle's element type, or to a checked resourceType.
89+
const subject = BundleParser.resolveReferenceOfType(obs.subject, bundle, 'Patient');
90+
```
91+
92+
Pass an explicit type argument for profile types from other generated packages —
93+
`resourceType` is then constrained to that type's own discriminant:
94+
95+
```ts
96+
BundleParser.getResourcesByType<USCorePatientProfile>(bundle, 'Patient');
7997
```
8098

99+
### Migrating to 0.3
100+
101+
`BundleParser` no longer returns types it has not checked, so code written against
102+
0.2 can see new compile errors:
103+
104+
- `getResourcesByType` and `getFirstResourceByType` return `T` instead of
105+
`WithId<T>`. `id` is optional in FHIR and was never verified, so add
106+
`.filter(hasId)` where the guarantee is needed.
107+
- `resourceType` is checked against the FHIR resource type union, which turns a
108+
typo into a compile error. A value chosen at runtime should be typed
109+
`FhirResourceType` rather than `string`.
110+
- `resolveReference` takes its return type from the bundle. To narrow a mixed
111+
bundle, call `resolveReferenceOfType(ref, bundle, 'Patient')`, which returns
112+
`undefined` when the reference resolves to another resource type.
113+
81114
## API
82115

83116
| Export | Description |
@@ -87,6 +120,8 @@ const first = BundleParser.getFirstResourceByType<fhir4.Patient>(bundle, 'Patien
87120
| `FhirClient` | Combined read + write client |
88121
| `SmartFhirClient` | SMART-authenticated FHIR client |
89122
| `BundleParser` | Static bundle extraction helpers |
123+
| `hasId` | Type guard narrowing a resource to `WithId<T>` |
124+
| `FhirResourceType` / `ResourceOfType` | The `resourceType` union, and the resource behind one of its members |
90125
| `SmartAuth` | SMART on FHIR v2 authorization (re-exported from `@babelfhir-ts/smart-auth`) |
91126
| `discoverEndpoints` | SMART endpoint discovery |
92127

packages/client-r4/package-lock.json

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

packages/client-r4/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@babelfhir-ts/client-r4",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "Typed FHIR R4 client with read/write/search methods for all 145 base resource types, SMART on FHIR auth, and bundle parsing",
55
"type": "module",
66
"main": "dist/index.js",

packages/client-r4/src/bundle-parser.test.ts

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
import { describe, it, expect } from "vitest";
55
import { BundleParser } from "./bundle-parser.js";
6-
import type { Bundle } from "./types.js";
6+
import { hasId } from "./types.js";
7+
import type { Bundle, WithId } from "./types.js";
78

89
describe("BundleParser", () => {
910
const patient1 = { resourceType: "Patient" as const, id: "1", name: [{ family: "Smith" }] };
@@ -22,14 +23,14 @@ describe("BundleParser", () => {
2223

2324
describe("getResourcesByType", () => {
2425
it("filters resources by resourceType", () => {
25-
const patients = BundleParser.getResourcesByType<fhir4.Patient>(bundle, "Patient");
26+
const patients = BundleParser.getResourcesByType(bundle, "Patient");
2627
expect(patients).toHaveLength(2);
27-
expect(patients[0].id).toBe("1");
28-
expect(patients[1].id).toBe("2");
28+
expect(patients[0]?.id).toBe("1");
29+
expect(patients[1]?.id).toBe("2");
2930
});
3031

3132
it("returns empty array for non-matching type", () => {
32-
const conditions = BundleParser.getResourcesByType<fhir4.Condition>(bundle, "Condition");
33+
const conditions = BundleParser.getResourcesByType(bundle, "Condition");
3334
expect(conditions).toEqual([]);
3435
});
3536

@@ -46,11 +47,27 @@ describe("BundleParser", () => {
4647
};
4748
expect(BundleParser.getResourcesByType(noRes, "Patient")).toEqual([]);
4849
});
50+
51+
it("keeps resources that have no id, and hasId drops them", () => {
52+
const posting: Bundle<fhir4.Resource> = {
53+
resourceType: "Bundle",
54+
type: "transaction",
55+
entry: [
56+
{ resource: { resourceType: "Patient" } as fhir4.Resource, request: { method: "POST", url: "Patient" } },
57+
{ resource: patient1 as fhir4.Resource },
58+
],
59+
};
60+
expect(BundleParser.getResourcesByType(posting, "Patient")).toHaveLength(2);
61+
62+
const withIds: WithId<fhir4.Patient>[] = BundleParser.getResourcesByType(posting, "Patient").filter(hasId);
63+
expect(withIds).toHaveLength(1);
64+
expect(withIds[0]?.id).toBe("1");
65+
});
4966
});
5067

5168
describe("getFirstResourceByType", () => {
5269
it("returns first matching resource", () => {
53-
const first = BundleParser.getFirstResourceByType<fhir4.Patient>(bundle, "Patient");
70+
const first = BundleParser.getFirstResourceByType(bundle, "Patient");
5471
expect(first?.id).toBe("1");
5572
});
5673

@@ -65,6 +82,17 @@ describe("BundleParser", () => {
6582
expect(all).toHaveLength(3);
6683
});
6784

85+
it("preserves the bundle's element type", () => {
86+
const searchset: Bundle<WithId<fhir4.Patient>> = {
87+
resourceType: "Bundle",
88+
type: "searchset",
89+
entry: [{ resource: patient1 }, { resource: patient2 }],
90+
};
91+
// No cast: what the reader knew is what comes back.
92+
const patients: WithId<fhir4.Patient>[] = BundleParser.getAllResources(searchset);
93+
expect(patients.map(p => p.id)).toEqual(["1", "2"]);
94+
});
95+
6896
it("returns empty array for empty bundle", () => {
6997
const empty: Bundle<fhir4.Resource> = { resourceType: "Bundle", type: "searchset" };
7098
expect(BundleParser.getAllResources(empty)).toEqual([]);
@@ -96,26 +124,23 @@ describe("BundleParser", () => {
96124
};
97125

98126
it("resolves by exact fullUrl match", () => {
99-
const result = BundleParser.resolveReference<fhir4.Patient>(
127+
const result = BundleParser.resolveReference(
100128
{ reference: "https://fhir.example.com/Patient/1" },
101129
bundleWithFullUrls,
102130
);
103131
expect(result?.id).toBe("1");
104132
});
105133

106134
it("resolves by urn:uuid fullUrl match", () => {
107-
const result = BundleParser.resolveReference<fhir4.Patient>(
135+
const result = BundleParser.resolveReference(
108136
{ reference: "urn:uuid:abc-123" },
109137
bundleWithFullUrls,
110138
);
111139
expect(result?.id).toBe("2");
112140
});
113141

114142
it("resolves by ResourceType/id relative reference", () => {
115-
const result = BundleParser.resolveReference<fhir4.Patient>(
116-
{ reference: "Patient/1" },
117-
bundleWithFullUrls,
118-
);
143+
const result = BundleParser.resolveReference({ reference: "Patient/1" }, bundleWithFullUrls);
119144
expect(result?.id).toBe("1");
120145
});
121146

@@ -127,10 +152,7 @@ describe("BundleParser", () => {
127152
{ fullUrl: "https://fhir.example.com/Patient/99", resource: { resourceType: "Patient" } as fhir4.Resource },
128153
],
129154
};
130-
const result = BundleParser.resolveReference<fhir4.Patient>(
131-
{ reference: "Patient/99" },
132-
bundleNoId,
133-
);
155+
const result = BundleParser.resolveReference({ reference: "Patient/99" }, bundleNoId);
134156
expect(result?.resourceType).toBe("Patient");
135157
});
136158

@@ -151,5 +173,47 @@ describe("BundleParser", () => {
151173
bundleWithFullUrls,
152174
)).toBeUndefined();
153175
});
176+
177+
it("preserves the bundle's element type", () => {
178+
const searchset: Bundle<WithId<fhir4.Patient>> = {
179+
resourceType: "Bundle",
180+
type: "searchset",
181+
entry: [{ fullUrl: "https://fhir.example.com/Patient/1", resource: patient1 }],
182+
};
183+
const resolved: WithId<fhir4.Patient> | undefined = BundleParser.resolveReference(
184+
{ reference: "Patient/1" },
185+
searchset,
186+
);
187+
expect(resolved?.id).toBe("1");
188+
});
189+
});
190+
191+
describe("resolveReferenceOfType", () => {
192+
const bundleWithFullUrls: Bundle<fhir4.Resource> = {
193+
resourceType: "Bundle",
194+
type: "searchset",
195+
entry: [
196+
{ fullUrl: "https://fhir.example.com/Patient/1", resource: patient1 as fhir4.Resource },
197+
{ fullUrl: "https://fhir.example.com/Observation/obs-1", resource: observation as fhir4.Resource },
198+
],
199+
};
200+
201+
it("resolves a reference of the requested type", () => {
202+
const result = BundleParser.resolveReferenceOfType(
203+
{ reference: "Patient/1" },
204+
bundleWithFullUrls,
205+
"Patient",
206+
);
207+
expect(result?.id).toBe("1");
208+
});
209+
210+
it("returns undefined when the reference resolves to another type", () => {
211+
const result = BundleParser.resolveReferenceOfType(
212+
{ reference: "https://fhir.example.com/Observation/obs-1" },
213+
bundleWithFullUrls,
214+
"Patient",
215+
);
216+
expect(result).toBeUndefined();
217+
});
154218
});
155219
});

0 commit comments

Comments
 (0)