fhir-test-data 0.1.0
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 +225 -0
- package/dist/chunk-CBIPVWLL.js +121 -0
- package/dist/chunk-CBIPVWLL.js.map +1 -0
- package/dist/chunk-T46LJ67Q.js +4302 -0
- package/dist/chunk-T46LJ67Q.js.map +1 -0
- package/dist/chunk-U2QJNKBG.js +40 -0
- package/dist/chunk-U2QJNKBG.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +850 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/faults/index.d.ts +33 -0
- package/dist/core/faults/index.js +14 -0
- package/dist/core/faults/index.js.map +1 -0
- package/dist/core/index.d.ts +115 -0
- package/dist/core/index.js +30 -0
- package/dist/core/index.js.map +1 -0
- package/dist/types-BvGNm2YJ.d.ts +132 -0
- package/package.json +102 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Veronez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# fhir-test-data
|
|
2
|
+
|
|
3
|
+
[](https://github.com/dnlbox/fhir-test-data/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/dnlbox/fhir-test-data/actions/workflows/codeql.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/fhir-test-data)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Generate valid FHIR R4 / R4B / R5 test resources with country-aware identifiers — from the CLI, a TypeScript library, or any tool that reads from a pipe.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why this exists
|
|
13
|
+
|
|
14
|
+
FHIR development requires realistic test data, but building it by hand is tedious and error-prone. Most teams either copy production data (a compliance risk) or write custom generators that hardcode US-centric identifiers — useless if your system needs to handle UK NHS numbers, Dutch BSNs, Korean RRNs, or Brazilian CPFs.
|
|
15
|
+
|
|
16
|
+
**fhir-test-data** generates structurally valid FHIR resources across 14 locales, with identifiers that pass each country's official check-digit algorithm. Clinical resources use real LOINC codes with values in realistic clinical ranges and HL7-consistent units of measurement, and SNOMED CT codes for conditions — so the data makes sense medically, not just structurally. All output is seeded and deterministic — the same seed always produces the same data, anywhere it runs.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
### CLI
|
|
23
|
+
|
|
24
|
+
No install required — run directly with `npx`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx fhir-test-data generate patient --locale uk --count 5 --seed 42
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install globally for repeated use:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm add -g fhir-test-data
|
|
34
|
+
# or
|
|
35
|
+
npm install -g fhir-test-data
|
|
36
|
+
|
|
37
|
+
# Generate 5 UK patients to stdout
|
|
38
|
+
fhir-test-data generate patient --locale uk --count 5 --seed 42
|
|
39
|
+
|
|
40
|
+
# Generate a full bundle to a fixtures directory
|
|
41
|
+
fhir-test-data generate bundle --locale au --seed 1 --output ./fixtures/
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Library
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm add fhir-test-data
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { createPatientBuilder, createBundleBuilder } from "fhir-test-data";
|
|
52
|
+
|
|
53
|
+
const [patient] = createPatientBuilder().locale("uk").seed(42).build();
|
|
54
|
+
|
|
55
|
+
const [bundle] = createBundleBuilder()
|
|
56
|
+
.locale("us")
|
|
57
|
+
.seed(42)
|
|
58
|
+
.type("transaction")
|
|
59
|
+
.clinicalResourcesPerPatient(5)
|
|
60
|
+
.build();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## CLI-first, pipe-friendly, AI-friendly
|
|
66
|
+
|
|
67
|
+
The CLI writes to stdout by default — making it a natural fit for shell pipelines, scripting, and AI-assisted workflows. Combine it with `jq`, FHIR validators, load testing tools, or large language models without any glue code.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Inspect a generated resource with jq
|
|
71
|
+
fhir-test-data generate patient --locale uk --seed 42 | jq '.identifier[0]'
|
|
72
|
+
|
|
73
|
+
# Stream NDJSON into a validator or ingest tool
|
|
74
|
+
fhir-test-data generate patient --count 100 --format ndjson | your-fhir-validator --stream
|
|
75
|
+
|
|
76
|
+
# POST to a FHIR server
|
|
77
|
+
fhir-test-data generate patient --locale nl --seed 1 | \
|
|
78
|
+
curl -s -X POST https://your-fhir-server/Patient \
|
|
79
|
+
-H "Content-Type: application/fhir+json" -d @-
|
|
80
|
+
|
|
81
|
+
# Ask an AI assistant to explore or explain a generated bundle
|
|
82
|
+
fhir-test-data generate bundle --locale kr --seed 5 | \
|
|
83
|
+
llm "summarise the clinical findings in this FHIR bundle"
|
|
84
|
+
|
|
85
|
+
# Generate NDJSON for bulk load testing
|
|
86
|
+
fhir-test-data generate patient --locale us --count 1000 --format ndjson --no-pretty \
|
|
87
|
+
> patients.ndjson
|
|
88
|
+
|
|
89
|
+
# Loop across locales in a CI step
|
|
90
|
+
for locale in us uk au de fr nl; do
|
|
91
|
+
fhir-test-data generate bundle --locale $locale --seed 1 --output "./fixtures/$locale/"
|
|
92
|
+
done
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Because output goes to stdout by default, there is nothing to configure — drop it into any pipeline that reads JSON or NDJSON.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Supported locales
|
|
100
|
+
|
|
101
|
+
14 locales with check-digit-validated identifiers and locale-appropriate names, addresses, and phone formats:
|
|
102
|
+
|
|
103
|
+
| Country | Code | Patient identifiers | Algorithm | Practitioner identifiers |
|
|
104
|
+
|---------|------|--------------------|-----------|-----------------------|
|
|
105
|
+
| United States | `us` | SSN, MRN | Format validation | NPI (Luhn) |
|
|
106
|
+
| United Kingdom | `uk` | NHS Number | Modulus 11 | GMC Number, GMP Number |
|
|
107
|
+
| Australia | `au` | IHI, Medicare | Luhn | HPI-I (Luhn) |
|
|
108
|
+
| Canada | `ca` | Ontario HCN | Format validation | — |
|
|
109
|
+
| Germany | `de` | KVID-10 | Format validation | LANR (Modulus 10) |
|
|
110
|
+
| France | `fr` | NIR | Modulus 97 | RPPS (Luhn) |
|
|
111
|
+
| Netherlands | `nl` | BSN | 11-proef | UZI Number |
|
|
112
|
+
| India | `in` | Aadhaar, ABHA | Verhoeff | — |
|
|
113
|
+
| Japan | `jp` | Hospital MRN | Format validation | Doctor License |
|
|
114
|
+
| South Korea | `kr` | RRN | Format + gender encoding | MOHW Doctor License |
|
|
115
|
+
| Mexico | `mx` | CURP | Format validation | Cédula Profesional |
|
|
116
|
+
| Brazil | `br` | CPF | Modulus 11 variant | CRM |
|
|
117
|
+
| Singapore | `sg` | NRIC / FIN | Check letter | SMC Registration |
|
|
118
|
+
| South Africa | `za` | SA ID Number | Luhn | HPCSA Registration |
|
|
119
|
+
|
|
120
|
+
Identifier validation is baked in — generated values always pass the official algorithm for their country. The library also includes tools like [fhir-resource-diff](https://github.com/dnlbox/fhir-resource-diff) for structural comparison and validation of generated fixtures.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Clinical code quality
|
|
125
|
+
|
|
126
|
+
Observations use real LOINC codes with `valueQuantity` in clinically plausible ranges and HL7-consistent units of measurement (e.g. `mm[Hg]` for blood pressure, `kg/m2` for BMI). Conditions use SNOMED CT codes. AllergyIntolerance resources include coded substances. The goal is data that makes sense to a clinician, not just data that passes a schema validator.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Seeded determinism
|
|
131
|
+
|
|
132
|
+
The same seed produces identical output on any machine, any Node version, any CI environment:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
fhir-test-data generate patient --locale uk --seed 42 > a.json
|
|
136
|
+
fhir-test-data generate patient --locale uk --seed 42 > b.json
|
|
137
|
+
diff a.json b.json # empty — identical output
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Reliable for snapshot tests, golden file comparison, and regression test fixtures.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Multi-version FHIR support
|
|
145
|
+
|
|
146
|
+
All builders target R4 (default), R4B, or R5. R5 structural adaptations are applied automatically — `MedicationStatement` becomes `MedicationUsage`, and `AllergyIntolerance.type` becomes a `CodeableConcept`.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
fhir-test-data generate bundle --locale us --seed 1 --fhir-version R5
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Bundle builder
|
|
155
|
+
|
|
156
|
+
The Bundle builder composes all resource types into a single FHIR Bundle with automatic reference wiring using `urn:uuid:` format:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const [bundle] = createBundleBuilder()
|
|
160
|
+
.locale("uk")
|
|
161
|
+
.seed(1)
|
|
162
|
+
.type("transaction")
|
|
163
|
+
.clinicalResourcesPerPatient(5)
|
|
164
|
+
.build();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Each bundle includes: Patient, Organization, Practitioner, PractitionerRole, and N clinical resources (Observations, Conditions, AllergyIntolerance, MedicationStatement). All internal references are consistent — `Observation.subject` → Patient, `Observation.performer[0]` → Practitioner, `Patient.managingOrganization` → Organization.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Fault injection
|
|
172
|
+
|
|
173
|
+
Generate intentionally invalid resources for testing validation pipelines and error handlers:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { withFault } from "fhir-test-data/faults";
|
|
177
|
+
|
|
178
|
+
const [invalid] = withFault(
|
|
179
|
+
createPatientBuilder().locale("uk").seed(42),
|
|
180
|
+
"missing-resource-type"
|
|
181
|
+
).build();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Comparison with Synthea
|
|
187
|
+
|
|
188
|
+
[Synthea](https://github.com/synthetichealth/synthea) is a fantastic open-source patient generator that produces clinically realistic longitudinal patient histories — disease progression, care pathways, medication histories — and has been hugely valuable for healthcare research and simulation work.
|
|
189
|
+
|
|
190
|
+
fhir-test-data serves a different need: TypeScript-native, internationally correct, deterministic fixtures for developer testing workflows.
|
|
191
|
+
|
|
192
|
+
| | fhir-test-data | Synthea |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| Language | TypeScript | Java |
|
|
195
|
+
| Usage | Library + CLI | Standalone tool |
|
|
196
|
+
| International identifiers | 14 locales | US-only |
|
|
197
|
+
| Deterministic output | Yes (seeded PRNG) | Partial |
|
|
198
|
+
| TypeScript integration | Native types | JSON only |
|
|
199
|
+
| Clinical realism | Coded LOINC/SNOMED values | High (disease progression) |
|
|
200
|
+
| Browser-safe core | Yes | No |
|
|
201
|
+
| Pipe-friendly CLI | Yes | No |
|
|
202
|
+
|
|
203
|
+
Use **fhir-test-data** when you need TypeScript-native, internationally correct, deterministic fixtures for unit tests, integration tests, CI pipelines, or FHIR server load testing. Use **Synthea** when you need clinically realistic patient histories with disease progression for research or simulation.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Related resources
|
|
208
|
+
|
|
209
|
+
**[fhir-resource-diff](https://github.com/dnlbox/fhir-resource-diff)** — compare FHIR resources structurally, ignoring irrelevant differences like IDs and timestamps. A natural companion for validating that generated fixtures stay stable across library upgrades:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# Generate fixtures
|
|
213
|
+
fhir-test-data generate bundle --locale uk --seed 1 --output ./fixtures/
|
|
214
|
+
|
|
215
|
+
# Compare against a committed baseline
|
|
216
|
+
fhir-resource-diff compare ./fixtures/Bundle-001.json ./baseline/Bundle-001.json
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Documentation
|
|
222
|
+
|
|
223
|
+
Full API reference, locale details, fault injection guide, and VitePress documentation at [dnlbox.github.io/fhir-test-data](https://dnlbox.github.io/fhir-test-data).
|
|
224
|
+
|
|
225
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pickRandom
|
|
3
|
+
} from "./chunk-U2QJNKBG.js";
|
|
4
|
+
|
|
5
|
+
// src/core/faults/registry.ts
|
|
6
|
+
var missingResourceType = (r) => {
|
|
7
|
+
const { resourceType: _dropped, ...rest } = r;
|
|
8
|
+
return rest;
|
|
9
|
+
};
|
|
10
|
+
var invalidResourceType = (r) => ({
|
|
11
|
+
...r,
|
|
12
|
+
resourceType: "InvalidResourceXYZ"
|
|
13
|
+
});
|
|
14
|
+
var missingId = (r) => {
|
|
15
|
+
const { id: _dropped, ...rest } = r;
|
|
16
|
+
return rest;
|
|
17
|
+
};
|
|
18
|
+
var invalidGender = (r) => {
|
|
19
|
+
if (!("gender" in r)) return r;
|
|
20
|
+
return { ...r, gender: "INVALID_GENDER" };
|
|
21
|
+
};
|
|
22
|
+
var PRIMARY_DATE_FIELD = {
|
|
23
|
+
Patient: "birthDate",
|
|
24
|
+
Observation: "effectiveDateTime",
|
|
25
|
+
Condition: "onsetDateTime",
|
|
26
|
+
AllergyIntolerance: "recordedDate",
|
|
27
|
+
MedicationStatement: "effectiveDateTime",
|
|
28
|
+
MedicationUsage: "effectiveDateTime",
|
|
29
|
+
// R5 rename of MedicationStatement
|
|
30
|
+
Immunization: "occurrenceDateTime"
|
|
31
|
+
};
|
|
32
|
+
var DATE_FIELD_SUFFIX_PATTERN = /(?:Date|DateTime)$/;
|
|
33
|
+
var malformedDate = (r) => {
|
|
34
|
+
const resourceType = r["resourceType"];
|
|
35
|
+
const primaryField = resourceType ? PRIMARY_DATE_FIELD[resourceType] : void 0;
|
|
36
|
+
if (primaryField !== void 0 && primaryField in r) {
|
|
37
|
+
return { ...r, [primaryField]: "not-a-date" };
|
|
38
|
+
}
|
|
39
|
+
for (const key of Object.keys(r)) {
|
|
40
|
+
if (DATE_FIELD_SUFFIX_PATTERN.test(key)) {
|
|
41
|
+
return { ...r, [key]: "not-a-date" };
|
|
42
|
+
}
|
|
43
|
+
if (key.endsWith("Period")) {
|
|
44
|
+
const period = r[key];
|
|
45
|
+
if (period !== null && typeof period === "object" && "start" in period) {
|
|
46
|
+
return { ...r, [key]: { ...period, start: "not-a-date" } };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return r;
|
|
51
|
+
};
|
|
52
|
+
var emptyName = (r) => {
|
|
53
|
+
if (!("name" in r)) return r;
|
|
54
|
+
return { ...r, name: [] };
|
|
55
|
+
};
|
|
56
|
+
var wrongTypeOnField = (r) => {
|
|
57
|
+
if (!("birthDate" in r)) return r;
|
|
58
|
+
return { ...r, birthDate: 19850315 };
|
|
59
|
+
};
|
|
60
|
+
var duplicateIdentifier = (r) => {
|
|
61
|
+
const identifiers = r["identifier"];
|
|
62
|
+
if (!Array.isArray(identifiers) || identifiers.length === 0) return r;
|
|
63
|
+
return { ...r, identifier: [...identifiers, identifiers[0]] };
|
|
64
|
+
};
|
|
65
|
+
var invalidTelecomSystem = (r) => {
|
|
66
|
+
const telecom = r["telecom"];
|
|
67
|
+
if (!Array.isArray(telecom) || telecom.length === 0) return r;
|
|
68
|
+
const first = telecom[0];
|
|
69
|
+
return {
|
|
70
|
+
...r,
|
|
71
|
+
telecom: [{ ...first, system: "fax-machine" }, ...telecom.slice(1)]
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
var FAULT_REGISTRY = {
|
|
75
|
+
"missing-resource-type": missingResourceType,
|
|
76
|
+
"invalid-resource-type": invalidResourceType,
|
|
77
|
+
"missing-id": missingId,
|
|
78
|
+
"invalid-gender": invalidGender,
|
|
79
|
+
"malformed-date": malformedDate,
|
|
80
|
+
"empty-name": emptyName,
|
|
81
|
+
"wrong-type-on-field": wrongTypeOnField,
|
|
82
|
+
"duplicate-identifier": duplicateIdentifier,
|
|
83
|
+
"invalid-telecom-system": invalidTelecomSystem
|
|
84
|
+
};
|
|
85
|
+
var CONCRETE_FAULT_TYPES = Object.keys(
|
|
86
|
+
FAULT_REGISTRY
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// src/core/faults/inject.ts
|
|
90
|
+
function injectFaults(resource, faults, rng) {
|
|
91
|
+
const expanded = faults.map(
|
|
92
|
+
(f) => f === "random" ? pickRandom(CONCRETE_FAULT_TYPES, rng) : f
|
|
93
|
+
);
|
|
94
|
+
const unique = [...new Set(expanded)];
|
|
95
|
+
return unique.reduce(
|
|
96
|
+
(r, fault) => FAULT_REGISTRY[fault](r, rng),
|
|
97
|
+
resource
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/core/faults/types.ts
|
|
102
|
+
var FAULT_TYPES = [
|
|
103
|
+
"missing-resource-type",
|
|
104
|
+
"invalid-resource-type",
|
|
105
|
+
"missing-id",
|
|
106
|
+
"invalid-gender",
|
|
107
|
+
"malformed-date",
|
|
108
|
+
"empty-name",
|
|
109
|
+
"wrong-type-on-field",
|
|
110
|
+
"duplicate-identifier",
|
|
111
|
+
"invalid-telecom-system",
|
|
112
|
+
"random"
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
FAULT_REGISTRY,
|
|
117
|
+
CONCRETE_FAULT_TYPES,
|
|
118
|
+
injectFaults,
|
|
119
|
+
FAULT_TYPES
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=chunk-CBIPVWLL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/faults/registry.ts","../src/core/faults/inject.ts","../src/core/faults/types.ts"],"sourcesContent":["import type { ConcreteFaultType, FaultStrategy } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Individual fault strategies\n// ---------------------------------------------------------------------------\n\nconst missingResourceType: FaultStrategy = (r) => {\n const { resourceType: _dropped, ...rest } = r;\n return rest;\n};\n\nconst invalidResourceType: FaultStrategy = (r) => ({\n ...r,\n resourceType: \"InvalidResourceXYZ\",\n});\n\nconst missingId: FaultStrategy = (r) => {\n const { id: _dropped, ...rest } = r;\n return rest;\n};\n\nconst invalidGender: FaultStrategy = (r) => {\n if (!(\"gender\" in r)) return r;\n return { ...r, gender: \"INVALID_GENDER\" };\n};\n\n// Maps each resource type to its primary date field.\nconst PRIMARY_DATE_FIELD: Record<string, string> = {\n Patient: \"birthDate\",\n Observation: \"effectiveDateTime\",\n Condition: \"onsetDateTime\",\n AllergyIntolerance: \"recordedDate\",\n MedicationStatement: \"effectiveDateTime\",\n MedicationUsage: \"effectiveDateTime\", // R5 rename of MedicationStatement\n Immunization: \"occurrenceDateTime\",\n};\n\nconst DATE_FIELD_SUFFIX_PATTERN = /(?:Date|DateTime)$/;\n\nconst malformedDate: FaultStrategy = (r) => {\n const resourceType = r[\"resourceType\"] as string | undefined;\n const primaryField = resourceType ? PRIMARY_DATE_FIELD[resourceType] : undefined;\n\n // 1. Primary field lookup: if field exists on the resource, corrupt it.\n if (primaryField !== undefined && primaryField in r) {\n return { ...r, [primaryField]: \"not-a-date\" };\n }\n\n // 2. Fallback scan: find first field ending in Date or DateTime and corrupt it.\n for (const key of Object.keys(r)) {\n if (DATE_FIELD_SUFFIX_PATTERN.test(key)) {\n return { ...r, [key]: \"not-a-date\" };\n }\n // Handle Period fields — corrupt .start if present.\n if (key.endsWith(\"Period\")) {\n const period = r[key] as Record<string, unknown> | null | undefined;\n if (period !== null && typeof period === \"object\" && \"start\" in period) {\n return { ...r, [key]: { ...period, start: \"not-a-date\" } };\n }\n }\n }\n\n // 3. No recognised date field — silent no-op.\n return r;\n};\n\nconst emptyName: FaultStrategy = (r) => {\n if (!(\"name\" in r)) return r;\n return { ...r, name: [] };\n};\n\nconst wrongTypeOnField: FaultStrategy = (r) => {\n if (!(\"birthDate\" in r)) return r;\n // Integer instead of ISO string — wrong JSON type for FHIR date field.\n return { ...r, birthDate: 19850315 };\n};\n\nconst duplicateIdentifier: FaultStrategy = (r) => {\n const identifiers = r[\"identifier\"];\n if (!Array.isArray(identifiers) || identifiers.length === 0) return r;\n return { ...r, identifier: [...identifiers, identifiers[0]] };\n};\n\nconst invalidTelecomSystem: FaultStrategy = (r) => {\n const telecom = r[\"telecom\"];\n if (!Array.isArray(telecom) || telecom.length === 0) return r;\n const first = telecom[0] as Record<string, unknown>;\n return {\n ...r,\n telecom: [{ ...first, system: \"fax-machine\" }, ...telecom.slice(1)],\n };\n};\n\n// ---------------------------------------------------------------------------\n// Registry\n// ---------------------------------------------------------------------------\n\nexport const FAULT_REGISTRY: Record<ConcreteFaultType, FaultStrategy> = {\n \"missing-resource-type\": missingResourceType,\n \"invalid-resource-type\": invalidResourceType,\n \"missing-id\": missingId,\n \"invalid-gender\": invalidGender,\n \"malformed-date\": malformedDate,\n \"empty-name\": emptyName,\n \"wrong-type-on-field\": wrongTypeOnField,\n \"duplicate-identifier\": duplicateIdentifier,\n \"invalid-telecom-system\": invalidTelecomSystem,\n};\n\nexport const CONCRETE_FAULT_TYPES = Object.keys(\n FAULT_REGISTRY,\n) as ConcreteFaultType[];\n","import type { FhirResource, RandomFn, FaultType } from \"./types.js\";\nimport { FAULT_REGISTRY, CONCRETE_FAULT_TYPES } from \"./registry.js\";\nimport { pickRandom } from \"@/core/generators/rng.js\";\n\n/**\n * Apply a list of fault types to a FHIR resource.\n *\n * - \"random\" expands to one concrete fault chosen by the seeded RNG.\n * - Duplicate fault types in the list are applied once each (deduped by type).\n * - Faults targeting fields not present on the resource are silent no-ops.\n * - The original resource is never mutated; a new object is returned.\n */\nexport function injectFaults(\n resource: FhirResource,\n faults: FaultType[],\n rng: RandomFn,\n): FhirResource {\n const expanded = faults.map((f) =>\n f === \"random\" ? pickRandom(CONCRETE_FAULT_TYPES, rng) : f,\n );\n\n // Deduplicate while preserving first-occurrence order.\n const unique = [...new Set(expanded)];\n\n return unique.reduce<Record<string, unknown>>(\n (r, fault) => FAULT_REGISTRY[fault](r, rng),\n resource as Record<string, unknown>,\n ) as FhirResource;\n}\n","import type { FhirResource, RandomFn } from \"@/core/types.js\";\n\n/**\n * Concrete fault types — each maps to a specific FHIR violation.\n */\nexport type ConcreteFaultType =\n | \"missing-resource-type\" // remove resourceType entirely\n | \"invalid-resource-type\" // set resourceType to a non-existent value\n | \"missing-id\" // remove id field\n | \"invalid-gender\" // set gender to a value not in the FHIR ValueSet\n | \"malformed-date\" // set birthDate to a non-ISO-8601 value\n | \"empty-name\" // set name to an empty array\n | \"wrong-type-on-field\" // set birthDate to an integer instead of a string\n | \"duplicate-identifier\" // repeat identifier[0] in the identifier array\n | \"invalid-telecom-system\"; // set telecom[0].system to an unrecognised value\n\n/**\n * Full fault type including the \"random\" convenience alias.\n * \"random\" expands to one concrete fault chosen by the seeded RNG.\n */\nexport type FaultType = ConcreteFaultType | \"random\";\n\n/** All valid fault type strings, for CLI validation. */\nexport const FAULT_TYPES: FaultType[] = [\n \"missing-resource-type\",\n \"invalid-resource-type\",\n \"missing-id\",\n \"invalid-gender\",\n \"malformed-date\",\n \"empty-name\",\n \"wrong-type-on-field\",\n \"duplicate-identifier\",\n \"invalid-telecom-system\",\n \"random\",\n];\n\n/**\n * A fault strategy receives a resource as a plain object and an RNG,\n * and returns a new object with the fault applied. Never mutates the input.\n */\nexport type FaultStrategy = (\n resource: Record<string, unknown>,\n rng: RandomFn,\n) => Record<string, unknown>;\n\n// Re-export FhirResource so callers only need one import.\nexport type { FhirResource, RandomFn };\n"],"mappings":";;;;;AAMA,IAAM,sBAAqC,CAAC,MAAM;AAChD,QAAM,EAAE,cAAc,UAAU,GAAG,KAAK,IAAI;AAC5C,SAAO;AACT;AAEA,IAAM,sBAAqC,CAAC,OAAO;AAAA,EACjD,GAAG;AAAA,EACH,cAAc;AAChB;AAEA,IAAM,YAA2B,CAAC,MAAM;AACtC,QAAM,EAAE,IAAI,UAAU,GAAG,KAAK,IAAI;AAClC,SAAO;AACT;AAEA,IAAM,gBAA+B,CAAC,MAAM;AAC1C,MAAI,EAAE,YAAY,GAAI,QAAO;AAC7B,SAAO,EAAE,GAAG,GAAG,QAAQ,iBAAiB;AAC1C;AAGA,IAAM,qBAA6C;AAAA,EACjD,SAAqB;AAAA,EACrB,aAAqB;AAAA,EACrB,WAAqB;AAAA,EACrB,oBAAqB;AAAA,EACrB,qBAAqB;AAAA,EACrB,iBAAqB;AAAA;AAAA,EACrB,cAAqB;AACvB;AAEA,IAAM,4BAA4B;AAElC,IAAM,gBAA+B,CAAC,MAAM;AAC1C,QAAM,eAAe,EAAE,cAAc;AACrC,QAAM,eAAe,eAAe,mBAAmB,YAAY,IAAI;AAGvE,MAAI,iBAAiB,UAAa,gBAAgB,GAAG;AACnD,WAAO,EAAE,GAAG,GAAG,CAAC,YAAY,GAAG,aAAa;AAAA,EAC9C;AAGA,aAAW,OAAO,OAAO,KAAK,CAAC,GAAG;AAChC,QAAI,0BAA0B,KAAK,GAAG,GAAG;AACvC,aAAO,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,aAAa;AAAA,IACrC;AAEA,QAAI,IAAI,SAAS,QAAQ,GAAG;AAC1B,YAAM,SAAS,EAAE,GAAG;AACpB,UAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,WAAW,QAAQ;AACtE,eAAO,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,GAAG,QAAQ,OAAO,aAAa,EAAE;AAAA,MAC3D;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AACT;AAEA,IAAM,YAA2B,CAAC,MAAM;AACtC,MAAI,EAAE,UAAU,GAAI,QAAO;AAC3B,SAAO,EAAE,GAAG,GAAG,MAAM,CAAC,EAAE;AAC1B;AAEA,IAAM,mBAAkC,CAAC,MAAM;AAC7C,MAAI,EAAE,eAAe,GAAI,QAAO;AAEhC,SAAO,EAAE,GAAG,GAAG,WAAW,SAAS;AACrC;AAEA,IAAM,sBAAqC,CAAC,MAAM;AAChD,QAAM,cAAc,EAAE,YAAY;AAClC,MAAI,CAAC,MAAM,QAAQ,WAAW,KAAK,YAAY,WAAW,EAAG,QAAO;AACpE,SAAO,EAAE,GAAG,GAAG,YAAY,CAAC,GAAG,aAAa,YAAY,CAAC,CAAC,EAAE;AAC9D;AAEA,IAAM,uBAAsC,CAAC,MAAM;AACjD,QAAM,UAAU,EAAE,SAAS;AAC3B,MAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,EAAG,QAAO;AAC5D,QAAM,QAAQ,QAAQ,CAAC;AACvB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS,CAAC,EAAE,GAAG,OAAO,QAAQ,cAAc,GAAG,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,EACpE;AACF;AAMO,IAAM,iBAA2D;AAAA,EACtE,yBAA0B;AAAA,EAC1B,yBAA0B;AAAA,EAC1B,cAA0B;AAAA,EAC1B,kBAA0B;AAAA,EAC1B,kBAA0B;AAAA,EAC1B,cAA0B;AAAA,EAC1B,uBAA0B;AAAA,EAC1B,wBAA0B;AAAA,EAC1B,0BAA0B;AAC5B;AAEO,IAAM,uBAAuB,OAAO;AAAA,EACzC;AACF;;;ACnGO,SAAS,aACd,UACA,QACA,KACc;AACd,QAAM,WAAW,OAAO;AAAA,IAAI,CAAC,MAC3B,MAAM,WAAW,WAAW,sBAAsB,GAAG,IAAI;AAAA,EAC3D;AAGA,QAAM,SAAS,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC;AAEpC,SAAO,OAAO;AAAA,IACZ,CAAC,GAAG,UAAU,eAAe,KAAK,EAAE,GAAG,GAAG;AAAA,IAC1C;AAAA,EACF;AACF;;;ACLO,IAAM,cAA2B;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":[]}
|