@metriport/fhir-sdk 0.30.0-alpha.1
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/LICENSE +21 -0
- package/README.md +564 -0
- package/dist/__tests__/careplan.test.d.ts +2 -0
- package/dist/__tests__/careplan.test.d.ts.map +1 -0
- package/dist/__tests__/careplan.test.js +544 -0
- package/dist/__tests__/careplan.test.js.map +1 -0
- package/dist/__tests__/clinical-dates.test.d.ts +2 -0
- package/dist/__tests__/clinical-dates.test.d.ts.map +1 -0
- package/dist/__tests__/clinical-dates.test.js +341 -0
- package/dist/__tests__/clinical-dates.test.js.map +1 -0
- package/dist/__tests__/coding-utilities.test.d.ts +2 -0
- package/dist/__tests__/coding-utilities.test.d.ts.map +1 -0
- package/dist/__tests__/coding-utilities.test.js +482 -0
- package/dist/__tests__/coding-utilities.test.js.map +1 -0
- package/dist/__tests__/date-range-performance.test.d.ts +2 -0
- package/dist/__tests__/date-range-performance.test.d.ts.map +1 -0
- package/dist/__tests__/date-range-performance.test.js +218 -0
- package/dist/__tests__/date-range-performance.test.js.map +1 -0
- package/dist/__tests__/date-range-search.test.d.ts +2 -0
- package/dist/__tests__/date-range-search.test.d.ts.map +1 -0
- package/dist/__tests__/date-range-search.test.js +215 -0
- package/dist/__tests__/date-range-search.test.js.map +1 -0
- package/dist/__tests__/env-setup.d.ts +2 -0
- package/dist/__tests__/env-setup.d.ts.map +1 -0
- package/dist/__tests__/env-setup.js +37 -0
- package/dist/__tests__/env-setup.js.map +1 -0
- package/dist/__tests__/fhir-bundle-sdk-basic.test.d.ts +2 -0
- package/dist/__tests__/fhir-bundle-sdk-basic.test.d.ts.map +1 -0
- package/dist/__tests__/fhir-bundle-sdk-basic.test.js +19 -0
- package/dist/__tests__/fhir-bundle-sdk-basic.test.js.map +1 -0
- package/dist/__tests__/fhir-bundle-sdk.test.d.ts +2 -0
- package/dist/__tests__/fhir-bundle-sdk.test.d.ts.map +1 -0
- package/dist/__tests__/fhir-bundle-sdk.test.js +953 -0
- package/dist/__tests__/fhir-bundle-sdk.test.js.map +1 -0
- package/dist/__tests__/fixtures/fhir-bundles.d.ts +31 -0
- package/dist/__tests__/fixtures/fhir-bundles.d.ts.map +1 -0
- package/dist/__tests__/fixtures/fhir-bundles.js +487 -0
- package/dist/__tests__/fixtures/fhir-bundles.js.map +1 -0
- package/dist/__tests__/phase1-verification.test.d.ts +2 -0
- package/dist/__tests__/phase1-verification.test.d.ts.map +1 -0
- package/dist/__tests__/phase1-verification.test.js +141 -0
- package/dist/__tests__/phase1-verification.test.js.map +1 -0
- package/dist/__tests__/phase2-verification.test.d.ts +2 -0
- package/dist/__tests__/phase2-verification.test.d.ts.map +1 -0
- package/dist/__tests__/phase2-verification.test.js +234 -0
- package/dist/__tests__/phase2-verification.test.js.map +1 -0
- package/dist/__tests__/phase3-verification.test.d.ts +2 -0
- package/dist/__tests__/phase3-verification.test.d.ts.map +1 -0
- package/dist/__tests__/phase3-verification.test.js +121 -0
- package/dist/__tests__/phase3-verification.test.js.map +1 -0
- package/dist/__tests__/phase4-verification.test.d.ts +2 -0
- package/dist/__tests__/phase4-verification.test.d.ts.map +1 -0
- package/dist/__tests__/phase4-verification.test.js +168 -0
- package/dist/__tests__/phase4-verification.test.js.map +1 -0
- package/dist/__tests__/phase5-verification.test.d.ts +2 -0
- package/dist/__tests__/phase5-verification.test.d.ts.map +1 -0
- package/dist/__tests__/phase5-verification.test.js +397 -0
- package/dist/__tests__/phase5-verification.test.js.map +1 -0
- package/dist/__tests__/reverse-references.test.d.ts +2 -0
- package/dist/__tests__/reverse-references.test.d.ts.map +1 -0
- package/dist/__tests__/reverse-references.test.js +294 -0
- package/dist/__tests__/reverse-references.test.js.map +1 -0
- package/dist/__tests__/type-guards.test.d.ts +2 -0
- package/dist/__tests__/type-guards.test.d.ts.map +1 -0
- package/dist/__tests__/type-guards.test.js +163 -0
- package/dist/__tests__/type-guards.test.js.map +1 -0
- package/dist/clinical-dates.d.ts +26 -0
- package/dist/clinical-dates.d.ts.map +1 -0
- package/dist/clinical-dates.js +571 -0
- package/dist/clinical-dates.js.map +1 -0
- package/dist/fhir-bundle-sdk.d.ts +233 -0
- package/dist/fhir-bundle-sdk.d.ts.map +1 -0
- package/dist/fhir-bundle-sdk.js +545 -0
- package/dist/fhir-bundle-sdk.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/bundle-operations.d.ts +14 -0
- package/dist/internal/bundle-operations.d.ts.map +1 -0
- package/dist/internal/bundle-operations.js +55 -0
- package/dist/internal/bundle-operations.js.map +1 -0
- package/dist/internal/coding-systems.d.ts +48 -0
- package/dist/internal/coding-systems.d.ts.map +1 -0
- package/dist/internal/coding-systems.js +55 -0
- package/dist/internal/coding-systems.js.map +1 -0
- package/dist/internal/coding-utilities.d.ts +27 -0
- package/dist/internal/coding-utilities.d.ts.map +1 -0
- package/dist/internal/coding-utilities.js +297 -0
- package/dist/internal/coding-utilities.js.map +1 -0
- package/dist/internal/date-extraction.d.ts +12 -0
- package/dist/internal/date-extraction.d.ts.map +1 -0
- package/dist/internal/date-extraction.js +629 -0
- package/dist/internal/date-extraction.js.map +1 -0
- package/dist/internal/graph-traversal.d.ts +13 -0
- package/dist/internal/graph-traversal.d.ts.map +1 -0
- package/dist/internal/graph-traversal.js +89 -0
- package/dist/internal/graph-traversal.js.map +1 -0
- package/dist/internal/indexing.d.ts +17 -0
- package/dist/internal/indexing.d.ts.map +1 -0
- package/dist/internal/indexing.js +129 -0
- package/dist/internal/indexing.js.map +1 -0
- package/dist/internal/llm-context.d.ts +40 -0
- package/dist/internal/llm-context.d.ts.map +1 -0
- package/dist/internal/llm-context.js +214 -0
- package/dist/internal/llm-context.js.map +1 -0
- package/dist/internal/reference-resolution.d.ts +29 -0
- package/dist/internal/reference-resolution.d.ts.map +1 -0
- package/dist/internal/reference-resolution.js +338 -0
- package/dist/internal/reference-resolution.js.map +1 -0
- package/dist/internal/reference-utils.d.ts +26 -0
- package/dist/internal/reference-utils.d.ts.map +1 -0
- package/dist/internal/reference-utils.js +89 -0
- package/dist/internal/reference-utils.js.map +1 -0
- package/dist/internal/transparent-proxy.d.ts +12 -0
- package/dist/internal/transparent-proxy.d.ts.map +1 -0
- package/dist/internal/transparent-proxy.js +81 -0
- package/dist/internal/transparent-proxy.js.map +1 -0
- package/dist/internal/validation.d.ts +16 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +45 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/type-guards.d.ts +212 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +375 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types/coding-fields.d.ts +311 -0
- package/dist/types/coding-fields.d.ts.map +1 -0
- package/dist/types/coding-fields.js +3 -0
- package/dist/types/coding-fields.js.map +1 -0
- package/dist/types/sdk-types.d.ts +107 -0
- package/dist/types/sdk-types.d.ts.map +1 -0
- package/dist/types/sdk-types.js +17 -0
- package/dist/types/sdk-types.js.map +1 -0
- package/dist/types/smart-resources.d.ts +470 -0
- package/dist/types/smart-resources.d.ts.map +1 -0
- package/dist/types/smart-resources.js +249 -0
- package/dist/types/smart-resources.js.map +1 -0
- package/dist/utils/interval-tree/index.d.ts +87 -0
- package/dist/utils/interval-tree/index.d.ts.map +1 -0
- package/dist/utils/interval-tree/index.js +774 -0
- package/dist/utils/interval-tree/index.js.map +1 -0
- package/dist/utils/interval-tree/shallowequal/arrays.d.ts +3 -0
- package/dist/utils/interval-tree/shallowequal/arrays.d.ts.map +1 -0
- package/dist/utils/interval-tree/shallowequal/arrays.js +25 -0
- package/dist/utils/interval-tree/shallowequal/arrays.js.map +1 -0
- package/dist/utils/interval-tree/shallowequal/index.d.ts +6 -0
- package/dist/utils/interval-tree/shallowequal/index.d.ts.map +1 -0
- package/dist/utils/interval-tree/shallowequal/index.js +28 -0
- package/dist/utils/interval-tree/shallowequal/index.js.map +1 -0
- package/dist/utils/interval-tree/shallowequal/objects.d.ts +3 -0
- package/dist/utils/interval-tree/shallowequal/objects.d.ts.map +1 -0
- package/dist/utils/interval-tree/shallowequal/objects.js +28 -0
- package/dist/utils/interval-tree/shallowequal/objects.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("../index");
|
|
4
|
+
const fhir_bundles_1 = require("./fixtures/fhir-bundles");
|
|
5
|
+
describe("FhirBundleSdk", () => {
|
|
6
|
+
describe("Bundle Loading and Initialization", () => {
|
|
7
|
+
describe("FR-1.1: SDK constructor accepts a FHIR Bundle object", () => {
|
|
8
|
+
it("should accept a valid FHIR bundle", async () => {
|
|
9
|
+
await expect(index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle)).resolves.not.toThrow();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
describe("FR-1.2: SDK constructor throws error if bundle.resourceType !== 'Bundle'", () => {
|
|
13
|
+
it("should throw error for invalid resourceType", async () => {
|
|
14
|
+
await expect(index_1.FhirBundleSdk.create(fhir_bundles_1.invalidBundleWrongType)).rejects.toThrow();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe("FR-1.4: SDK constructor creates internal indexes for O(1) resource lookup", () => {
|
|
18
|
+
it("should create internal indexes during construction", async () => {
|
|
19
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
20
|
+
const start = performance.now();
|
|
21
|
+
const patient = sdk.getResourceById("patient-123");
|
|
22
|
+
const end = performance.now();
|
|
23
|
+
expect(patient).toBeDefined();
|
|
24
|
+
expect(patient?.resourceType).toBe("Patient");
|
|
25
|
+
expect(end - start).toBeLessThan(fhir_bundles_1.CONSTANT_TIME_EXPECTED_THRESHOLD_MS);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe("Reference Validation", () => {
|
|
30
|
+
describe("FR-2.1: lookForBrokenReferences() method returns validation result", () => {
|
|
31
|
+
it("should return validation result for valid bundle", async () => {
|
|
32
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
33
|
+
const result = sdk.lookForBrokenReferences();
|
|
34
|
+
expect(result).toBeDefined();
|
|
35
|
+
expect(typeof result.hasBrokenReferences).toBe("boolean");
|
|
36
|
+
expect(Array.isArray(result.brokenReferences)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it("should return true for valid references", async () => {
|
|
39
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
40
|
+
const result = sdk.lookForBrokenReferences();
|
|
41
|
+
expect(result.hasBrokenReferences).toBe(false);
|
|
42
|
+
expect(result.brokenReferences).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
it("should return false for invalid references", async () => {
|
|
45
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithBrokenReferences);
|
|
46
|
+
const result = sdk.lookForBrokenReferences();
|
|
47
|
+
expect(result.hasBrokenReferences).toBe(true);
|
|
48
|
+
expect(result.brokenReferences.length).toBeGreaterThan(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("FR-2.2: Validation identifies references by Resource/id pattern and fullUrl references", () => {
|
|
52
|
+
it("should identify Resource/id pattern references", async () => {
|
|
53
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithBrokenReferences);
|
|
54
|
+
const result = sdk.lookForBrokenReferences();
|
|
55
|
+
const brokenSubjectRef = result.brokenReferences.find(ref => ref.reference === "Patient/nonexistent-patient");
|
|
56
|
+
expect(brokenSubjectRef).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
it("should validate fullUrl references", async () => {
|
|
59
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithFullUrlReferences);
|
|
60
|
+
const result = sdk.lookForBrokenReferences();
|
|
61
|
+
expect(result.hasBrokenReferences).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe("FR-2.3: Validation handles both relative and absolute references", () => {
|
|
65
|
+
it("should handle relative references (Patient/123)", async () => {
|
|
66
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
67
|
+
const result = sdk.lookForBrokenReferences();
|
|
68
|
+
expect(result.hasBrokenReferences).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
it("should handle absolute references (urn:uuid:123)", async () => {
|
|
71
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithFullUrlReferences);
|
|
72
|
+
const result = sdk.lookForBrokenReferences();
|
|
73
|
+
expect(result.hasBrokenReferences).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("FR-2.4: Validation result includes list of broken references with details", () => {
|
|
77
|
+
it("should provide detailed broken reference information", async () => {
|
|
78
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithBrokenReferences);
|
|
79
|
+
const result = sdk.lookForBrokenReferences();
|
|
80
|
+
expect(result.brokenReferences.length).toBeGreaterThan(0);
|
|
81
|
+
const brokenRef = result.brokenReferences[0];
|
|
82
|
+
expect(brokenRef).toBeDefined();
|
|
83
|
+
if (brokenRef) {
|
|
84
|
+
expect(brokenRef.sourceResourceId).toBeDefined();
|
|
85
|
+
expect(brokenRef.sourceResourceType).toBeDefined();
|
|
86
|
+
expect(brokenRef.referenceField).toBeDefined();
|
|
87
|
+
expect(brokenRef.reference).toBeDefined();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("Resource Retrieval by ID", () => {
|
|
93
|
+
describe("FR-3.1: getResourceById<T>(id: string): T | undefined returns resource matching the ID", () => {
|
|
94
|
+
it("should return resource by ID", async () => {
|
|
95
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
96
|
+
const patient = sdk.getResourceById("patient-123");
|
|
97
|
+
expect(patient).toBeDefined();
|
|
98
|
+
expect(patient?.id).toBe("patient-123");
|
|
99
|
+
expect(patient?.resourceType).toBe("Patient");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("FR-3.2: Method searches both resource.id and entry.fullUrl for matches", () => {
|
|
103
|
+
it("should find resource by resource.id", async () => {
|
|
104
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
105
|
+
const patient = sdk.getResourceById("patient-123");
|
|
106
|
+
expect(patient?.id).toBe("patient-123");
|
|
107
|
+
});
|
|
108
|
+
it("should find resource by fullUrl", async () => {
|
|
109
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
110
|
+
const patient = sdk.getResourceById("urn:uuid:patient-123");
|
|
111
|
+
expect(patient?.id).toBe("patient-123");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe("FR-3.3: Method supports type parameter for proper TypeScript return typing", () => {
|
|
115
|
+
it("should return properly typed resource", async () => {
|
|
116
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
117
|
+
const patient = sdk.getResourceById("patient-123");
|
|
118
|
+
// TypeScript should infer this as Patient | undefined
|
|
119
|
+
expect(patient?.name).toBeDefined();
|
|
120
|
+
expect(patient?.birthDate).toBeDefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe("FR-3.4: Method returns undefined if resource not found", () => {
|
|
124
|
+
it("should return undefined for nonexistent resource", async () => {
|
|
125
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
126
|
+
const result = sdk.getResourceById("nonexistent-id");
|
|
127
|
+
expect(result).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe("FR-3.5: Lookup operates in O(1) time complexity", () => {
|
|
131
|
+
it("should perform lookup in O(1) time", async () => {
|
|
132
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
133
|
+
const start = performance.now();
|
|
134
|
+
sdk.getResourceById("patient-123");
|
|
135
|
+
const end = performance.now();
|
|
136
|
+
// O(1) lookup should be very fast
|
|
137
|
+
expect(end - start).toBeLessThan(fhir_bundles_1.CONSTANT_TIME_EXPECTED_THRESHOLD_MS);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe("Type-Specific Resource Getters", () => {
|
|
142
|
+
describe("FR-4.1: getPatients(): Patient[] returns all Patient resources", () => {
|
|
143
|
+
it("should return all patients", async () => {
|
|
144
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
145
|
+
const patients = sdk.getPatients();
|
|
146
|
+
expect(Array.isArray(patients)).toBe(true);
|
|
147
|
+
expect(patients).toHaveLength(1);
|
|
148
|
+
expect(patients[0]?.resourceType).toBe("Patient");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe("FR-4.2: getObservations(): Observation[] returns all Observation resources", () => {
|
|
152
|
+
it("should return all observations", async () => {
|
|
153
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
154
|
+
const observations = sdk.getObservations();
|
|
155
|
+
expect(Array.isArray(observations)).toBe(true);
|
|
156
|
+
expect(observations).toHaveLength(1);
|
|
157
|
+
expect(observations[0]?.resourceType).toBe("Observation");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe("FR-4.3: getEncounters(): Encounter[] returns all Encounter resources", () => {
|
|
161
|
+
it("should return all encounters", async () => {
|
|
162
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
163
|
+
const encounters = sdk.getEncounters();
|
|
164
|
+
expect(Array.isArray(encounters)).toBe(true);
|
|
165
|
+
expect(encounters).toHaveLength(1);
|
|
166
|
+
expect(encounters[0]?.resourceType).toBe("Encounter");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe("FR-4.4: getPractitioners(): Practitioner[] returns all Practitioner resources", () => {
|
|
170
|
+
it("should return all practitioners", async () => {
|
|
171
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
172
|
+
const practitioners = sdk.getPractitioners();
|
|
173
|
+
expect(Array.isArray(practitioners)).toBe(true);
|
|
174
|
+
expect(practitioners).toHaveLength(1);
|
|
175
|
+
expect(practitioners[0]?.resourceType).toBe("Practitioner");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe("FR-4.5: getDiagnosticReports(): DiagnosticReport[] returns all DiagnosticReport resources", () => {
|
|
179
|
+
it("should return all diagnostic reports", async () => {
|
|
180
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
181
|
+
const reports = sdk.getDiagnosticReports();
|
|
182
|
+
expect(Array.isArray(reports)).toBe(true);
|
|
183
|
+
expect(reports).toHaveLength(1);
|
|
184
|
+
expect(reports[0]?.resourceType).toBe("DiagnosticReport");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe("FR-4.6: All type-specific getters return empty array if no resources of that type exist", () => {
|
|
188
|
+
it("should return empty array when no resources exist", async () => {
|
|
189
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.emptyBundle);
|
|
190
|
+
expect(sdk.getPatients()).toEqual([]);
|
|
191
|
+
expect(sdk.getObservations()).toEqual([]);
|
|
192
|
+
expect(sdk.getEncounters()).toEqual([]);
|
|
193
|
+
expect(sdk.getPractitioners()).toEqual([]);
|
|
194
|
+
expect(sdk.getDiagnosticReports()).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
it("should return empty array for missing resource types", async () => {
|
|
197
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.patientsOnlyBundle);
|
|
198
|
+
expect(sdk.getPatients()).toHaveLength(3);
|
|
199
|
+
expect(sdk.getObservations()).toEqual([]);
|
|
200
|
+
expect(sdk.getEncounters()).toEqual([]);
|
|
201
|
+
expect(sdk.getPractitioners()).toEqual([]);
|
|
202
|
+
expect(sdk.getDiagnosticReports()).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe("FR-4.7: All methods use @medplum/fhirtypes for return type definitions", () => {
|
|
206
|
+
it("should return properly typed resources", async () => {
|
|
207
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
208
|
+
const patients = sdk.getPatients();
|
|
209
|
+
const observations = sdk.getObservations();
|
|
210
|
+
const encounters = sdk.getEncounters();
|
|
211
|
+
const practitioners = sdk.getPractitioners();
|
|
212
|
+
const reports = sdk.getDiagnosticReports();
|
|
213
|
+
// TypeScript compilation will enforce correct typing
|
|
214
|
+
expect(patients[0]?.name).toBeDefined();
|
|
215
|
+
expect(observations[0]?.status).toBeDefined();
|
|
216
|
+
expect(encounters[0]?.status).toBeDefined();
|
|
217
|
+
expect(practitioners[0]?.name).toBeDefined();
|
|
218
|
+
expect(reports[0]?.status).toBeDefined();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe("Smart Reference Resolution", () => {
|
|
223
|
+
describe("FR-5.1: Resources returned by SDK have additional getter methods for each Reference field", () => {
|
|
224
|
+
it("should add getter methods for reference fields", async () => {
|
|
225
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
226
|
+
const observations = sdk.getObservations();
|
|
227
|
+
const observation = observations[0];
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
229
|
+
expect(typeof observation?.getSubject).toBe("function");
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
231
|
+
expect(typeof observation?.getEncounter).toBe("function");
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
233
|
+
expect(typeof observation?.getPerformers).toBe("function");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe("FR-5.2: For Observation.subject reference, SDK adds getSubject() method", () => {
|
|
237
|
+
it("should resolve subject reference to Patient", async () => {
|
|
238
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
239
|
+
const observations = sdk.getObservations();
|
|
240
|
+
const observation = observations[0];
|
|
241
|
+
const subject = observation?.getSubject();
|
|
242
|
+
expect(subject).toBeDefined();
|
|
243
|
+
expect(subject?.resourceType).toBe("Patient");
|
|
244
|
+
expect(subject?.id).toBe("patient-123");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
describe("FR-5.3: For Observation.encounter reference, SDK adds getEncounter() method", () => {
|
|
248
|
+
it("should resolve encounter reference to Encounter", async () => {
|
|
249
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
250
|
+
const observations = sdk.getObservations();
|
|
251
|
+
const observation = observations[0];
|
|
252
|
+
const encounter = observation?.getEncounter();
|
|
253
|
+
expect(encounter).toBeDefined();
|
|
254
|
+
expect(encounter?.resourceType).toBe("Encounter");
|
|
255
|
+
expect(encounter?.id).toBe("encounter-789");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe("FR-5.4: For Observation.performer reference array, SDK adds getPerformer() method", () => {
|
|
259
|
+
it("should resolve performer references to array of resources", async () => {
|
|
260
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
261
|
+
const observations = sdk.getObservations();
|
|
262
|
+
const observation = observations[0];
|
|
263
|
+
const performers = observation?.getPerformers();
|
|
264
|
+
expect(Array.isArray(performers)).toBe(true);
|
|
265
|
+
expect(performers).toHaveLength(1);
|
|
266
|
+
expect(performers?.[0]?.resourceType).toBe("Practitioner");
|
|
267
|
+
expect(performers?.[0]?.id).toBe("practitioner-456");
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
describe("FR-5.5: Reference resolution methods handle both resource.id and fullUrl matching", () => {
|
|
271
|
+
it("should resolve references by resource.id", async () => {
|
|
272
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
273
|
+
const observations = sdk.getObservations();
|
|
274
|
+
const subject = observations[0]?.getSubject();
|
|
275
|
+
expect(subject?.id).toBe("patient-123");
|
|
276
|
+
});
|
|
277
|
+
it("should resolve references by fullUrl", async () => {
|
|
278
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithFullUrlReferences);
|
|
279
|
+
const observations = sdk.getObservations();
|
|
280
|
+
const subject = observations[0]?.getSubject();
|
|
281
|
+
expect(subject?.id).toBe("patient-fullurl");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
describe("FR-5.6: Reference resolution methods return undefined for unresolvable references", () => {
|
|
285
|
+
it("should return undefined for broken references", async () => {
|
|
286
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithBrokenReferences);
|
|
287
|
+
const observations = sdk.getObservations();
|
|
288
|
+
const observation = observations[0];
|
|
289
|
+
expect(observation?.getSubject()).toBeUndefined();
|
|
290
|
+
expect(observation?.getEncounter()).toBeUndefined();
|
|
291
|
+
expect(observation?.getPerformers()).toEqual([]);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
describe("FR-5.7: Reference resolution operates in O(1) time complexity per reference", () => {
|
|
295
|
+
it("should resolve references in O(1) time", async () => {
|
|
296
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
297
|
+
const observations = sdk.getObservations();
|
|
298
|
+
const observation = observations[0];
|
|
299
|
+
const start = performance.now();
|
|
300
|
+
observation?.getSubject();
|
|
301
|
+
const end = performance.now();
|
|
302
|
+
// O(1) resolution should be very fast
|
|
303
|
+
expect(end - start).toBeLessThan(fhir_bundles_1.CONSTANT_TIME_EXPECTED_THRESHOLD_MS);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
describe("FR-5.8: Original reference fields remain unchanged", () => {
|
|
307
|
+
it("should preserve original reference fields", async () => {
|
|
308
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
309
|
+
const observations = sdk.getObservations();
|
|
310
|
+
const observation = observations[0];
|
|
311
|
+
expect(observation?.subject?.reference).toBe("Patient/patient-123");
|
|
312
|
+
expect(observation?.encounter?.reference).toBe("Encounter/encounter-789");
|
|
313
|
+
expect(observation?.performer?.[0]?.reference).toBe("Practitioner/practitioner-456");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
describe("Bundle Export Functionality", () => {
|
|
318
|
+
describe("FR-6.1: exportSubset(resourceIds: string[]): Bundle creates new bundle with specified resources", () => {
|
|
319
|
+
it("should export subset of resources by ID", async () => {
|
|
320
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
321
|
+
const subset = sdk.exportSubset(["patient-123", "observation-001"]);
|
|
322
|
+
expect(subset.resourceType).toBe("Bundle");
|
|
323
|
+
expect(subset.type).toBe("collection");
|
|
324
|
+
expect(subset.entry).toHaveLength(2);
|
|
325
|
+
expect(subset.total).toBe(2);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
describe("FR-6.2: exportByType(resourceType: string): Bundle creates new bundle with all resources of specified type", () => {
|
|
329
|
+
it("should export all resources of specified type", async () => {
|
|
330
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.mixedResourceTypesBundle);
|
|
331
|
+
const patientBundle = sdk.exportByType("Patient");
|
|
332
|
+
expect(patientBundle.resourceType).toBe("Bundle");
|
|
333
|
+
expect(patientBundle.type).toBe("collection");
|
|
334
|
+
expect(patientBundle.entry).toHaveLength(2);
|
|
335
|
+
expect(patientBundle.total).toBe(2);
|
|
336
|
+
expect(
|
|
337
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
338
|
+
patientBundle.entry?.every((entry) => entry.resource?.resourceType === "Patient")).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
describe("FR-6.3: exportByTypes(resourceTypes: string[]): Bundle creates new bundle with all resources of specified types", () => {
|
|
342
|
+
it("should export all resources of specified types", async () => {
|
|
343
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.mixedResourceTypesBundle);
|
|
344
|
+
const bundle = sdk.exportByTypes(["Patient", "Observation"]);
|
|
345
|
+
expect(bundle.resourceType).toBe("Bundle");
|
|
346
|
+
expect(bundle.type).toBe("collection");
|
|
347
|
+
expect(bundle.entry).toHaveLength(4); // 2 patients + 2 observations
|
|
348
|
+
expect(bundle.total).toBe(4);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
describe("FR-6.4: Exported bundles maintain original bundle metadata but update total count", () => {
|
|
352
|
+
it("should maintain metadata and update total", async () => {
|
|
353
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
354
|
+
const subset = sdk.exportSubset(["patient-123"]);
|
|
355
|
+
expect(subset.resourceType).toBe("Bundle");
|
|
356
|
+
expect(subset.type).toBe("collection");
|
|
357
|
+
expect(subset.total).toBe(1); // Updated count
|
|
358
|
+
expect(subset.id).toBeDefined(); // Metadata preserved
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
describe("FR-6.5: Exported bundles include only resources that exist in the original bundle", () => {
|
|
362
|
+
it("should ignore nonexistent resource IDs", async () => {
|
|
363
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
364
|
+
const subset = sdk.exportSubset(["patient-123", "nonexistent-id"]);
|
|
365
|
+
expect(subset.entry).toHaveLength(1);
|
|
366
|
+
expect(subset.total).toBe(1);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
describe("FR-6.6: Exported bundles preserve original entry.fullUrl values", () => {
|
|
370
|
+
it("should preserve fullUrl values", async () => {
|
|
371
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
372
|
+
const subset = sdk.exportSubset(["patient-123"]);
|
|
373
|
+
expect(subset.entry?.[0]?.fullUrl).toBe("urn:uuid:patient-123");
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
describe("Error Handling", () => {
|
|
378
|
+
describe("FR-7.1: All methods handle malformed resource references gracefully", () => {
|
|
379
|
+
it("should handle malformed references without throwing", async () => {
|
|
380
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithBrokenReferences);
|
|
381
|
+
expect(() => {
|
|
382
|
+
const observations = sdk.getObservations();
|
|
383
|
+
observations[0]?.getSubject();
|
|
384
|
+
}).not.toThrow();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
describe("FR-7.2: SDK throws descriptive errors for invalid bundle structure during initialization", () => {
|
|
388
|
+
it("should throw descriptive error for invalid bundle", async () => {
|
|
389
|
+
await expect(index_1.FhirBundleSdk.create(fhir_bundles_1.invalidBundleWrongType)).rejects.toThrow("Invalid bundle: resourceType must be 'Bundle'");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
describe("FR-7.3: Reference resolution methods never throw errors, only return undefined for invalid references", () => {
|
|
393
|
+
it("should return undefined instead of throwing for invalid references", async () => {
|
|
394
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithBrokenReferences);
|
|
395
|
+
const observations = sdk.getObservations();
|
|
396
|
+
expect(() => observations[0]?.getSubject()).not.toThrow();
|
|
397
|
+
expect(observations[0]?.getSubject()).toBeUndefined();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
describe("Performance Requirements", () => {
|
|
402
|
+
describe("FR-9.1: Resource lookup by ID completes in O(1) time", () => {
|
|
403
|
+
it("should perform ID lookup in O(1) time", async () => {
|
|
404
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
405
|
+
const start = performance.now();
|
|
406
|
+
sdk.getResourceById("patient-123");
|
|
407
|
+
const end = performance.now();
|
|
408
|
+
expect(end - start).toBeLessThan(fhir_bundles_1.CONSTANT_TIME_EXPECTED_THRESHOLD_MS);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
describe("FR-9.2: Type-specific getters complete in O(n) time where n is number of resources of that type", () => {
|
|
412
|
+
it("should perform type-specific queries efficiently", async () => {
|
|
413
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.mixedResourceTypesBundle);
|
|
414
|
+
const start = performance.now();
|
|
415
|
+
const patients = sdk.getPatients();
|
|
416
|
+
const end = performance.now();
|
|
417
|
+
expect(patients).toHaveLength(2);
|
|
418
|
+
expect(end - start).toBeLessThan(fhir_bundles_1.CONSTANT_TIME_EXPECTED_THRESHOLD_MS * 2); // Should be fast for small bundles
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
describe("FR-9.4: Reference resolution per reference completes in O(1) time", () => {
|
|
422
|
+
it("should resolve references in O(1) time", async () => {
|
|
423
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
424
|
+
const observations = sdk.getObservations();
|
|
425
|
+
const start = performance.now();
|
|
426
|
+
observations[0]?.getSubject();
|
|
427
|
+
const end = performance.now();
|
|
428
|
+
expect(end - start).toBeLessThan(fhir_bundles_1.CONSTANT_TIME_EXPECTED_THRESHOLD_MS);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
describe("Reference Walking (BFS)", () => {
|
|
433
|
+
describe("Basic single-level traversal", () => {
|
|
434
|
+
it("should walk references from an Observation to related resources", async () => {
|
|
435
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
436
|
+
const observation = sdk.getObservationById("observation-001");
|
|
437
|
+
if (observation === undefined) {
|
|
438
|
+
throw new Error("Observation not found");
|
|
439
|
+
}
|
|
440
|
+
const result = sdk.walkReferences(observation);
|
|
441
|
+
expect(result).toBeDefined();
|
|
442
|
+
expect(result.startResource).toBe(observation);
|
|
443
|
+
expect(result.allResources).toBeDefined();
|
|
444
|
+
expect(Array.isArray(result.allResources)).toBe(true);
|
|
445
|
+
// Should include the observation itself plus referenced resources
|
|
446
|
+
// Observation references: subject (Patient), encounter (Encounter), performer (Practitioner)
|
|
447
|
+
expect(result.allResources.length).toBeGreaterThanOrEqual(4);
|
|
448
|
+
// Check that referenced resources are included
|
|
449
|
+
const resourceIds = result.allResources.map(r => r.id);
|
|
450
|
+
expect(resourceIds).toContain("observation-001"); // start resource
|
|
451
|
+
expect(resourceIds).toContain("patient-123"); // subject
|
|
452
|
+
expect(resourceIds).toContain("encounter-789"); // encounter
|
|
453
|
+
expect(resourceIds).toContain("practitioner-456"); // performer
|
|
454
|
+
});
|
|
455
|
+
it("should organize resources by depth level", async () => {
|
|
456
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
457
|
+
const observation = sdk.getObservationById("observation-001");
|
|
458
|
+
if (observation === undefined) {
|
|
459
|
+
throw new Error("Observation not found");
|
|
460
|
+
}
|
|
461
|
+
const result = sdk.walkReferences(observation);
|
|
462
|
+
expect(result.resourcesByDepth).toBeDefined();
|
|
463
|
+
expect(result.resourcesByDepth instanceof Map).toBe(true);
|
|
464
|
+
// Depth 0 should contain the starting resource
|
|
465
|
+
const depth0 = result.resourcesByDepth.get(0);
|
|
466
|
+
expect(depth0).toBeDefined();
|
|
467
|
+
expect(depth0?.length).toBe(1);
|
|
468
|
+
if (depth0 && depth0[0]) {
|
|
469
|
+
expect(depth0[0].id).toBe("observation-001");
|
|
470
|
+
}
|
|
471
|
+
// Depth 1 should contain directly referenced resources
|
|
472
|
+
const depth1 = result.resourcesByDepth.get(1);
|
|
473
|
+
if (depth1 === undefined) {
|
|
474
|
+
throw new Error("Depth 1 not found");
|
|
475
|
+
}
|
|
476
|
+
expect(depth1.length).toBeGreaterThan(0);
|
|
477
|
+
});
|
|
478
|
+
it("should report the depth reached", async () => {
|
|
479
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
480
|
+
const observation = sdk.getObservationById("observation-001");
|
|
481
|
+
if (observation === undefined) {
|
|
482
|
+
throw new Error("Observation not found");
|
|
483
|
+
}
|
|
484
|
+
const result = sdk.walkReferences(observation);
|
|
485
|
+
expect(result.depthReached).toBeGreaterThanOrEqual(0);
|
|
486
|
+
expect(typeof result.depthReached).toBe("number");
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
describe("Multi-level traversal", () => {
|
|
490
|
+
it("should traverse multiple levels from DiagnosticReport", async () => {
|
|
491
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
492
|
+
const diagnosticReport = sdk.getDiagnosticReportById("diagnostic-report-002");
|
|
493
|
+
if (diagnosticReport === undefined) {
|
|
494
|
+
throw new Error("DiagnosticReport not found");
|
|
495
|
+
}
|
|
496
|
+
const result = sdk.walkReferences(diagnosticReport);
|
|
497
|
+
// DiagnosticReport -> Observation (level 1) -> Patient/Encounter/Practitioner (level 2)
|
|
498
|
+
expect(result.depthReached).toBeGreaterThanOrEqual(1);
|
|
499
|
+
const allResourceIds = result.allResources.map(r => r.id);
|
|
500
|
+
expect(allResourceIds).toContain("diagnostic-report-002"); // start
|
|
501
|
+
expect(allResourceIds).toContain("observation-001"); // result reference
|
|
502
|
+
expect(allResourceIds).toContain("patient-123"); // from observation
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
describe("Include start resource option", () => {
|
|
506
|
+
it("should include start resource by default", async () => {
|
|
507
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
508
|
+
const observation = sdk.getObservationById("observation-001");
|
|
509
|
+
if (observation === undefined) {
|
|
510
|
+
throw new Error("Observation not found");
|
|
511
|
+
}
|
|
512
|
+
const result = sdk.walkReferences(observation);
|
|
513
|
+
expect(result.allResources.map(r => r.id)).toContain("observation-001");
|
|
514
|
+
});
|
|
515
|
+
it("should exclude start resource when includeStartResource is false", async () => {
|
|
516
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
517
|
+
const observation = sdk.getObservationById("observation-001");
|
|
518
|
+
if (observation === undefined) {
|
|
519
|
+
throw new Error("Observation not found");
|
|
520
|
+
}
|
|
521
|
+
const result = sdk.walkReferences(observation, { includeStartResource: false });
|
|
522
|
+
expect(result.allResources.map(r => r.id)).not.toContain("observation-001");
|
|
523
|
+
// But should still have referenced resources
|
|
524
|
+
expect(result.allResources.length).toBeGreaterThan(0);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
describe("Max depth limiting", () => {
|
|
528
|
+
it("should respect maxDepth of 0 (only start resource)", async () => {
|
|
529
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
530
|
+
const observation = sdk.getObservationById("observation-001");
|
|
531
|
+
if (observation === undefined) {
|
|
532
|
+
throw new Error("Observation not found");
|
|
533
|
+
}
|
|
534
|
+
const result = sdk.walkReferences(observation, { maxDepth: 0 });
|
|
535
|
+
expect(result.depthReached).toBe(0);
|
|
536
|
+
expect(result.allResources.length).toBe(1);
|
|
537
|
+
if (result.allResources[0]) {
|
|
538
|
+
expect(result.allResources[0].id).toBe("observation-001");
|
|
539
|
+
}
|
|
540
|
+
expect(result.resourcesByDepth.size).toBe(1);
|
|
541
|
+
});
|
|
542
|
+
it("should respect maxDepth of 1 (start + direct references)", async () => {
|
|
543
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
544
|
+
const observation = sdk.getObservationById("observation-001");
|
|
545
|
+
if (observation === undefined) {
|
|
546
|
+
throw new Error("Observation not found");
|
|
547
|
+
}
|
|
548
|
+
const result = sdk.walkReferences(observation, { maxDepth: 1 });
|
|
549
|
+
expect(result.depthReached).toBe(1);
|
|
550
|
+
// Should have observation + its direct references (patient, encounter, practitioner)
|
|
551
|
+
expect(result.allResources.length).toBeGreaterThanOrEqual(4);
|
|
552
|
+
expect(result.resourcesByDepth.size).toBeGreaterThanOrEqual(2);
|
|
553
|
+
// Should have depth 0 and depth 1
|
|
554
|
+
expect(result.resourcesByDepth.has(0)).toBe(true);
|
|
555
|
+
expect(result.resourcesByDepth.has(1)).toBe(true);
|
|
556
|
+
// Should not have depth 2
|
|
557
|
+
expect(result.resourcesByDepth.has(2)).toBe(false);
|
|
558
|
+
});
|
|
559
|
+
it("should respect maxDepth of 2 from DiagnosticReport", async () => {
|
|
560
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
561
|
+
const diagnosticReport = sdk.getDiagnosticReportById("diagnostic-report-002");
|
|
562
|
+
if (diagnosticReport === undefined) {
|
|
563
|
+
throw new Error("DiagnosticReport not found");
|
|
564
|
+
}
|
|
565
|
+
const result = sdk.walkReferences(diagnosticReport, { maxDepth: 2 });
|
|
566
|
+
// Note: In validCompleteBundle, DiagnosticReport references form a tightly connected graph
|
|
567
|
+
// where all resources at depth 2 have already been visited at depth 1, so depthReached is 1
|
|
568
|
+
expect(result.depthReached).toBeGreaterThanOrEqual(1);
|
|
569
|
+
expect(result.resourcesByDepth.has(0)).toBe(true); // DiagnosticReport
|
|
570
|
+
expect(result.resourcesByDepth.has(1)).toBe(true); // Observation, Patient, Encounter, Practitioner
|
|
571
|
+
// Depth 2 may or may not exist depending on the reference structure
|
|
572
|
+
expect(result.resourcesByDepth.has(3)).toBe(false); // Should not exist
|
|
573
|
+
// Verify we got all expected resources
|
|
574
|
+
const allResourceIds = result.allResources.map(r => r.id);
|
|
575
|
+
expect(allResourceIds).toContain("diagnostic-report-002");
|
|
576
|
+
expect(allResourceIds).toContain("observation-001");
|
|
577
|
+
expect(allResourceIds).toContain("patient-123");
|
|
578
|
+
});
|
|
579
|
+
it("should handle Infinity maxDepth (default)", async () => {
|
|
580
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
581
|
+
const observation = sdk.getObservationById("observation-001");
|
|
582
|
+
if (observation === undefined) {
|
|
583
|
+
throw new Error("Observation not found");
|
|
584
|
+
}
|
|
585
|
+
const resultWithInfinity = sdk.walkReferences(observation, { maxDepth: Infinity });
|
|
586
|
+
const resultWithoutOption = sdk.walkReferences(observation);
|
|
587
|
+
// Both should traverse all reachable resources
|
|
588
|
+
expect(resultWithInfinity.allResources.length).toBe(resultWithoutOption.allResources.length);
|
|
589
|
+
expect(resultWithInfinity.depthReached).toBe(resultWithoutOption.depthReached);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
describe("Circular reference handling", () => {
|
|
593
|
+
it("should handle circular references without infinite loops", async () => {
|
|
594
|
+
// Create bundle with circular references
|
|
595
|
+
const circularBundle = {
|
|
596
|
+
resourceType: "Bundle",
|
|
597
|
+
type: "collection",
|
|
598
|
+
entry: [
|
|
599
|
+
{
|
|
600
|
+
resource: {
|
|
601
|
+
resourceType: "Organization",
|
|
602
|
+
id: "org-1",
|
|
603
|
+
partOf: { reference: "Organization/org-2" },
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
resource: {
|
|
608
|
+
resourceType: "Organization",
|
|
609
|
+
id: "org-2",
|
|
610
|
+
partOf: { reference: "Organization/org-1" },
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
const sdk = await index_1.FhirBundleSdk.create(circularBundle);
|
|
616
|
+
const org1 = sdk.getOrganizationById("org-1");
|
|
617
|
+
// Should complete without hanging
|
|
618
|
+
if (org1 === undefined) {
|
|
619
|
+
throw new Error("Organization not found");
|
|
620
|
+
}
|
|
621
|
+
const result = sdk.walkReferences(org1);
|
|
622
|
+
expect(result).toBeDefined();
|
|
623
|
+
expect(result.allResources.length).toBe(2); // Both organizations
|
|
624
|
+
const ids = result.allResources.map(r => r.id);
|
|
625
|
+
expect(ids).toContain("org-1");
|
|
626
|
+
expect(ids).toContain("org-2");
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
describe("Array reference handling", () => {
|
|
630
|
+
it("should handle array references correctly", async () => {
|
|
631
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
632
|
+
const observation = sdk.getObservationById("observation-001");
|
|
633
|
+
if (observation === undefined) {
|
|
634
|
+
throw new Error("Observation not found");
|
|
635
|
+
}
|
|
636
|
+
const result = sdk.walkReferences(observation);
|
|
637
|
+
// Observation has performer array
|
|
638
|
+
const practitionerId = "practitioner-456";
|
|
639
|
+
expect(result.allResources.map(r => r.id)).toContain(practitionerId);
|
|
640
|
+
});
|
|
641
|
+
it("should handle DiagnosticReport result array", async () => {
|
|
642
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
643
|
+
const diagnosticReport = sdk.getDiagnosticReportById("diagnostic-report-002");
|
|
644
|
+
if (diagnosticReport === undefined) {
|
|
645
|
+
throw new Error("DiagnosticReport not found");
|
|
646
|
+
}
|
|
647
|
+
const result = sdk.walkReferences(diagnosticReport);
|
|
648
|
+
// DiagnosticReport has result array pointing to Observations
|
|
649
|
+
expect(result.allResources.map(r => r.id)).toContain("observation-001");
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
describe("Edge cases", () => {
|
|
653
|
+
it("should handle resources with no references", async () => {
|
|
654
|
+
const bundleWithIsolatedPatient = {
|
|
655
|
+
resourceType: "Bundle",
|
|
656
|
+
type: "collection",
|
|
657
|
+
entry: [
|
|
658
|
+
{
|
|
659
|
+
resource: {
|
|
660
|
+
resourceType: "Patient",
|
|
661
|
+
id: "patient-isolated",
|
|
662
|
+
name: [{ family: "Isolated", given: ["Patient"] }],
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
};
|
|
667
|
+
const sdk = await index_1.FhirBundleSdk.create(bundleWithIsolatedPatient);
|
|
668
|
+
const patient = sdk.getPatientById("patient-isolated");
|
|
669
|
+
if (patient === undefined) {
|
|
670
|
+
throw new Error("Patient not found");
|
|
671
|
+
}
|
|
672
|
+
const result = sdk.walkReferences(patient);
|
|
673
|
+
expect(result.allResources.length).toBe(1);
|
|
674
|
+
expect(result.depthReached).toBe(0);
|
|
675
|
+
expect(result.allResources[0]?.id).toBe("patient-isolated");
|
|
676
|
+
});
|
|
677
|
+
it("should handle resources with broken references", async () => {
|
|
678
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.bundleWithBrokenReferences);
|
|
679
|
+
const observation = sdk.getObservationById("observation-broken");
|
|
680
|
+
// Should not throw, just return what's reachable
|
|
681
|
+
if (observation === undefined) {
|
|
682
|
+
throw new Error("Observation not found");
|
|
683
|
+
}
|
|
684
|
+
const result = sdk.walkReferences(observation);
|
|
685
|
+
expect(result).toBeDefined();
|
|
686
|
+
expect(result.startResource.id).toBe("observation-broken");
|
|
687
|
+
});
|
|
688
|
+
it("should handle empty options", async () => {
|
|
689
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
690
|
+
const observation = sdk.getObservationById("observation-001");
|
|
691
|
+
if (observation === undefined) {
|
|
692
|
+
throw new Error("Observation not found");
|
|
693
|
+
}
|
|
694
|
+
const result = sdk.walkReferences(observation, {});
|
|
695
|
+
expect(result).toBeDefined();
|
|
696
|
+
expect(result.allResources.length).toBeGreaterThan(0);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
describe("LLM Context Generation", () => {
|
|
701
|
+
describe("stripNonClinicalData", () => {
|
|
702
|
+
it("should remove meta field", () => {
|
|
703
|
+
const patient = {
|
|
704
|
+
resourceType: "Patient",
|
|
705
|
+
id: "patient-123",
|
|
706
|
+
meta: {
|
|
707
|
+
lastUpdated: "2023-01-01T00:00:00Z",
|
|
708
|
+
versionId: "1",
|
|
709
|
+
source: "http://example.com",
|
|
710
|
+
},
|
|
711
|
+
name: [{ family: "Smith", given: ["John"] }],
|
|
712
|
+
};
|
|
713
|
+
const cleaned = index_1.FhirBundleSdk.stripNonClinicalData(patient);
|
|
714
|
+
expect(cleaned.meta).toBeUndefined();
|
|
715
|
+
expect(cleaned.name).toEqual([{ family: "Smith", given: ["John"] }]);
|
|
716
|
+
expect(cleaned.id).toBeUndefined();
|
|
717
|
+
});
|
|
718
|
+
it("should remove extension field", () => {
|
|
719
|
+
const patient = {
|
|
720
|
+
resourceType: "Patient",
|
|
721
|
+
id: "patient-123",
|
|
722
|
+
extension: [
|
|
723
|
+
{
|
|
724
|
+
url: "http://example.com/extension",
|
|
725
|
+
valueString: "some extension",
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
name: [{ family: "Smith", given: ["John"] }],
|
|
729
|
+
};
|
|
730
|
+
const cleaned = index_1.FhirBundleSdk.stripNonClinicalData(patient);
|
|
731
|
+
expect(cleaned.extension).toBeUndefined();
|
|
732
|
+
expect(cleaned.name).toEqual([{ family: "Smith", given: ["John"] }]);
|
|
733
|
+
});
|
|
734
|
+
it("should remove text field", () => {
|
|
735
|
+
const observation = {
|
|
736
|
+
resourceType: "Observation",
|
|
737
|
+
id: "obs-123",
|
|
738
|
+
status: "final",
|
|
739
|
+
code: {
|
|
740
|
+
coding: [{ system: "http://loinc.org", code: "12345" }],
|
|
741
|
+
},
|
|
742
|
+
text: {
|
|
743
|
+
status: "generated",
|
|
744
|
+
div: "<div>Some narrative</div>",
|
|
745
|
+
},
|
|
746
|
+
};
|
|
747
|
+
const cleaned = index_1.FhirBundleSdk.stripNonClinicalData(observation);
|
|
748
|
+
expect(cleaned.text).toBeUndefined();
|
|
749
|
+
expect(cleaned.code).toBeDefined();
|
|
750
|
+
expect(cleaned.status).toBe("final");
|
|
751
|
+
});
|
|
752
|
+
it("should not mutate the original resource", () => {
|
|
753
|
+
const patient = {
|
|
754
|
+
resourceType: "Patient",
|
|
755
|
+
id: "patient-123",
|
|
756
|
+
meta: {
|
|
757
|
+
lastUpdated: "2023-01-01T00:00:00Z",
|
|
758
|
+
},
|
|
759
|
+
name: [{ family: "Smith", given: ["John"] }],
|
|
760
|
+
};
|
|
761
|
+
const cleaned = index_1.FhirBundleSdk.stripNonClinicalData(patient);
|
|
762
|
+
// Original should be unchanged
|
|
763
|
+
expect(patient.meta).toBeDefined();
|
|
764
|
+
expect(patient.id).toBe("patient-123");
|
|
765
|
+
// Cleaned should have removed them
|
|
766
|
+
expect(cleaned.meta).toBeUndefined();
|
|
767
|
+
expect(cleaned.id).toBeUndefined();
|
|
768
|
+
});
|
|
769
|
+
it("should handle nested extension fields", () => {
|
|
770
|
+
const patient = {
|
|
771
|
+
resourceType: "Patient",
|
|
772
|
+
id: "patient-123",
|
|
773
|
+
name: [
|
|
774
|
+
{
|
|
775
|
+
family: "Smith",
|
|
776
|
+
given: ["John"],
|
|
777
|
+
extension: [
|
|
778
|
+
{
|
|
779
|
+
url: "http://example.com/name-extension",
|
|
780
|
+
valueString: "some value",
|
|
781
|
+
},
|
|
782
|
+
],
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
};
|
|
786
|
+
const cleaned = index_1.FhirBundleSdk.stripNonClinicalData(patient);
|
|
787
|
+
expect(cleaned.name?.[0]?.extension).toBeUndefined();
|
|
788
|
+
expect(cleaned.name?.[0]?.family).toBe("Smith");
|
|
789
|
+
});
|
|
790
|
+
it("should remove id field", () => {
|
|
791
|
+
const patient = {
|
|
792
|
+
resourceType: "Patient",
|
|
793
|
+
id: "patient-123",
|
|
794
|
+
name: [{ family: "Smith", given: ["John"] }],
|
|
795
|
+
};
|
|
796
|
+
const cleaned = index_1.FhirBundleSdk.stripNonClinicalData(patient);
|
|
797
|
+
expect(cleaned.id).toBeUndefined();
|
|
798
|
+
expect(cleaned.name).toEqual([{ family: "Smith", given: ["John"] }]);
|
|
799
|
+
});
|
|
800
|
+
it("should remove identifier field", () => {
|
|
801
|
+
const patient = {
|
|
802
|
+
resourceType: "Patient",
|
|
803
|
+
id: "patient-123",
|
|
804
|
+
identifier: [
|
|
805
|
+
{
|
|
806
|
+
system: "http://hospital.example.org",
|
|
807
|
+
value: "123456",
|
|
808
|
+
},
|
|
809
|
+
],
|
|
810
|
+
name: [{ family: "Smith", given: ["John"] }],
|
|
811
|
+
};
|
|
812
|
+
const cleaned = index_1.FhirBundleSdk.stripNonClinicalData(patient);
|
|
813
|
+
expect(cleaned.identifier).toBeUndefined();
|
|
814
|
+
expect(cleaned.name).toEqual([{ family: "Smith", given: ["John"] }]);
|
|
815
|
+
});
|
|
816
|
+
it("should remove all reference fields from REFERENCE_METHOD_MAPPING", () => {
|
|
817
|
+
const observation = {
|
|
818
|
+
resourceType: "Observation",
|
|
819
|
+
id: "obs-123",
|
|
820
|
+
status: "final",
|
|
821
|
+
code: {
|
|
822
|
+
coding: [{ system: "http://loinc.org", code: "12345" }],
|
|
823
|
+
},
|
|
824
|
+
subject: { reference: "Patient/patient-123" },
|
|
825
|
+
encounter: { reference: "Encounter/encounter-456" },
|
|
826
|
+
performer: [{ reference: "Practitioner/practitioner-789" }],
|
|
827
|
+
basedOn: [{ reference: "ServiceRequest/sr-001" }],
|
|
828
|
+
};
|
|
829
|
+
const cleaned = index_1.FhirBundleSdk.stripNonClinicalData(observation);
|
|
830
|
+
// All reference fields should be removed
|
|
831
|
+
expect(cleaned.subject).toBeUndefined();
|
|
832
|
+
expect(cleaned.encounter).toBeUndefined();
|
|
833
|
+
expect(cleaned.performer).toBeUndefined();
|
|
834
|
+
expect(cleaned.basedOn).toBeUndefined();
|
|
835
|
+
// Clinical data should remain
|
|
836
|
+
expect(cleaned.status).toBe("final");
|
|
837
|
+
expect(cleaned.code).toBeDefined();
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
describe("generateLLMContext", () => {
|
|
841
|
+
it("should generate structured text format by default", async () => {
|
|
842
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
843
|
+
const observation = sdk.getObservationById("observation-001");
|
|
844
|
+
if (observation === undefined) {
|
|
845
|
+
throw new Error("Observation not found");
|
|
846
|
+
}
|
|
847
|
+
const context = sdk.generateLLMContext(observation);
|
|
848
|
+
expect(context).toContain("PRIMARY RESOURCE:");
|
|
849
|
+
expect(context).toContain("[Type: Observation");
|
|
850
|
+
expect(context).toContain("DIRECTLY REFERENCED");
|
|
851
|
+
});
|
|
852
|
+
it("should respect maxDepth option", async () => {
|
|
853
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
854
|
+
const observation = sdk.getObservationById("observation-001");
|
|
855
|
+
if (observation === undefined) {
|
|
856
|
+
throw new Error("Observation not found");
|
|
857
|
+
}
|
|
858
|
+
const context = sdk.generateLLMContext(observation, { maxDepth: 0 });
|
|
859
|
+
expect(context).toContain("PRIMARY RESOURCE:");
|
|
860
|
+
expect(context).not.toContain("DIRECTLY REFERENCED");
|
|
861
|
+
});
|
|
862
|
+
it("should generate JSON format when specified", async () => {
|
|
863
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
864
|
+
const observation = sdk.getObservationById("observation-001");
|
|
865
|
+
if (observation === undefined) {
|
|
866
|
+
throw new Error("Observation not found");
|
|
867
|
+
}
|
|
868
|
+
const context = sdk.generateLLMContext(observation, { format: "json" });
|
|
869
|
+
expect(() => JSON.parse(context)).not.toThrow();
|
|
870
|
+
const parsed = JSON.parse(context);
|
|
871
|
+
expect(parsed.primaryResource).toBeDefined();
|
|
872
|
+
expect(parsed.primaryResource.resourceType).toBe("Observation");
|
|
873
|
+
});
|
|
874
|
+
it("should strip non-clinical data from all resources", async () => {
|
|
875
|
+
const bundleWithMeta = {
|
|
876
|
+
resourceType: "Bundle",
|
|
877
|
+
type: "collection",
|
|
878
|
+
entry: [
|
|
879
|
+
{
|
|
880
|
+
resource: {
|
|
881
|
+
resourceType: "Observation",
|
|
882
|
+
id: "obs-with-meta",
|
|
883
|
+
status: "final",
|
|
884
|
+
code: {
|
|
885
|
+
coding: [{ system: "http://loinc.org", code: "12345" }],
|
|
886
|
+
},
|
|
887
|
+
meta: {
|
|
888
|
+
lastUpdated: "2023-01-01T00:00:00Z",
|
|
889
|
+
},
|
|
890
|
+
extension: [
|
|
891
|
+
{
|
|
892
|
+
url: "http://example.com/extension",
|
|
893
|
+
valueString: "test",
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
],
|
|
899
|
+
};
|
|
900
|
+
const sdk = await index_1.FhirBundleSdk.create(bundleWithMeta);
|
|
901
|
+
const observation = sdk.getObservationById("obs-with-meta");
|
|
902
|
+
if (observation === undefined) {
|
|
903
|
+
throw new Error("Observation not found");
|
|
904
|
+
}
|
|
905
|
+
const context = sdk.generateLLMContext(observation, { format: "json" });
|
|
906
|
+
const parsed = JSON.parse(context);
|
|
907
|
+
expect(parsed.primaryResource.meta).toBeUndefined();
|
|
908
|
+
expect(parsed.primaryResource.extension).toBeUndefined();
|
|
909
|
+
expect(parsed.primaryResource.code).toBeDefined();
|
|
910
|
+
});
|
|
911
|
+
it("should group resources by type at each depth", async () => {
|
|
912
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
913
|
+
const diagnosticReport = sdk.getDiagnosticReportById("diagnostic-report-002");
|
|
914
|
+
if (diagnosticReport === undefined) {
|
|
915
|
+
throw new Error("DiagnosticReport not found");
|
|
916
|
+
}
|
|
917
|
+
const context = sdk.generateLLMContext(diagnosticReport, { maxDepth: 2 });
|
|
918
|
+
// Should have resource type groupings
|
|
919
|
+
expect(context).toContain("---");
|
|
920
|
+
expect(context).toMatch(/--- \w+ \(\d+\) ---/);
|
|
921
|
+
});
|
|
922
|
+
it("should handle includeStartResource option", async () => {
|
|
923
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
924
|
+
const observation = sdk.getObservationById("observation-001");
|
|
925
|
+
if (observation === undefined) {
|
|
926
|
+
throw new Error("Observation not found");
|
|
927
|
+
}
|
|
928
|
+
const contextWithStart = sdk.generateLLMContext(observation, {
|
|
929
|
+
includeStartResource: true,
|
|
930
|
+
});
|
|
931
|
+
const contextWithoutStart = sdk.generateLLMContext(observation, {
|
|
932
|
+
includeStartResource: false,
|
|
933
|
+
});
|
|
934
|
+
expect(contextWithStart).toContain("PRIMARY RESOURCE:");
|
|
935
|
+
expect(contextWithoutStart).not.toContain("PRIMARY RESOURCE:");
|
|
936
|
+
expect(contextWithoutStart).toContain("DIRECTLY REFERENCED");
|
|
937
|
+
});
|
|
938
|
+
it("should log resource counts to console", async () => {
|
|
939
|
+
const consoleSpy = jest.spyOn(console, "log").mockImplementation();
|
|
940
|
+
const sdk = await index_1.FhirBundleSdk.create(fhir_bundles_1.validCompleteBundle);
|
|
941
|
+
const observation = sdk.getObservationById("observation-001");
|
|
942
|
+
if (observation === undefined) {
|
|
943
|
+
throw new Error("Observation not found");
|
|
944
|
+
}
|
|
945
|
+
sdk.generateLLMContext(observation);
|
|
946
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("[LLM Context] Total resources discovered:"));
|
|
947
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("[LLM Context] Depth"));
|
|
948
|
+
consoleSpy.mockRestore();
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
//# sourceMappingURL=fhir-bundle-sdk.test.js.map
|