@healthcare-interoperability/fhir-tools 1.0.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/.babelrc +9 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/FHIRReferenceAssigner.js +132 -0
- package/dist/index.js +12 -0
- package/package.json +27 -0
package/.babelrc
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 quicore
|
|
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,267 @@
|
|
|
1
|
+
# @healthcare-interoperability/fhir-tools
|
|
2
|
+
|
|
3
|
+
Utilities for working with **FHIR (Fast Healthcare Interoperability Resources)** in JavaScript/Node.js, focused on **safe reference assignment, deterministic ID generation, and schema-aware data shaping**.
|
|
4
|
+
|
|
5
|
+
This package is designed for **interop pipelines**, **FHIR normalization layers**, and **HL7 → FHIR transformations**, where nested structures and reference integrity matter.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
* ✅ Assign FHIR references to **deeply nested object paths**
|
|
12
|
+
* ✅ Automatically **creates missing objects and arrays**
|
|
13
|
+
* ✅ Prevents **type conflicts** during assignment
|
|
14
|
+
* ✅ Supports **explicit array declarations** in paths
|
|
15
|
+
* ✅ Generates **deterministic FHIR IDs** using integration context
|
|
16
|
+
* ✅ Produces **clear, path-aware error messages**
|
|
17
|
+
* ✅ Zero runtime dependencies (except ID generator)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @healthcare-interoperability/fhir-tools
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🔧 Dependencies
|
|
30
|
+
|
|
31
|
+
This package depends on:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
@quicore/resource-id
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Make sure it is resolvable in your project.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🚀 Quick Start
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { FHIRReferenceAssigner } from "@healthcare-interoperability/fhir-tools";
|
|
45
|
+
|
|
46
|
+
const assigner = new FHIRReferenceAssigner("my-integration-id");
|
|
47
|
+
|
|
48
|
+
const resource = {};
|
|
49
|
+
|
|
50
|
+
assigner.assign(
|
|
51
|
+
resource,
|
|
52
|
+
["subject"],
|
|
53
|
+
"Patient/123"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
console.log(resource);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Output
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"subject": {
|
|
64
|
+
"id": "generated-fhir-id",
|
|
65
|
+
"type": "Patient",
|
|
66
|
+
"originalReference": "Patient/123"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 🧠 Core Concept
|
|
74
|
+
|
|
75
|
+
FHIR data structures are often:
|
|
76
|
+
|
|
77
|
+
* deeply nested
|
|
78
|
+
* partially populated
|
|
79
|
+
* array-heavy
|
|
80
|
+
* schema-sensitive
|
|
81
|
+
|
|
82
|
+
`FHIRReferenceAssigner` safely **walks a declared path**, creates intermediate structures if needed, validates types, and assigns a **normalized FHIR reference object**.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 🏗️ Path Segments Explained
|
|
87
|
+
|
|
88
|
+
The `pathSegments` argument is an array where each segment describes **how to traverse or create the structure**.
|
|
89
|
+
|
|
90
|
+
| Segment Type | Meaning |
|
|
91
|
+
| ------------ | ---------------------------------------- |
|
|
92
|
+
| `string` | Object key |
|
|
93
|
+
| `number` | Array index |
|
|
94
|
+
| `string[]` | Explicitly declares an array at that key |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 📌 Examples
|
|
99
|
+
|
|
100
|
+
### 1️⃣ Assigning into Nested Objects
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
assigner.assign(
|
|
104
|
+
resource,
|
|
105
|
+
["encounter", "serviceProvider"],
|
|
106
|
+
"Organization/456"
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"encounter": {
|
|
113
|
+
"serviceProvider": {
|
|
114
|
+
"id": "generated-id",
|
|
115
|
+
"type": "Organization",
|
|
116
|
+
"originalReference": "Organization/456"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### 2️⃣ Assigning into Arrays (Implicit)
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
assigner.assign(
|
|
128
|
+
resource,
|
|
129
|
+
["participant", 0, "individual"],
|
|
130
|
+
"Practitioner/789"
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Creates `participant` as an array automatically.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### 3️⃣ Explicit Array Declaration
|
|
139
|
+
|
|
140
|
+
Use `["key"]` to **force an array**, even if no numeric index is present yet.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
assigner.assign(
|
|
144
|
+
resource,
|
|
145
|
+
[["performer"], 0, "actor"],
|
|
146
|
+
"Practitioner/321"
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This ensures `performer` **must be an array**.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 🧩 Reference Object Structure
|
|
155
|
+
|
|
156
|
+
Every assigned reference has the shape:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
{
|
|
160
|
+
id: string; // Generated FHIR ID
|
|
161
|
+
type: string; // Resource type (Patient, Practitioner, etc.)
|
|
162
|
+
originalReference: string; // Original FHIR reference (ResourceType/ID)
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
This preserves traceability while enabling internal ID normalization.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 🔐 Validation & Safety
|
|
171
|
+
|
|
172
|
+
### ✔ Reference Format Validation
|
|
173
|
+
|
|
174
|
+
Only valid FHIR references are accepted:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
ResourceType/ID
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Invalid formats throw immediately.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### ✔ Type Conflict Detection
|
|
185
|
+
|
|
186
|
+
If an existing path segment conflicts with the expected type:
|
|
187
|
+
|
|
188
|
+
* object vs array
|
|
189
|
+
* array vs object
|
|
190
|
+
|
|
191
|
+
An error is thrown with **full path context**, e.g.:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
Path segment 'participant' at 'participant[0]' must be an array
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### ✔ Array Bounds Protection
|
|
200
|
+
|
|
201
|
+
Array indices are validated to avoid runaway memory usage:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
RangeError: Array index out of bounds
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
(Max index: `10000`)
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 🧪 Error Handling
|
|
212
|
+
|
|
213
|
+
Errors are explicit and actionable:
|
|
214
|
+
|
|
215
|
+
* `TypeError` → structural conflicts
|
|
216
|
+
* `RangeError` → invalid array access
|
|
217
|
+
* `Error` → invalid references or ID generation failures
|
|
218
|
+
|
|
219
|
+
This makes the class **safe for automated pipelines and AI-driven transformations**.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 🏥 Typical Use Cases
|
|
224
|
+
|
|
225
|
+
* HL7v2 → FHIR normalization
|
|
226
|
+
* FHIR ingestion pipelines
|
|
227
|
+
* Appointment, Encounter, and Observation consolidation
|
|
228
|
+
* Interop middleware and data harmonization layers
|
|
229
|
+
* Schema-aware AI preprocessing
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 📄 API
|
|
234
|
+
|
|
235
|
+
### `new FHIRReferenceAssigner(integrationId: string)`
|
|
236
|
+
|
|
237
|
+
Creates a new assigner scoped to an integration.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### `assign(baseObject, pathSegments, sourceReference)`
|
|
242
|
+
|
|
243
|
+
Assigns a normalized FHIR reference to the target path.
|
|
244
|
+
|
|
245
|
+
**Parameters**
|
|
246
|
+
|
|
247
|
+
| Name | Type | Description |
|
|
248
|
+
| ----------------- | ---------------------------------- | ---------------------------------- |
|
|
249
|
+
| `baseObject` | `Object` | Root object to mutate |
|
|
250
|
+
| `pathSegments` | `(string \| number \| string[])[]` | Path definition |
|
|
251
|
+
| `sourceReference` | `string` | FHIR reference (`ResourceType/ID`) |
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 📜 License
|
|
256
|
+
|
|
257
|
+
MIT
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 🤝 Contributing
|
|
262
|
+
|
|
263
|
+
Contributions, issues, and design discussions are welcome—especially around:
|
|
264
|
+
|
|
265
|
+
* FHIR R4/R5 alignment
|
|
266
|
+
* Schema validation helpers
|
|
267
|
+
* Bulk reference assignment utilities
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.FHIRReferenceAssigner = void 0;
|
|
7
|
+
var _resourceId = require("@quicore/resource-id");
|
|
8
|
+
/**
|
|
9
|
+
* FHIRReferenceAssigner
|
|
10
|
+
*
|
|
11
|
+
* This class provides utilities for assigning FHIR (Fast Healthcare Interoperability Resources)
|
|
12
|
+
* reference IDs to nested object paths within healthcare data records.
|
|
13
|
+
*/
|
|
14
|
+
class FHIRReferenceAssigner {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new FHIRReferenceAssigner
|
|
17
|
+
*
|
|
18
|
+
* @param {string} integrationId - Integration ID used in FHIRID generation
|
|
19
|
+
*/
|
|
20
|
+
constructor(integrationId) {
|
|
21
|
+
if (!integrationId || typeof integrationId !== 'string') {
|
|
22
|
+
throw new TypeError("integrationId must be a non-empty string");
|
|
23
|
+
}
|
|
24
|
+
this.integrationId = integrationId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Assigns a FHIR reference to a nested object path.
|
|
29
|
+
*
|
|
30
|
+
* This method provides robust handling of nested paths, including:
|
|
31
|
+
* - Creating intermediate objects and arrays as needed
|
|
32
|
+
* - Validating existing field types to prevent conflicts
|
|
33
|
+
* - Supporting explicit array declarations via wrapped syntax
|
|
34
|
+
* - Comprehensive error reporting with full path context
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} baseObject - The root object to modify
|
|
37
|
+
* @param {Array<string | number | string[]>} pathSegments - Array of path segments:
|
|
38
|
+
* - string: object key
|
|
39
|
+
* - number: array index
|
|
40
|
+
* - string[]: declares array at given key
|
|
41
|
+
* @param {string} sourceReference - FHIR reference string (e.g., "Patient/123")
|
|
42
|
+
*
|
|
43
|
+
* @throws {TypeError} If existing values conflict with required types
|
|
44
|
+
* @throws {RangeError} If array index is invalid
|
|
45
|
+
* @throws {Error} If sourceReference is invalid or FHIR ID generation fails
|
|
46
|
+
*/
|
|
47
|
+
assign(baseObject, pathSegments, sourceReference) {
|
|
48
|
+
if (!sourceReference) return;
|
|
49
|
+
if (!FHIRReferenceAssigner.isValidFHIRReference(sourceReference)) {
|
|
50
|
+
throw new Error(`Invalid FHIR reference format: "${sourceReference}". Expected format: "ResourceType/ID"`);
|
|
51
|
+
}
|
|
52
|
+
let current = baseObject;
|
|
53
|
+
const totalSegments = pathSegments.length;
|
|
54
|
+
for (let i = 0; i < totalSegments - 1; i++) {
|
|
55
|
+
const segment = pathSegments[i];
|
|
56
|
+
const nextSegment = pathSegments[i + 1];
|
|
57
|
+
const currentPath = FHIRReferenceAssigner.buildPathString(pathSegments, i + 1);
|
|
58
|
+
let actualSegment = Array.isArray(segment) ? segment[0] : segment;
|
|
59
|
+
const isArray = Array.isArray(segment) || typeof nextSegment === 'number';
|
|
60
|
+
if (current[actualSegment] == null) {
|
|
61
|
+
current[actualSegment] = isArray ? [] : {};
|
|
62
|
+
} else {
|
|
63
|
+
const actualType = typeof current[actualSegment];
|
|
64
|
+
if (isArray && !Array.isArray(current[actualSegment])) {
|
|
65
|
+
throw new TypeError(`Path segment '${actualSegment}' at '${currentPath}' must be an array`);
|
|
66
|
+
}
|
|
67
|
+
if (!isArray && (actualType !== 'object' || Array.isArray(current[actualSegment]))) {
|
|
68
|
+
throw new TypeError(`Path segment '${actualSegment}' at '${currentPath}' must be an object`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof nextSegment === 'number') {
|
|
72
|
+
if (nextSegment < 0 || nextSegment > 10000) {
|
|
73
|
+
throw new RangeError(`Array index out of bounds at '${currentPath}': ${nextSegment}`);
|
|
74
|
+
}
|
|
75
|
+
FHIRReferenceAssigner.ensureArrayLength(current[actualSegment], nextSegment);
|
|
76
|
+
}
|
|
77
|
+
current = current[actualSegment];
|
|
78
|
+
}
|
|
79
|
+
const finalSegment = Array.isArray(pathSegments[totalSegments - 1]) ? pathSegments[totalSegments - 1][0] : pathSegments[totalSegments - 1];
|
|
80
|
+
const [resourceType, sourceId] = sourceReference.split('/');
|
|
81
|
+
try {
|
|
82
|
+
const idGen = new _resourceId.FHIRIDGenerator({
|
|
83
|
+
id: sourceId,
|
|
84
|
+
resourceType
|
|
85
|
+
}).setIntegrationId(this.integrationId);
|
|
86
|
+
current[finalSegment] = {
|
|
87
|
+
id: idGen.generate(),
|
|
88
|
+
type: resourceType,
|
|
89
|
+
originalReference: sourceReference
|
|
90
|
+
};
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new Error(`Failed to generate FHIR ID for reference '${sourceReference}': ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validates that a reference string follows the FHIR reference format (ResourceType/ID)
|
|
98
|
+
*
|
|
99
|
+
* @param {string} reference - The reference string to validate
|
|
100
|
+
* @returns {boolean} True if valid, false otherwise
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
static isValidFHIRReference(reference) {
|
|
104
|
+
if (!reference || typeof reference !== 'string') return false;
|
|
105
|
+
const parts = reference.split('/');
|
|
106
|
+
return parts.length === 2 && parts[0] && parts[1];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Builds a human-readable path string from path segments
|
|
111
|
+
*
|
|
112
|
+
* @param {Array<string | number | string[]>} segments - Path segments
|
|
113
|
+
* @param {number} upTo - Index up to which path should be built
|
|
114
|
+
* @returns {string} Path string
|
|
115
|
+
* @private
|
|
116
|
+
*/
|
|
117
|
+
static buildPathString(segments, upTo = segments.length) {
|
|
118
|
+
return segments.slice(0, upTo).map(seg => Array.isArray(seg) ? `[${seg[0]}]` : typeof seg === 'number' ? `[${seg}]` : seg).join('.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Ensures an array has at least requiredIndex+1 elements
|
|
123
|
+
*
|
|
124
|
+
* @param {Array} arr - The array to extend
|
|
125
|
+
* @param {number} requiredIndex - Index that must be valid
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
static ensureArrayLength(arr, requiredIndex) {
|
|
129
|
+
while (arr.length <= requiredIndex) arr.push({});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.FHIRReferenceAssigner = FHIRReferenceAssigner;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "FHIRReferenceAssigner", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _FHIRReferenceAssigner.FHIRReferenceAssigner;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
var _FHIRReferenceAssigner = require("./FHIRReferenceAssigner");
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@healthcare-interoperability/fhir-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Helper tools for FHIR processing",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "babel src -d dist"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"fhir",
|
|
11
|
+
"healthcare",
|
|
12
|
+
"quicore"
|
|
13
|
+
],
|
|
14
|
+
"author": "quicore",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@quicore/resource-id": "^1.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@babel/cli": "^7.27.0",
|
|
21
|
+
"@babel/core": "^7.26.10",
|
|
22
|
+
"@babel/preset-env": "^7.26.9"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|