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: string; url: string }; response?: { status: string } }[];
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
- import type { FhirViewRegistry } from '@babelfhir-ts/mcp';
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.0",
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
- "@maxhealth.tech/prefab": "^0.2.1",
100
- "@types/fhir": "^0.0.41",
101
+ "@types/fhir": "^0.0.42",
101
102
  "consola": "^3.4.2",
102
- "fhirpath": "^4.9.1",
103
+ "fhirpath": "^4.10.0",
103
104
  "tar": "^7.4.3",
104
105
  "tsx": "^4.19.3",
105
106
  "unzipper": "^0.12.3"