@the-cascade-protocol/cli 0.2.0 → 0.2.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/dist/commands/pod/helpers.d.ts +1 -1
- package/dist/commands/pod/helpers.d.ts.map +1 -1
- package/dist/commands/pod/helpers.js +5 -20
- package/dist/commands/pod/helpers.js.map +1 -1
- package/package.json +17 -5
- package/.dockerignore +0 -7
- package/.eslintrc.json +0 -23
- package/.prettierrc +0 -7
- package/Dockerfile +0 -18
- package/src/commands/capabilities.ts +0 -235
- package/src/commands/conformance.ts +0 -447
- package/src/commands/convert.ts +0 -164
- package/src/commands/pod/export.ts +0 -85
- package/src/commands/pod/helpers.ts +0 -449
- package/src/commands/pod/index.ts +0 -32
- package/src/commands/pod/info.ts +0 -239
- package/src/commands/pod/init.ts +0 -273
- package/src/commands/pod/query.ts +0 -224
- package/src/commands/serve.ts +0 -92
- package/src/commands/validate.ts +0 -303
- package/src/index.ts +0 -58
- package/src/lib/fhir-converter/cascade-to-fhir.ts +0 -369
- package/src/lib/fhir-converter/converters-clinical.ts +0 -446
- package/src/lib/fhir-converter/converters-demographics.ts +0 -270
- package/src/lib/fhir-converter/fhir-to-cascade.ts +0 -82
- package/src/lib/fhir-converter/index.ts +0 -215
- package/src/lib/fhir-converter/types.ts +0 -318
- package/src/lib/mcp/audit.ts +0 -107
- package/src/lib/mcp/server.ts +0 -192
- package/src/lib/mcp/tools.ts +0 -668
- package/src/lib/output.ts +0 -76
- package/src/lib/shacl-validator.ts +0 -314
- package/src/lib/turtle-parser.ts +0 -277
- package/src/shapes/checkup.shapes.ttl +0 -1459
- package/src/shapes/clinical.shapes.ttl +0 -1350
- package/src/shapes/clinical.ttl +0 -1369
- package/src/shapes/core.shapes.ttl +0 -450
- package/src/shapes/core.ttl +0 -603
- package/src/shapes/coverage.shapes.ttl +0 -214
- package/src/shapes/coverage.ttl +0 -182
- package/src/shapes/health.shapes.ttl +0 -697
- package/src/shapes/health.ttl +0 -859
- package/src/shapes/pots.shapes.ttl +0 -481
- package/test-fixtures/fhir-bundle-example.json +0 -216
- package/test-fixtures/fhir-medication-example.json +0 -18
- package/tests/cli.test.ts +0 -126
- package/tests/fhir-converter.test.ts +0 -874
- package/tests/mcp-server.test.ts +0 -396
- package/tests/pod.test.ts +0 -400
- package/tsconfig.json +0 -24
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FHIR -> Cascade converters for demographics and administrative types.
|
|
3
|
-
*
|
|
4
|
-
* Converts:
|
|
5
|
-
* - Patient -> cascade:PatientProfile
|
|
6
|
-
* - Immunization -> health:ImmunizationRecord
|
|
7
|
-
* - Coverage -> coverage:InsurancePlan
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
-
import type { Quad } from 'n3';
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
type ConversionResult,
|
|
15
|
-
NS,
|
|
16
|
-
extractCodings,
|
|
17
|
-
codeableConceptText,
|
|
18
|
-
tripleStr,
|
|
19
|
-
tripleInt,
|
|
20
|
-
tripleType,
|
|
21
|
-
tripleDateTime,
|
|
22
|
-
tripleDate,
|
|
23
|
-
commonTriples,
|
|
24
|
-
quadsToJsonLd,
|
|
25
|
-
} from './types.js';
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Patient converter
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
export function convertPatient(resource: any): ConversionResult & { _quads: Quad[] } {
|
|
32
|
-
const warnings: string[] = [];
|
|
33
|
-
const subjectUri = `urn:uuid:${randomUUID()}`;
|
|
34
|
-
const quads: Quad[] = [];
|
|
35
|
-
|
|
36
|
-
quads.push(tripleType(subjectUri, NS.cascade + 'PatientProfile'));
|
|
37
|
-
quads.push(...commonTriples(subjectUri));
|
|
38
|
-
|
|
39
|
-
if (resource.birthDate) {
|
|
40
|
-
quads.push(tripleDate(subjectUri, NS.cascade + 'dateOfBirth', resource.birthDate));
|
|
41
|
-
const dob = new Date(resource.birthDate);
|
|
42
|
-
const now = new Date();
|
|
43
|
-
let age = now.getFullYear() - dob.getFullYear();
|
|
44
|
-
const monthDiff = now.getMonth() - dob.getMonth();
|
|
45
|
-
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < dob.getDate())) {
|
|
46
|
-
age--;
|
|
47
|
-
}
|
|
48
|
-
quads.push(tripleInt(subjectUri, NS.cascade + 'computedAge', age));
|
|
49
|
-
let ageGroup: string;
|
|
50
|
-
if (age < 18) ageGroup = 'pediatric';
|
|
51
|
-
else if (age < 40) ageGroup = 'young_adult';
|
|
52
|
-
else if (age < 65) ageGroup = 'adult';
|
|
53
|
-
else ageGroup = 'senior';
|
|
54
|
-
quads.push(tripleStr(subjectUri, NS.cascade + 'ageGroup', ageGroup));
|
|
55
|
-
} else {
|
|
56
|
-
warnings.push('No birthDate found in Patient resource');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (resource.gender) {
|
|
60
|
-
const genderMap: Record<string, string> = {
|
|
61
|
-
male: 'male', female: 'female', other: 'intersex', unknown: 'intersex',
|
|
62
|
-
};
|
|
63
|
-
quads.push(tripleStr(subjectUri, NS.cascade + 'biologicalSex', genderMap[resource.gender] ?? resource.gender));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (Array.isArray(resource.address) && resource.address.length > 0) {
|
|
67
|
-
const addr = resource.address[0];
|
|
68
|
-
if (addr.city) quads.push(tripleStr(subjectUri, NS.cascade + 'addressCity', addr.city));
|
|
69
|
-
if (addr.state) quads.push(tripleStr(subjectUri, NS.cascade + 'addressState', addr.state));
|
|
70
|
-
if (addr.postalCode) quads.push(tripleStr(subjectUri, NS.cascade + 'addressPostalCode', addr.postalCode));
|
|
71
|
-
if (addr.country) quads.push(tripleStr(subjectUri, NS.cascade + 'addressCountry', addr.country));
|
|
72
|
-
if (Array.isArray(addr.line)) {
|
|
73
|
-
for (const line of addr.line) {
|
|
74
|
-
quads.push(tripleStr(subjectUri, NS.cascade + 'addressLine', line));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
warnings.push('Patient address flattened onto profile (blank node structure simplified)');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (resource.maritalStatus) {
|
|
81
|
-
const maritalText = codeableConceptText(resource.maritalStatus);
|
|
82
|
-
if (maritalText) {
|
|
83
|
-
const maritalMap: Record<string, string> = {
|
|
84
|
-
S: 'single', M: 'married', D: 'divorced', W: 'widowed',
|
|
85
|
-
A: 'separated', T: 'domestic_partnership', UNK: 'prefer_not_to_say',
|
|
86
|
-
'Never Married': 'single', 'Married': 'married', 'Divorced': 'divorced',
|
|
87
|
-
'Widowed': 'widowed', 'Separated': 'separated',
|
|
88
|
-
};
|
|
89
|
-
const code = resource.maritalStatus.coding?.[0]?.code;
|
|
90
|
-
const mapped = maritalMap[code] ?? maritalMap[maritalText] ?? maritalText.toLowerCase();
|
|
91
|
-
quads.push(tripleStr(subjectUri, NS.cascade + 'maritalStatus', mapped));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (resource.id) {
|
|
96
|
-
quads.push(tripleStr(subjectUri, NS.cascade + 'profileId', resource.id));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
turtle: '',
|
|
101
|
-
jsonld: quadsToJsonLd(quads, 'cascade:PatientProfile'),
|
|
102
|
-
warnings,
|
|
103
|
-
resourceType: 'Patient',
|
|
104
|
-
cascadeType: 'cascade:PatientProfile',
|
|
105
|
-
_quads: quads,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
// Immunization converter
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
export function convertImmunization(resource: any): ConversionResult & { _quads: Quad[] } {
|
|
114
|
-
const warnings: string[] = [];
|
|
115
|
-
const subjectUri = `urn:uuid:${randomUUID()}`;
|
|
116
|
-
const quads: Quad[] = [];
|
|
117
|
-
|
|
118
|
-
quads.push(tripleType(subjectUri, NS.health + 'ImmunizationRecord'));
|
|
119
|
-
quads.push(...commonTriples(subjectUri));
|
|
120
|
-
|
|
121
|
-
const vaccineName = codeableConceptText(resource.vaccineCode) ?? 'Unknown Vaccine';
|
|
122
|
-
quads.push(tripleStr(subjectUri, NS.health + 'vaccineName', vaccineName));
|
|
123
|
-
|
|
124
|
-
if (resource.occurrenceDateTime) {
|
|
125
|
-
quads.push(tripleDateTime(subjectUri, NS.health + 'administrationDate', resource.occurrenceDateTime));
|
|
126
|
-
} else if (resource.occurrenceString) {
|
|
127
|
-
warnings.push(`Immunization date is a string: ${resource.occurrenceString}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
quads.push(tripleStr(subjectUri, NS.health + 'status', resource.status ?? 'completed'));
|
|
131
|
-
|
|
132
|
-
const codings = extractCodings(resource.vaccineCode);
|
|
133
|
-
for (const c of codings) {
|
|
134
|
-
if (c.system === 'http://hl7.org/fhir/sid/cvx' || c.system === 'urn:oid:2.16.840.1.113883.12.292') {
|
|
135
|
-
quads.push(tripleStr(subjectUri, NS.health + 'vaccineCode', `CVX-${c.code}`));
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (resource.manufacturer?.display) {
|
|
141
|
-
quads.push(tripleStr(subjectUri, NS.health + 'manufacturer', resource.manufacturer.display));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (resource.lotNumber) {
|
|
145
|
-
quads.push(tripleStr(subjectUri, NS.health + 'lotNumber', resource.lotNumber));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (resource.doseQuantity) {
|
|
149
|
-
const qty = `${resource.doseQuantity.value} ${resource.doseQuantity.unit ?? ''}`.trim();
|
|
150
|
-
quads.push(tripleStr(subjectUri, NS.health + 'doseQuantity', qty));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (resource.route) {
|
|
154
|
-
const routeText = codeableConceptText(resource.route);
|
|
155
|
-
if (routeText) quads.push(tripleStr(subjectUri, NS.health + 'route', routeText));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (resource.site) {
|
|
159
|
-
const siteText = codeableConceptText(resource.site);
|
|
160
|
-
if (siteText) quads.push(tripleStr(subjectUri, NS.health + 'site', siteText));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (Array.isArray(resource.performer) && resource.performer.length > 0) {
|
|
164
|
-
const performer = resource.performer[0]?.actor?.display;
|
|
165
|
-
if (performer) quads.push(tripleStr(subjectUri, NS.health + 'administeringProvider', performer));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (resource.location?.display) {
|
|
169
|
-
quads.push(tripleStr(subjectUri, NS.health + 'administeringLocation', resource.location.display));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (resource.note && Array.isArray(resource.note)) {
|
|
173
|
-
const noteText = resource.note.map((n: any) => n.text).filter(Boolean).join('; ');
|
|
174
|
-
if (noteText) quads.push(tripleStr(subjectUri, NS.health + 'notes', noteText));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (resource.id) {
|
|
178
|
-
quads.push(tripleStr(subjectUri, NS.health + 'sourceRecordId', resource.id));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
turtle: '',
|
|
183
|
-
jsonld: quadsToJsonLd(quads, 'health:ImmunizationRecord'),
|
|
184
|
-
warnings,
|
|
185
|
-
resourceType: 'Immunization',
|
|
186
|
-
cascadeType: 'health:ImmunizationRecord',
|
|
187
|
-
_quads: quads,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
// Coverage converter
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
|
|
195
|
-
export function convertCoverage(resource: any): ConversionResult & { _quads: Quad[] } {
|
|
196
|
-
const warnings: string[] = [];
|
|
197
|
-
const subjectUri = `urn:uuid:${randomUUID()}`;
|
|
198
|
-
const quads: Quad[] = [];
|
|
199
|
-
|
|
200
|
-
quads.push(tripleType(subjectUri, NS.coverage + 'InsurancePlan'));
|
|
201
|
-
quads.push(...commonTriples(subjectUri));
|
|
202
|
-
|
|
203
|
-
if (Array.isArray(resource.payor) && resource.payor.length > 0) {
|
|
204
|
-
const payorName = resource.payor[0]?.display ?? 'Unknown Insurance';
|
|
205
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'providerName', payorName));
|
|
206
|
-
} else {
|
|
207
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'providerName', 'Unknown Insurance'));
|
|
208
|
-
warnings.push('No payor information found in Coverage resource');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (resource.subscriberId) {
|
|
212
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'memberId', resource.subscriberId));
|
|
213
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'subscriberId', resource.subscriberId));
|
|
214
|
-
} else if (resource.identifier && Array.isArray(resource.identifier) && resource.identifier.length > 0) {
|
|
215
|
-
const memberId = resource.identifier[0]?.value ?? '';
|
|
216
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'memberId', memberId));
|
|
217
|
-
} else {
|
|
218
|
-
warnings.push('No member/subscriber ID found in Coverage resource');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (resource.type) {
|
|
222
|
-
const typeText = resource.type.coding?.[0]?.code ?? codeableConceptText(resource.type) ?? 'primary';
|
|
223
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'coverageType', typeText));
|
|
224
|
-
} else {
|
|
225
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'coverageType', 'primary'));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (Array.isArray(resource.class)) {
|
|
229
|
-
for (const cls of resource.class) {
|
|
230
|
-
const clsType = cls.type?.coding?.[0]?.code ?? '';
|
|
231
|
-
if (clsType === 'group' && cls.value) {
|
|
232
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'groupNumber', cls.value));
|
|
233
|
-
if (cls.name) quads.push(tripleStr(subjectUri, NS.coverage + 'planName', cls.name));
|
|
234
|
-
} else if (clsType === 'plan' && cls.value) {
|
|
235
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'planName', cls.name ?? cls.value));
|
|
236
|
-
} else if (clsType === 'rxbin' && cls.value) {
|
|
237
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'rxBin', cls.value));
|
|
238
|
-
} else if (clsType === 'rxpcn' && cls.value) {
|
|
239
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'rxPcn', cls.value));
|
|
240
|
-
} else if (clsType === 'rxgroup' && cls.value) {
|
|
241
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'rxGroup', cls.value));
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (resource.relationship) {
|
|
247
|
-
const relCode = resource.relationship.coding?.[0]?.code ?? 'self';
|
|
248
|
-
quads.push(tripleStr(subjectUri, NS.coverage + 'subscriberRelationship', relCode));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (resource.period?.start) {
|
|
252
|
-
quads.push(tripleDate(subjectUri, NS.coverage + 'effectiveStart', resource.period.start.substring(0, 10)));
|
|
253
|
-
}
|
|
254
|
-
if (resource.period?.end) {
|
|
255
|
-
quads.push(tripleDate(subjectUri, NS.coverage + 'effectiveEnd', resource.period.end.substring(0, 10)));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (resource.id) {
|
|
259
|
-
quads.push(tripleStr(subjectUri, NS.health + 'sourceRecordId', resource.id));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return {
|
|
263
|
-
turtle: '',
|
|
264
|
-
jsonld: quadsToJsonLd(quads, 'coverage:InsurancePlan'),
|
|
265
|
-
warnings,
|
|
266
|
-
resourceType: 'Coverage',
|
|
267
|
-
cascadeType: 'coverage:InsurancePlan',
|
|
268
|
-
_quads: quads,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FHIR -> Cascade dispatcher and public API.
|
|
3
|
-
*
|
|
4
|
-
* Routes FHIR resources to the appropriate per-type converter and
|
|
5
|
-
* provides the main public conversion functions.
|
|
6
|
-
*
|
|
7
|
-
* Individual converters are in:
|
|
8
|
-
* - converters-clinical.ts Medications, conditions, allergies, observations
|
|
9
|
-
* - converters-demographics.ts Patient, immunization, coverage
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { Quad } from 'n3';
|
|
13
|
-
|
|
14
|
-
import { type ConversionResult, quadsToTurtle } from './types.js';
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
convertMedicationStatement,
|
|
18
|
-
convertCondition,
|
|
19
|
-
convertAllergyIntolerance,
|
|
20
|
-
isVitalSignObservation,
|
|
21
|
-
convertObservationLab,
|
|
22
|
-
convertObservationVital,
|
|
23
|
-
} from './converters-clinical.js';
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
convertPatient,
|
|
27
|
-
convertImmunization,
|
|
28
|
-
convertCoverage,
|
|
29
|
-
} from './converters-demographics.js';
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Main dispatcher: single FHIR resource -> Cascade
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
export function convertFhirResourceToQuads(fhirResource: any): (ConversionResult & { _quads: Quad[] }) | null {
|
|
36
|
-
const resourceType = fhirResource?.resourceType as string | undefined;
|
|
37
|
-
if (!resourceType) return null;
|
|
38
|
-
|
|
39
|
-
switch (resourceType) {
|
|
40
|
-
case 'MedicationStatement':
|
|
41
|
-
case 'MedicationRequest':
|
|
42
|
-
return convertMedicationStatement(fhirResource);
|
|
43
|
-
case 'Condition':
|
|
44
|
-
return convertCondition(fhirResource);
|
|
45
|
-
case 'AllergyIntolerance':
|
|
46
|
-
return convertAllergyIntolerance(fhirResource);
|
|
47
|
-
case 'Observation':
|
|
48
|
-
if (isVitalSignObservation(fhirResource)) {
|
|
49
|
-
return convertObservationVital(fhirResource);
|
|
50
|
-
}
|
|
51
|
-
return convertObservationLab(fhirResource);
|
|
52
|
-
case 'Patient':
|
|
53
|
-
return convertPatient(fhirResource);
|
|
54
|
-
case 'Immunization':
|
|
55
|
-
return convertImmunization(fhirResource);
|
|
56
|
-
case 'Coverage':
|
|
57
|
-
return convertCoverage(fhirResource);
|
|
58
|
-
default:
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function convertFhirToCascade(fhirResource: any): Promise<ConversionResult> {
|
|
64
|
-
const result = convertFhirResourceToQuads(fhirResource);
|
|
65
|
-
if (!result) {
|
|
66
|
-
return {
|
|
67
|
-
turtle: '',
|
|
68
|
-
warnings: [`Unsupported FHIR resource type: ${fhirResource?.resourceType ?? 'unknown'}`],
|
|
69
|
-
resourceType: fhirResource?.resourceType ?? 'unknown',
|
|
70
|
-
cascadeType: 'unknown',
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const turtle = await quadsToTurtle(result._quads);
|
|
75
|
-
return {
|
|
76
|
-
turtle,
|
|
77
|
-
jsonld: result.jsonld,
|
|
78
|
-
warnings: result.warnings,
|
|
79
|
-
resourceType: result.resourceType,
|
|
80
|
-
cascadeType: result.cascadeType,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FHIR conversion utilities.
|
|
3
|
-
*
|
|
4
|
-
* Converts between FHIR R4 JSON and Cascade Protocol RDF (Turtle/JSON-LD).
|
|
5
|
-
*
|
|
6
|
-
* Supported FHIR R4 resource types:
|
|
7
|
-
* - MedicationStatement / MedicationRequest -> health:MedicationRecord
|
|
8
|
-
* - Condition -> health:ConditionRecord
|
|
9
|
-
* - AllergyIntolerance -> health:AllergyRecord
|
|
10
|
-
* - Observation (lab) -> health:LabResultRecord
|
|
11
|
-
* - Observation (vital) -> clinical:VitalSign
|
|
12
|
-
* - Patient -> cascade:PatientProfile
|
|
13
|
-
* - Immunization -> health:ImmunizationRecord
|
|
14
|
-
* - Coverage -> coverage:InsurancePlan
|
|
15
|
-
*
|
|
16
|
-
* Zero network calls. All conversion is local.
|
|
17
|
-
*
|
|
18
|
-
* This module re-exports all public API for backward compatibility.
|
|
19
|
-
* Internal implementation is split across:
|
|
20
|
-
* - types.ts Shared types, namespaces, and quad helpers
|
|
21
|
-
* - fhir-to-cascade.ts FHIR -> Cascade converters
|
|
22
|
-
* - cascade-to-fhir.ts Cascade -> FHIR converters
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import type { Quad } from 'n3';
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
type InputFormat,
|
|
29
|
-
type OutputFormat,
|
|
30
|
-
type ConversionResult,
|
|
31
|
-
type BatchConversionResult,
|
|
32
|
-
SUPPORTED_TYPES,
|
|
33
|
-
quadsToTurtle,
|
|
34
|
-
quadsToJsonLd,
|
|
35
|
-
} from './types.js';
|
|
36
|
-
|
|
37
|
-
import { convertFhirResourceToQuads } from './fhir-to-cascade.js';
|
|
38
|
-
import { convertCascadeToFhir } from './cascade-to-fhir.js';
|
|
39
|
-
|
|
40
|
-
// Re-export public types
|
|
41
|
-
export type { InputFormat, OutputFormat, ConversionResult, BatchConversionResult };
|
|
42
|
-
|
|
43
|
-
// Re-export public functions from sub-modules
|
|
44
|
-
export { convertFhirResourceToQuads, convertFhirToCascade } from './fhir-to-cascade.js';
|
|
45
|
-
export { convertCascadeToFhir } from './cascade-to-fhir.js';
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Batch conversion (FHIR Bundle support)
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Convert an entire FHIR input (single resource or Bundle) to Cascade format.
|
|
53
|
-
*/
|
|
54
|
-
export async function convert(
|
|
55
|
-
input: string,
|
|
56
|
-
from: InputFormat,
|
|
57
|
-
to: OutputFormat,
|
|
58
|
-
outputSerialization: 'turtle' | 'jsonld' = 'turtle',
|
|
59
|
-
): Promise<BatchConversionResult> {
|
|
60
|
-
const warnings: string[] = [];
|
|
61
|
-
const errors: string[] = [];
|
|
62
|
-
const results: ConversionResult[] = [];
|
|
63
|
-
|
|
64
|
-
if (from === 'fhir' && (to === 'cascade' || to === 'turtle' || to === 'jsonld')) {
|
|
65
|
-
// FHIR -> Cascade
|
|
66
|
-
let parsed: any;
|
|
67
|
-
try {
|
|
68
|
-
parsed = JSON.parse(input);
|
|
69
|
-
} catch {
|
|
70
|
-
return {
|
|
71
|
-
success: false, output: '', format: to, resourceCount: 0,
|
|
72
|
-
warnings: [], errors: ['Invalid JSON input'], results: [],
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Collect resources from Bundle or single resource
|
|
77
|
-
const fhirResources: any[] = [];
|
|
78
|
-
if (parsed.resourceType === 'Bundle' && Array.isArray(parsed.entry)) {
|
|
79
|
-
for (const entry of parsed.entry) {
|
|
80
|
-
if (entry.resource) fhirResources.push(entry.resource);
|
|
81
|
-
}
|
|
82
|
-
} else if (parsed.resourceType) {
|
|
83
|
-
fhirResources.push(parsed);
|
|
84
|
-
} else {
|
|
85
|
-
return {
|
|
86
|
-
success: false, output: '', format: to, resourceCount: 0,
|
|
87
|
-
warnings: [], errors: ['Input does not appear to be a FHIR resource or Bundle'], results: [],
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const allQuads: Quad[] = [];
|
|
92
|
-
for (const res of fhirResources) {
|
|
93
|
-
if (!SUPPORTED_TYPES.has(res.resourceType)) {
|
|
94
|
-
warnings.push(`Skipping unsupported FHIR resource type: ${res.resourceType}`);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const result = convertFhirResourceToQuads(res);
|
|
98
|
-
if (result) {
|
|
99
|
-
allQuads.push(...result._quads);
|
|
100
|
-
results.push({
|
|
101
|
-
turtle: '', // will be filled with combined output
|
|
102
|
-
jsonld: result.jsonld,
|
|
103
|
-
warnings: result.warnings,
|
|
104
|
-
resourceType: result.resourceType,
|
|
105
|
-
cascadeType: result.cascadeType,
|
|
106
|
-
});
|
|
107
|
-
warnings.push(...result.warnings);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (allQuads.length === 0) {
|
|
112
|
-
return {
|
|
113
|
-
success: false, output: '', format: to, resourceCount: 0,
|
|
114
|
-
warnings, errors: ['No convertible FHIR resources found'], results: [],
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Determine output format
|
|
119
|
-
let output: string;
|
|
120
|
-
if (outputSerialization === 'jsonld' || to === 'jsonld') {
|
|
121
|
-
const jsonLd = quadsToJsonLd(allQuads, results[0]?.cascadeType ?? '');
|
|
122
|
-
output = JSON.stringify(jsonLd, null, 2);
|
|
123
|
-
} else {
|
|
124
|
-
output = await quadsToTurtle(allQuads);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
success: true,
|
|
129
|
-
output,
|
|
130
|
-
format: to === 'cascade' ? (outputSerialization === 'jsonld' ? 'jsonld' : 'turtle') : to,
|
|
131
|
-
resourceCount: results.length,
|
|
132
|
-
warnings,
|
|
133
|
-
errors,
|
|
134
|
-
results,
|
|
135
|
-
};
|
|
136
|
-
} else if (from === 'cascade' && to === 'fhir') {
|
|
137
|
-
// Cascade -> FHIR
|
|
138
|
-
const { resources, warnings: convWarnings } = await convertCascadeToFhir(input);
|
|
139
|
-
warnings.push(...convWarnings);
|
|
140
|
-
|
|
141
|
-
if (resources.length === 0) {
|
|
142
|
-
return {
|
|
143
|
-
success: false, output: '', format: 'fhir', resourceCount: 0,
|
|
144
|
-
warnings, errors: ['No resources converted from Cascade Turtle'], results: [],
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const output = resources.length === 1
|
|
149
|
-
? JSON.stringify(resources[0], null, 2)
|
|
150
|
-
: JSON.stringify({ resourceType: 'Bundle', type: 'collection', entry: resources.map(r => ({ resource: r })) }, null, 2);
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
success: true,
|
|
154
|
-
output,
|
|
155
|
-
format: 'fhir',
|
|
156
|
-
resourceCount: resources.length,
|
|
157
|
-
warnings,
|
|
158
|
-
errors,
|
|
159
|
-
results: resources.map(r => ({
|
|
160
|
-
turtle: '',
|
|
161
|
-
warnings: [],
|
|
162
|
-
resourceType: r.resourceType,
|
|
163
|
-
cascadeType: 'fhir',
|
|
164
|
-
})),
|
|
165
|
-
};
|
|
166
|
-
} else if (from === 'c-cda') {
|
|
167
|
-
return {
|
|
168
|
-
success: false, output: '', format: to, resourceCount: 0,
|
|
169
|
-
warnings: [], errors: ['C-CDA conversion is not yet supported'], results: [],
|
|
170
|
-
};
|
|
171
|
-
} else {
|
|
172
|
-
return {
|
|
173
|
-
success: false, output: '', format: to, resourceCount: 0,
|
|
174
|
-
warnings: [], errors: [`Unsupported conversion: ${from} -> ${to}`], results: [],
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
// Format detection
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Detect the format of input data by inspecting its content.
|
|
185
|
-
*/
|
|
186
|
-
export function detectFormat(input: string): InputFormat | null {
|
|
187
|
-
const trimmed = input.trim();
|
|
188
|
-
|
|
189
|
-
// Check for FHIR JSON (has "resourceType")
|
|
190
|
-
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
191
|
-
try {
|
|
192
|
-
const parsed = JSON.parse(trimmed);
|
|
193
|
-
if (parsed.resourceType) return 'fhir';
|
|
194
|
-
if (parsed['@context'] || parsed['@type']) return 'cascade'; // JSON-LD
|
|
195
|
-
} catch {
|
|
196
|
-
// Not valid JSON
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Check for Turtle (has @prefix declarations or common Cascade namespace URIs)
|
|
201
|
-
if (
|
|
202
|
-
trimmed.includes('@prefix') ||
|
|
203
|
-
trimmed.includes('ns.cascadeprotocol.org') ||
|
|
204
|
-
/^<[^>]+>\s+a\s+/.test(trimmed)
|
|
205
|
-
) {
|
|
206
|
-
return 'cascade';
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Check for C-CDA (XML with ClinicalDocument root)
|
|
210
|
-
if (trimmed.startsWith('<?xml') || trimmed.includes('<ClinicalDocument')) {
|
|
211
|
-
return 'c-cda';
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return null;
|
|
215
|
-
}
|