babelfhir-ts 1.5.0 → 1.5.2
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.
|
@@ -223,7 +223,7 @@ export interface Bundle<T = ${fhirNs}.Resource> {
|
|
|
223
223
|
type: "collection" | "searchset" | "transaction-response" | "transaction" | "batch" | "batch-response" | "document" | "message" | "history";
|
|
224
224
|
total?: number;
|
|
225
225
|
link?: { relation: string; url: string }[];
|
|
226
|
-
entry?: { resource?: T; fullUrl?: string; search?: { mode?: string; score?: number }; request?: { method:
|
|
226
|
+
entry?: { resource?: T; fullUrl?: string; search?: { mode?: string; score?: number }; request?: { method: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; url: string }; response?: { status: string } }[];
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
export declare class FhirResourceReader<T extends ${fhirNs}.Resource, S extends SearchParams = SearchParams> {
|
|
@@ -29,7 +29,7 @@ function ensurePrefabDir(outputDir) {
|
|
|
29
29
|
*/
|
|
30
30
|
export function writePrefabProfile(outputDir, interfaceName, fields, resourceType) {
|
|
31
31
|
const dir = ensurePrefabDir(outputDir);
|
|
32
|
-
const { code, fieldCount } = generatePrefabRenderer(interfaceName, fields);
|
|
32
|
+
const { code, fieldCount } = generatePrefabRenderer(interfaceName, fields, { resourceType });
|
|
33
33
|
const filePath = path.join(dir, `${interfaceName}Prefab.ts`);
|
|
34
34
|
fs.writeFileSync(filePath, code);
|
|
35
35
|
let map = emittedProfiles.get(outputDir);
|
|
@@ -138,7 +138,16 @@ function buildMcpViewsTs(profiles) {
|
|
|
138
138
|
* @see https://babelfhir-ts.dev/generated-code
|
|
139
139
|
*/
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Registry consumed by createFhirMcpServer() to resolve profile-aware
|
|
143
|
+
* table / detail views. Defined inline to avoid a heavy dependency on
|
|
144
|
+
* the full \`@babelfhir-ts/mcp\` package at compile time.
|
|
145
|
+
*/
|
|
146
|
+
export interface FhirViewRegistry {
|
|
147
|
+
table?: (resourceType: string, resources: unknown[]) => unknown | undefined;
|
|
148
|
+
detail?: (resource: unknown) => unknown | undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
142
151
|
${imports.join('\n')}
|
|
143
152
|
|
|
144
153
|
const TABLE_MAP: Record<string, (resources: unknown[]) => unknown> = {
|
|
@@ -72,6 +72,7 @@ function rowAccessor(d) {
|
|
|
72
72
|
return `formatPeriod(${first})`;
|
|
73
73
|
case 'image':
|
|
74
74
|
return `formatAttachmentUrl(${first})`;
|
|
75
|
+
case 'dicom':
|
|
75
76
|
case 'nested':
|
|
76
77
|
case 'json':
|
|
77
78
|
return `${prop}`; // raw; Detail view handles these separately
|
|
@@ -92,7 +93,7 @@ function emitFormFields(descriptors) {
|
|
|
92
93
|
if (seen.has(d.name))
|
|
93
94
|
continue;
|
|
94
95
|
// Forms can't capture complex nested structures cleanly; skip them.
|
|
95
|
-
if (d.kind === 'nested' || d.kind === 'json' || d.kind === 'image')
|
|
96
|
+
if (d.kind === 'nested' || d.kind === 'json' || d.kind === 'image' || d.kind === 'dicom')
|
|
96
97
|
continue;
|
|
97
98
|
if (d.isArray)
|
|
98
99
|
continue; // v1: scalars only
|
|
@@ -143,6 +144,7 @@ function formatPipeFor(kind) {
|
|
|
143
144
|
case 'date': return 'fhirDate';
|
|
144
145
|
case 'boolean': return 'fhirBoolean';
|
|
145
146
|
case 'image': return 'attachmentUrl';
|
|
147
|
+
case 'dicom': return undefined; // rendered as DicomViewer component, not a pipe
|
|
146
148
|
case 'text':
|
|
147
149
|
case 'number':
|
|
148
150
|
case 'nested':
|
|
@@ -220,9 +222,10 @@ function emitBrowserTitleExpr(titleDescriptor, interfaceName) {
|
|
|
220
222
|
? `selected.formatted('${titleDescriptor.name}', '${pipe}')`
|
|
221
223
|
: `selected.dot('${titleDescriptor.name}')`;
|
|
222
224
|
}
|
|
223
|
-
export function generatePrefabRenderer(interfaceName, fields) {
|
|
225
|
+
export function generatePrefabRenderer(interfaceName, fields, options) {
|
|
224
226
|
const rawDescriptors = fields.flatMap(describeField);
|
|
225
227
|
const descriptors = dedupeDescriptors(rawDescriptors);
|
|
228
|
+
const resourceType = options?.resourceType;
|
|
226
229
|
const tableDescriptors = pickTableDescriptors(descriptors);
|
|
227
230
|
const titleDescriptor = findTitleDescriptor(descriptors);
|
|
228
231
|
// Only use the title field for the detail heading when it's actually present
|
|
@@ -286,10 +289,12 @@ export function generatePrefabRenderer(interfaceName, fields) {
|
|
|
286
289
|
// unconditional when the profile has at least one table-renderable field.
|
|
287
290
|
// `PrefabAppOptions` is imported as a type to let callers thread through
|
|
288
291
|
// theme / scripts / onMount overrides without re-declaring the shape.
|
|
292
|
+
const isImagingStudy = resourceType === 'ImagingStudy';
|
|
289
293
|
const prefabImports = [
|
|
290
294
|
'Card',
|
|
291
295
|
'CardContent',
|
|
292
296
|
'Column',
|
|
297
|
+
...(isImagingStudy ? ['Component'] : []),
|
|
293
298
|
'DataTable',
|
|
294
299
|
'Detail',
|
|
295
300
|
'Heading',
|
|
@@ -304,6 +309,62 @@ export function generatePrefabRenderer(interfaceName, fields) {
|
|
|
304
309
|
];
|
|
305
310
|
const prefabImportLines = prefabImports.map(c => ` ${c},`).join('\n');
|
|
306
311
|
const prefabTypeImportLine = `import type { PrefabAppOptions } from '@maxhealth.tech/prefab';`;
|
|
312
|
+
// --- DicomViewer helper (emitted only for ImagingStudy profiles) --------
|
|
313
|
+
// Produces { type: "DicomViewer", wadoRsRoot, studyUID, seriesUID } in the
|
|
314
|
+
// wire format. The renderer resolves this via registerComponent at runtime.
|
|
315
|
+
const dicomHelperBlock = isImagingStudy ? `
|
|
316
|
+
/**
|
|
317
|
+
* Emit a DicomViewer custom component node.
|
|
318
|
+
*
|
|
319
|
+
* Wire format: \`{ type: "DicomViewer", wadoRsRoot, studyUID, seriesUID }\`
|
|
320
|
+
* The host renderer must have a \`DicomViewer\` component registered via
|
|
321
|
+
* \`registerComponent('DicomViewer', renderFn)\` — prefab passes props
|
|
322
|
+
* through to whatever render function is registered for the type.
|
|
323
|
+
*/
|
|
324
|
+
function DicomViewer(props: { wadoRsRoot: string; studyUID: string; seriesUID?: string }) {
|
|
325
|
+
const c = new Component('DicomViewer');
|
|
326
|
+
c.getProps = () => ({ ...props });
|
|
327
|
+
return c;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Extract WADO-RS root URL from ImagingStudy.endpoint references.
|
|
332
|
+
* Falls back to series-level endpoints if study-level is absent.
|
|
333
|
+
*/
|
|
334
|
+
function extractWadoRsRoot(resource: ${interfaceName}): string | undefined {
|
|
335
|
+
// Study-level endpoint: resource.endpoint[].reference
|
|
336
|
+
const studyEndpoint = (resource as any).endpoint?.[0]?.reference;
|
|
337
|
+
if (studyEndpoint) return studyEndpoint;
|
|
338
|
+
// Series-level fallback
|
|
339
|
+
return (resource as any).series?.[0]?.endpoint?.[0]?.reference;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Extract the DICOM Study Instance UID from the resource.
|
|
344
|
+
* ImagingStudy stores it as an identifier with system 'urn:dicom:uid'
|
|
345
|
+
* (value prefixed with 'urn:oid:') or in a top-level dedicated field
|
|
346
|
+
* depending on the FHIR version (R5 uses .identifier, R4 relies on
|
|
347
|
+
* identifier with the DICOM system).
|
|
348
|
+
*/
|
|
349
|
+
function extractStudyUID(resource: ${interfaceName}): string | undefined {
|
|
350
|
+
const identifiers = (resource as any).identifier as Array<{ system?: string; value?: string }> | undefined;
|
|
351
|
+
const dicomId = identifiers?.find(id => id.system === 'urn:dicom:uid');
|
|
352
|
+
if (dicomId?.value) {
|
|
353
|
+
// Strip 'urn:oid:' prefix if present
|
|
354
|
+
return dicomId.value.replace(/^urn:oid:/, '');
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
` : '';
|
|
359
|
+
// DicomViewer detail section — appended after the Card in the detail view
|
|
360
|
+
const dicomDetailSection = isImagingStudy ? `
|
|
361
|
+
// --- DICOM Viewer (ImagingStudy) ---
|
|
362
|
+
...((resource as any).series ?? []).map((s: any, idx: number) => {
|
|
363
|
+
const wadoRsRoot = extractWadoRsRoot(resource);
|
|
364
|
+
const studyUID = extractStudyUID(resource);
|
|
365
|
+
if (!wadoRsRoot || !studyUID || !s.uid) return undefined;
|
|
366
|
+
return DicomViewer({ wadoRsRoot, studyUID, seriesUID: s.uid });
|
|
367
|
+
}).filter(Boolean),` : '';
|
|
307
368
|
const code = `/**
|
|
308
369
|
* Auto-generated prefab render functions for ${interfaceName}.
|
|
309
370
|
* DO NOT EDIT MANUALLY — regenerate with: babelfhir-ts --prefab
|
|
@@ -320,7 +381,7 @@ ${formatterImportBlock}import { styles } from './styles.js';
|
|
|
320
381
|
// with \`new Function()\` in the browser — closures are not preserved, so
|
|
321
382
|
// each pipe in \`FHIR_PIPES\` inlines all logic it needs.
|
|
322
383
|
import { FHIR_PIPES } from './registerFhirPipes.js';
|
|
323
|
-
|
|
384
|
+
${dicomHelperBlock}
|
|
324
385
|
export interface ${interfaceName}Row {
|
|
325
386
|
${rowInterfaceProps}
|
|
326
387
|
}
|
|
@@ -359,6 +420,7 @@ ${detailRows}
|
|
|
359
420
|
}),
|
|
360
421
|
],
|
|
361
422
|
}),
|
|
423
|
+
${dicomDetailSection}
|
|
362
424
|
],
|
|
363
425
|
});
|
|
364
426
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B/R5 StructureDefinitions (profiles) directly inside package archives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "out/src/main.js",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@eslint/js": "^9.24.0",
|
|
82
|
+
"@maxhealth.tech/prefab": "^0.2.22",
|
|
82
83
|
"@types/node": "^25.5.0",
|
|
83
84
|
"@types/unzipper": "^0.10.11",
|
|
84
85
|
"@vitest/coverage-v8": "^4.0.15",
|
|
@@ -92,14 +93,14 @@
|
|
|
92
93
|
"typescript": "^6.0.2",
|
|
93
94
|
"typescript-eslint": "^8.30.1",
|
|
94
95
|
"vitepress": "^1.6.4",
|
|
96
|
+
"vitepress-plugin-llms": "^1.12.2",
|
|
95
97
|
"vitest": "^4.0.15",
|
|
96
98
|
"zod": "^4.3.6"
|
|
97
99
|
},
|
|
98
100
|
"dependencies": {
|
|
99
|
-
"@
|
|
100
|
-
"@types/fhir": "^0.0.41",
|
|
101
|
+
"@types/fhir": "^0.0.42",
|
|
101
102
|
"consola": "^3.4.2",
|
|
102
|
-
"fhirpath": "^4.
|
|
103
|
+
"fhirpath": "^4.10.0",
|
|
103
104
|
"tar": "^7.4.3",
|
|
104
105
|
"tsx": "^4.19.3",
|
|
105
106
|
"unzipper": "^0.12.3"
|