babelfhir-ts 1.4.0 → 1.4.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.
- package/README.md +3 -1
- package/out/src/cli/updateCommand.js +85 -2
- package/out/src/generator/emitters/client/clientGenerator.js +98 -25
- package/out/src/generator/emitters/client/clientReadmeGenerator.js +12 -5
- package/out/src/generator/emitters/client/searchParamHelpers.js +37 -0
- package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +4 -0
- package/out/src/generator/emitters/valueset/valueSetGenerator.js +15 -1
- package/out/src/generator/generationHelpers.js +57 -2
- package/out/src/generator/index.js +51 -4
- package/out/src/generator/parser/packageParser.js +36 -0
- package/out/src/generator/parser/txClient.js +50 -0
- package/out/src/generator/parser/vsParser.js +6 -2
- package/out/src/main.js +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -240,7 +240,9 @@ Options:
|
|
|
240
240
|
--registry <url> FHIR package registry URL (default: https://packages.simplifier.net)
|
|
241
241
|
--tx-server <url> Terminology server URL for ValueSet expansion (e.g., https://tx.fhir.org/r4)
|
|
242
242
|
When set, expands ValueSets without explicit codes using $expand operation
|
|
243
|
-
--display-language <lang> BCP-47 language for display terms (e.g., de,
|
|
243
|
+
--display-language <lang> BCP-47 language(s) for display terms (e.g., de or de,fr,en).
|
|
244
|
+
Single value replaces concept displays. Comma-separated values
|
|
245
|
+
also generate a multi-language display map with getDisplay() helper.
|
|
244
246
|
|
|
245
247
|
Examples:
|
|
246
248
|
babelfhir-ts # Process ./input to ./output
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* babelfhir-ts update <package-name@version> # update a single package
|
|
11
11
|
*/
|
|
12
12
|
import fs from 'fs';
|
|
13
|
+
import os from 'os';
|
|
13
14
|
import path from 'path';
|
|
14
15
|
import crypto from 'crypto';
|
|
15
16
|
import { generateIntoPackageDirect, resetFetchFailureTracking } from '../generator/index.js';
|
|
@@ -73,6 +74,10 @@ export async function handleUpdateCommand(opts) {
|
|
|
73
74
|
if (updatedPackages.length > 0) {
|
|
74
75
|
patchBunLockIntegrity(updatedPackages);
|
|
75
76
|
}
|
|
77
|
+
// 5b. Remove stale node_modules/<pkg> so bun re-extracts from the updated tgz
|
|
78
|
+
if (updatedPackages.length > 0) {
|
|
79
|
+
removeStaleNodeModules(updatedPackages.map((p) => p.generatedName));
|
|
80
|
+
}
|
|
76
81
|
// 6. Single npm install at the end
|
|
77
82
|
if (updated > 0) {
|
|
78
83
|
console.log('\nReinstalling dependencies...');
|
|
@@ -298,8 +303,21 @@ export function patchBunLockIntegrity(updatedPackages) {
|
|
|
298
303
|
const pm = detectPackageManager();
|
|
299
304
|
if (!pm.cmd.includes('bun'))
|
|
300
305
|
return;
|
|
301
|
-
|
|
302
|
-
|
|
306
|
+
// Walk up to find bun.lock (in monorepos it's at the workspace root, not cwd)
|
|
307
|
+
let lockPath = null;
|
|
308
|
+
let dir = process.cwd();
|
|
309
|
+
while (true) {
|
|
310
|
+
const candidate = path.join(dir, 'bun.lock');
|
|
311
|
+
if (fs.existsSync(candidate)) {
|
|
312
|
+
lockPath = candidate;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
const parent = path.dirname(dir);
|
|
316
|
+
if (parent === dir)
|
|
317
|
+
break;
|
|
318
|
+
dir = parent;
|
|
319
|
+
}
|
|
320
|
+
if (!lockPath)
|
|
303
321
|
return;
|
|
304
322
|
let lockContent = fs.readFileSync(lockPath, 'utf8');
|
|
305
323
|
let patched = 0;
|
|
@@ -348,6 +366,71 @@ function clearBunCacheForPackage(packageName) {
|
|
|
348
366
|
dir = parent;
|
|
349
367
|
}
|
|
350
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Remove node_modules/<pkg> directories, bun cached extractions, and bun global
|
|
371
|
+
* cache entries for each updated package so bun re-extracts from the fresh tgz.
|
|
372
|
+
* Finds the workspace root first, then cleans all node_modules dirs from cwd up.
|
|
373
|
+
*/
|
|
374
|
+
function removeStaleNodeModules(packageNames) {
|
|
375
|
+
// Find the workspace root: highest ancestor with a package.json
|
|
376
|
+
let root = process.cwd();
|
|
377
|
+
let search = root;
|
|
378
|
+
while (true) {
|
|
379
|
+
const parent = path.dirname(search);
|
|
380
|
+
if (parent === search)
|
|
381
|
+
break;
|
|
382
|
+
if (fs.existsSync(path.join(parent, 'package.json')))
|
|
383
|
+
root = parent;
|
|
384
|
+
search = parent;
|
|
385
|
+
}
|
|
386
|
+
// Walk from cwd up to (and including) the root, cleaning each node_modules
|
|
387
|
+
let dir = process.cwd();
|
|
388
|
+
while (true) {
|
|
389
|
+
const nm = path.join(dir, 'node_modules');
|
|
390
|
+
for (const name of packageNames) {
|
|
391
|
+
// Remove the symlink / junction / directory itself (lstat detects dangling junctions too)
|
|
392
|
+
const pkgDir = path.join(nm, name);
|
|
393
|
+
let pkgExists = false;
|
|
394
|
+
try {
|
|
395
|
+
fs.lstatSync(pkgDir);
|
|
396
|
+
pkgExists = true;
|
|
397
|
+
}
|
|
398
|
+
catch { /* does not exist */ }
|
|
399
|
+
if (pkgExists) {
|
|
400
|
+
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
// Remove bun's content-addressed cache entries (node_modules/.bun/<pkg>@*)
|
|
403
|
+
const bunDir = path.join(nm, '.bun');
|
|
404
|
+
if (fs.existsSync(bunDir)) {
|
|
405
|
+
const prefix = `${name}@`;
|
|
406
|
+
try {
|
|
407
|
+
for (const entry of fs.readdirSync(bunDir, { withFileTypes: true })) {
|
|
408
|
+
if (entry.isDirectory() && entry.name.startsWith(prefix)) {
|
|
409
|
+
fs.rmSync(path.join(bunDir, entry.name), { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch { /* ignore read errors */ }
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (dir === root)
|
|
417
|
+
break;
|
|
418
|
+
const parent = path.dirname(dir);
|
|
419
|
+
if (parent === dir)
|
|
420
|
+
break;
|
|
421
|
+
dir = parent;
|
|
422
|
+
}
|
|
423
|
+
// Clear bun's global install cache (~/.bun/install/cache/<pkg>)
|
|
424
|
+
const bunGlobalCache = path.join(os.homedir(), '.bun', 'install', 'cache');
|
|
425
|
+
if (fs.existsSync(bunGlobalCache)) {
|
|
426
|
+
for (const name of packageNames) {
|
|
427
|
+
const cached = path.join(bunGlobalCache, name);
|
|
428
|
+
if (fs.existsSync(cached)) {
|
|
429
|
+
fs.rmSync(cached, { recursive: true, force: true });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
351
434
|
// ── Package manager install ─────────────────────────────────────────────────
|
|
352
435
|
function runInstall() {
|
|
353
436
|
return new Promise((resolve, reject) => {
|
|
@@ -5,6 +5,7 @@ import { logger } from "../../../logger.js";
|
|
|
5
5
|
import { generateSmartAuth, generateSmartClient } from './smartAuthGenerator.js';
|
|
6
6
|
import { generateReadme, installBaseClientTypes } from './clientReadmeGenerator.js';
|
|
7
7
|
import { versionSlug } from '../../fhir/versionContext.js';
|
|
8
|
+
import { buildSearchParamInterfaces, getSearchParamsTypeName } from './searchParamHelpers.js';
|
|
8
9
|
const log = logger.withTag('client');
|
|
9
10
|
/**
|
|
10
11
|
* Generate FHIR client code for all resource types
|
|
@@ -26,14 +27,14 @@ export function generateClient(options) {
|
|
|
26
27
|
fs.mkdirSync(clientDir, { recursive: true });
|
|
27
28
|
}
|
|
28
29
|
// Generate each file
|
|
29
|
-
generateTypes(clientDir, resourceTypes);
|
|
30
|
-
generateResourceReader(clientDir, resourceTypes);
|
|
30
|
+
generateTypes(clientDir, resourceTypes, options.searchParams);
|
|
31
|
+
generateResourceReader(clientDir, resourceTypes, options.searchParams);
|
|
31
32
|
generateResourceWriter(clientDir, resourceTypes);
|
|
32
33
|
generateBundleParser(clientDir, resourceTypes);
|
|
33
|
-
generateFhirClient(clientDir, resourceTypes);
|
|
34
|
+
generateFhirClient(clientDir, resourceTypes, options.searchParams);
|
|
34
35
|
generateSmartAuth(clientDir);
|
|
35
36
|
generateSmartClient(clientDir, resourceTypes);
|
|
36
|
-
generateIndex(clientDir, resourceTypes);
|
|
37
|
+
generateIndex(clientDir, resourceTypes, options.searchParams);
|
|
37
38
|
generateReadme(clientDir);
|
|
38
39
|
// Install base client type declarations so tsc can resolve @babelfhir-ts/client-<version>
|
|
39
40
|
installBaseClientTypes(options.outputDir);
|
|
@@ -42,10 +43,12 @@ export function generateClient(options) {
|
|
|
42
43
|
/**
|
|
43
44
|
* Generate types.ts
|
|
44
45
|
*/
|
|
45
|
-
function generateTypes(clientDir, resourceTypes) {
|
|
46
|
+
function generateTypes(clientDir, resourceTypes, searchParams) {
|
|
46
47
|
const unionType = resourceTypes
|
|
47
48
|
.map((rt, idx) => ` ${idx === 0 ? "" : "| "}GeneratedTypes.${rt.profileName}`)
|
|
48
49
|
.join("\n");
|
|
50
|
+
// Generate per-base-resource-type search param interfaces
|
|
51
|
+
const searchParamInterfaces = buildSearchParamInterfaces(resourceTypes, searchParams);
|
|
49
52
|
const content = `import type * as GeneratedTypes from "../index.js";
|
|
50
53
|
|
|
51
54
|
/**
|
|
@@ -54,9 +57,34 @@ function generateTypes(clientDir, resourceTypes) {
|
|
|
54
57
|
export type WithId<T> = { id: string } & T;
|
|
55
58
|
|
|
56
59
|
/**
|
|
57
|
-
* Search
|
|
60
|
+
* Search parameter value type — all values are serialized to query strings.
|
|
58
61
|
*/
|
|
59
|
-
export type
|
|
62
|
+
export type SearchParamValue = boolean | number | string | string[] | undefined;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Search parameters for FHIR resources (generic untyped version).
|
|
66
|
+
*/
|
|
67
|
+
export type SearchParams = Record<string, SearchParamValue>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Common search parameters supported by all FHIR resources.
|
|
71
|
+
* Includes an index signature to allow search parameter modifiers (e.g. name:exact).
|
|
72
|
+
*/
|
|
73
|
+
export interface CommonSearchParams {
|
|
74
|
+
_id?: SearchParamValue;
|
|
75
|
+
_lastUpdated?: SearchParamValue;
|
|
76
|
+
_tag?: SearchParamValue;
|
|
77
|
+
_profile?: SearchParamValue;
|
|
78
|
+
_security?: SearchParamValue;
|
|
79
|
+
_count?: SearchParamValue;
|
|
80
|
+
_sort?: SearchParamValue;
|
|
81
|
+
_include?: SearchParamValue;
|
|
82
|
+
_revinclude?: SearchParamValue;
|
|
83
|
+
_summary?: SearchParamValue;
|
|
84
|
+
_elements?: SearchParamValue;
|
|
85
|
+
_total?: SearchParamValue;
|
|
86
|
+
[key: string]: SearchParamValue;
|
|
87
|
+
}
|
|
60
88
|
|
|
61
89
|
/**
|
|
62
90
|
* Bundle of FHIR resources
|
|
@@ -80,24 +108,34 @@ export interface Bundle<T> {
|
|
|
80
108
|
*/
|
|
81
109
|
export type FhirResource =
|
|
82
110
|
${unionType};
|
|
83
|
-
|
|
111
|
+
|
|
112
|
+
${searchParamInterfaces}`;
|
|
84
113
|
fs.writeFileSync(path.join(clientDir, "types.ts"), content);
|
|
85
114
|
}
|
|
86
115
|
/**
|
|
87
116
|
* Generate resource-reader.ts
|
|
88
117
|
*/
|
|
89
|
-
function generateResourceReader(clientDir, resourceTypes) {
|
|
118
|
+
function generateResourceReader(clientDir, resourceTypes, searchParams) {
|
|
119
|
+
// Build reader type aliases with typed search params
|
|
90
120
|
const readerTypes = resourceTypes
|
|
91
|
-
.map((rt) =>
|
|
121
|
+
.map((rt) => {
|
|
122
|
+
const spType = getSearchParamsTypeName(rt.baseResourceType, searchParams);
|
|
123
|
+
return `export type ${rt.profileName}Reader = FhirResourceSearcher<GeneratedTypes.${rt.profileName}, ${spType}>;`;
|
|
124
|
+
})
|
|
92
125
|
.join("\n");
|
|
126
|
+
// Collect unique SearchParams type imports from types.ts
|
|
127
|
+
const spTypeImports = new Set(['SearchParams']);
|
|
128
|
+
for (const rt of resourceTypes) {
|
|
129
|
+
spTypeImports.add(getSearchParamsTypeName(rt.baseResourceType, searchParams));
|
|
130
|
+
}
|
|
93
131
|
const content = `import type * as GeneratedTypes from "../index.js";
|
|
94
132
|
|
|
95
|
-
import type { Bundle,
|
|
133
|
+
import type { Bundle, ${[...spTypeImports].sort().join(', ')}, WithId } from "./types.js";
|
|
96
134
|
|
|
97
135
|
/**
|
|
98
136
|
* Generic FHIR resource searcher/reader
|
|
99
137
|
*/
|
|
100
|
-
export interface FhirResourceSearcher<T> {
|
|
138
|
+
export interface FhirResourceSearcher<T, S extends SearchParams = SearchParams> {
|
|
101
139
|
readonly baseUrl: string;
|
|
102
140
|
readonly resourceType: string;
|
|
103
141
|
|
|
@@ -109,17 +147,17 @@ export interface FhirResourceSearcher<T> {
|
|
|
109
147
|
/**
|
|
110
148
|
* Search for resources
|
|
111
149
|
*/
|
|
112
|
-
search(params?:
|
|
150
|
+
search(params?: S): Promise<Bundle<WithId<T>>>;
|
|
113
151
|
|
|
114
152
|
/**
|
|
115
153
|
* Search and return first result or undefined
|
|
116
154
|
*/
|
|
117
|
-
searchOne(params?:
|
|
155
|
+
searchOne(params?: S): Promise<undefined | WithId<T>>;
|
|
118
156
|
|
|
119
157
|
/**
|
|
120
158
|
* Search and return all results (handles pagination)
|
|
121
159
|
*/
|
|
122
|
-
searchAll(params?:
|
|
160
|
+
searchAll(params?: S): Promise<WithId<T>[]>;
|
|
123
161
|
}
|
|
124
162
|
|
|
125
163
|
/**
|
|
@@ -135,7 +173,7 @@ export type FetchFn = typeof globalThis.fetch;
|
|
|
135
173
|
/**
|
|
136
174
|
* Implementation of FHIR resource reader
|
|
137
175
|
*/
|
|
138
|
-
export class FhirResourceReader<T> implements FhirResourceSearcher<T> {
|
|
176
|
+
export class FhirResourceReader<T, S extends SearchParams = SearchParams> implements FhirResourceSearcher<T, S> {
|
|
139
177
|
private readonly fetchFn: FetchFn;
|
|
140
178
|
|
|
141
179
|
constructor(
|
|
@@ -163,7 +201,7 @@ export class FhirResourceReader<T> implements FhirResourceSearcher<T> {
|
|
|
163
201
|
return (await response.json()) as WithId<T>;
|
|
164
202
|
}
|
|
165
203
|
|
|
166
|
-
async search(params?:
|
|
204
|
+
async search(params?: S): Promise<Bundle<WithId<T>>> {
|
|
167
205
|
const url = new URL(\`\${this.baseUrl}/\${this.resourceType}\`);
|
|
168
206
|
|
|
169
207
|
if (params) {
|
|
@@ -195,12 +233,12 @@ export class FhirResourceReader<T> implements FhirResourceSearcher<T> {
|
|
|
195
233
|
return (await response.json()) as Bundle<WithId<T>>;
|
|
196
234
|
}
|
|
197
235
|
|
|
198
|
-
async searchOne(params?:
|
|
199
|
-
const bundle = await this.search({ ...params, _count: 1 });
|
|
236
|
+
async searchOne(params?: S): Promise<undefined | WithId<T>> {
|
|
237
|
+
const bundle = await this.search({ ...params, _count: 1 } as S & { _count: number });
|
|
200
238
|
return bundle.entry?.[0]?.resource;
|
|
201
239
|
}
|
|
202
240
|
|
|
203
|
-
async searchAll(params?:
|
|
241
|
+
async searchAll(params?: S): Promise<WithId<T>[]> {
|
|
204
242
|
const results: WithId<T>[] = [];
|
|
205
243
|
let bundle = await this.search(params);
|
|
206
244
|
|
|
@@ -364,6 +402,7 @@ export class FhirResourceWriterImpl<T> implements FhirResourceWriter<T> {
|
|
|
364
402
|
* Generate bundle-parser.ts
|
|
365
403
|
*/
|
|
366
404
|
function generateBundleParser(clientDir, resourceTypes) {
|
|
405
|
+
const clientPkg = `@babelfhir-ts/client-${versionSlug()}`;
|
|
367
406
|
// Group profiles by base resource type
|
|
368
407
|
const resourceTypeGroups = new Map();
|
|
369
408
|
for (const rt of resourceTypes) {
|
|
@@ -382,6 +421,7 @@ function generateBundleParser(clientDir, resourceTypes) {
|
|
|
382
421
|
})
|
|
383
422
|
.join("\n");
|
|
384
423
|
const content = `import type * as GeneratedTypes from "../index.js";
|
|
424
|
+
import { BundleParser as BaseBundleParser } from "${clientPkg}";
|
|
385
425
|
import type { Bundle, FhirResource } from "./types.js";
|
|
386
426
|
|
|
387
427
|
/**
|
|
@@ -515,6 +555,15 @@ ${parseByTypeCases}
|
|
|
515
555
|
getBundleType(): 'collection' | 'searchset' | 'transaction-response' | 'transaction' | undefined {
|
|
516
556
|
return this.bundle.type;
|
|
517
557
|
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Resolve a FHIR reference within this Bundle.
|
|
561
|
+
* Supports "ResourceType/id" relative references and full-URL references
|
|
562
|
+
* matched against entry.fullUrl.
|
|
563
|
+
*/
|
|
564
|
+
resolveReference<T extends FhirResource>(ref: { reference?: string } | undefined): T | undefined {
|
|
565
|
+
return BaseBundleParser.resolveReference(ref, this.bundle as any) as T | undefined;
|
|
566
|
+
}
|
|
518
567
|
}
|
|
519
568
|
|
|
520
569
|
/**
|
|
@@ -529,12 +578,20 @@ export function parseBundle(bundle: Bundle<FhirResource>): BundleParser {
|
|
|
529
578
|
/**
|
|
530
579
|
* Generate fhir-client.ts
|
|
531
580
|
*/
|
|
532
|
-
function generateFhirClient(clientDir, resourceTypes) {
|
|
581
|
+
function generateFhirClient(clientDir, resourceTypes, searchParams) {
|
|
582
|
+
// Collect search param type imports
|
|
583
|
+
const spTypeImports = new Set();
|
|
584
|
+
for (const rt of resourceTypes) {
|
|
585
|
+
const spType = getSearchParamsTypeName(rt.baseResourceType, searchParams);
|
|
586
|
+
if (spType !== 'SearchParams')
|
|
587
|
+
spTypeImports.add(spType);
|
|
588
|
+
}
|
|
533
589
|
const readerMethods = resourceTypes
|
|
534
590
|
.map((rt) => {
|
|
535
591
|
const methodName = rt.profileName.charAt(0).toLowerCase() + rt.profileName.slice(1);
|
|
592
|
+
const spType = getSearchParamsTypeName(rt.baseResourceType, searchParams);
|
|
536
593
|
return ` ${methodName}() {
|
|
537
|
-
return this.forType<GeneratedTypes.${rt.profileName}>("${rt.baseResourceType}");
|
|
594
|
+
return this.forType<GeneratedTypes.${rt.profileName}, ${spType}>("${rt.baseResourceType}");
|
|
538
595
|
}`;
|
|
539
596
|
})
|
|
540
597
|
.join("\n\n");
|
|
@@ -548,13 +605,17 @@ function generateFhirClient(clientDir, resourceTypes) {
|
|
|
548
605
|
.join("\n\n");
|
|
549
606
|
const slug = versionSlug();
|
|
550
607
|
const clientPkg = `@babelfhir-ts/client-${slug}`;
|
|
608
|
+
const spImportLine = spTypeImports.size > 0
|
|
609
|
+
? `\nimport type { ${[...spTypeImports].sort().join(', ')} } from "./types.js";\n`
|
|
610
|
+
: '';
|
|
551
611
|
const content = `import {
|
|
552
612
|
FhirReadClient as BaseFhirReadClient,
|
|
553
613
|
FhirWriteClient as BaseFhirWriteClient,
|
|
554
614
|
type FetchFn,
|
|
615
|
+
type SearchParams,
|
|
555
616
|
} from "${clientPkg}";
|
|
556
617
|
import type * as GeneratedTypes from "../index.js";
|
|
557
|
-
|
|
618
|
+
${spImportLine}
|
|
558
619
|
/**
|
|
559
620
|
* Profile-specific FHIR Read Client.
|
|
560
621
|
* Extends the base ${slug.toUpperCase()} read client with typed profile accessors.
|
|
@@ -622,13 +683,25 @@ export class FhirClient {
|
|
|
622
683
|
/**
|
|
623
684
|
* Generate index.ts barrel export
|
|
624
685
|
*/
|
|
625
|
-
function generateIndex(clientDir, resourceTypes) {
|
|
686
|
+
function generateIndex(clientDir, resourceTypes, searchParams) {
|
|
626
687
|
const readerTypes = resourceTypes
|
|
627
688
|
.map((rt) => ` ${rt.profileName}Reader,`)
|
|
628
689
|
.join("\n");
|
|
629
690
|
const writerTypes = resourceTypes
|
|
630
691
|
.map((rt) => ` ${rt.profileName}Writer,`)
|
|
631
692
|
.join("\n");
|
|
693
|
+
// Collect per-resource SearchParams type exports
|
|
694
|
+
const spTypeExports = new Set();
|
|
695
|
+
if (searchParams && searchParams.size > 0) {
|
|
696
|
+
for (const rt of resourceTypes) {
|
|
697
|
+
const spType = getSearchParamsTypeName(rt.baseResourceType, searchParams);
|
|
698
|
+
if (spType !== 'SearchParams')
|
|
699
|
+
spTypeExports.add(spType);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const spExportLine = spTypeExports.size > 0
|
|
703
|
+
? `export type { CommonSearchParams, SearchParamValue, ${[...spTypeExports].sort().join(', ')} } from "./types.js";\n`
|
|
704
|
+
: '';
|
|
632
705
|
const content = `export { FhirClient, FhirReadClient, FhirWriteClient } from "./fhir-client.js";
|
|
633
706
|
export { FhirResourceReader } from "./resource-reader.js";
|
|
634
707
|
export type { FetchFn } from "./resource-reader.js";
|
|
@@ -644,7 +717,7 @@ ${writerTypes}
|
|
|
644
717
|
export { BundleParser, parseBundle } from "./bundle-parser.js";
|
|
645
718
|
export type { BundleEntry } from "./bundle-parser.js";
|
|
646
719
|
export type { Bundle, FhirResource, SearchParams, WithId } from "./types.js";
|
|
647
|
-
export { SmartAuth, discoverEndpoints } from "./smart-auth.js";
|
|
720
|
+
${spExportLine}export { SmartAuth, discoverEndpoints } from "./smart-auth.js";
|
|
648
721
|
export type { SmartConfig, SmartToken, SmartConfiguration, LaunchMode } from "./smart-auth.js";
|
|
649
722
|
export { SmartFhirClient } from "./smart-client.js";
|
|
650
723
|
`;
|
|
@@ -226,14 +226,14 @@ export interface Bundle<T = ${fhirNs}.Resource> {
|
|
|
226
226
|
entry?: { resource?: T; fullUrl?: string; search?: { mode?: string; score?: number }; request?: { method: string; url: string }; response?: { status: string } }[];
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
export declare class FhirResourceReader<T extends ${fhirNs}.Resource> {
|
|
229
|
+
export declare class FhirResourceReader<T extends ${fhirNs}.Resource, S extends SearchParams = SearchParams> {
|
|
230
230
|
readonly baseUrl: string;
|
|
231
231
|
readonly resourceType: string;
|
|
232
232
|
constructor(baseUrl: string, resourceType: string, fetchFn?: FetchFn);
|
|
233
233
|
read(id: string): Promise<WithId<T>>;
|
|
234
|
-
search(params?:
|
|
235
|
-
searchOne(params?:
|
|
236
|
-
searchAll(params?:
|
|
234
|
+
search(params?: S): Promise<Bundle<WithId<T>>>;
|
|
235
|
+
searchOne(params?: S): Promise<WithId<T> | undefined>;
|
|
236
|
+
searchAll(params?: S): Promise<WithId<T>[]>;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
export declare class FhirResourceWriter<T extends ${fhirNs}.Resource> {
|
|
@@ -248,7 +248,7 @@ export declare class FhirResourceWriter<T extends ${fhirNs}.Resource> {
|
|
|
248
248
|
|
|
249
249
|
export declare class FhirReadClient {
|
|
250
250
|
constructor(baseUrl: string, fetchFn?: FetchFn);
|
|
251
|
-
protected forType<T extends ${fhirNs}.Resource>(resourceType: string): FhirResourceReader<T>;
|
|
251
|
+
protected forType<T extends ${fhirNs}.Resource, S extends SearchParams = SearchParams>(resourceType: string): FhirResourceReader<T, S>;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
export declare class FhirWriteClient {
|
|
@@ -262,6 +262,13 @@ export declare class FhirClient {
|
|
|
262
262
|
read(): FhirReadClient;
|
|
263
263
|
write(): FhirWriteClient;
|
|
264
264
|
}
|
|
265
|
+
|
|
266
|
+
export declare class BundleParser {
|
|
267
|
+
static getResourcesByType<T extends ${fhirNs}.Resource>(bundle: Bundle<${fhirNs}.Resource>, resourceType: string): WithId<T>[];
|
|
268
|
+
static getFirstResourceByType<T extends ${fhirNs}.Resource>(bundle: Bundle<${fhirNs}.Resource>, resourceType: string): WithId<T> | undefined;
|
|
269
|
+
static getAllResources(bundle: Bundle<${fhirNs}.Resource>): ${fhirNs}.Resource[];
|
|
270
|
+
static resolveReference<T extends ${fhirNs}.Resource>(ref: { reference?: string } | undefined, bundle: Bundle<${fhirNs}.Resource>): T | undefined;
|
|
271
|
+
}
|
|
265
272
|
`;
|
|
266
273
|
fs.writeFileSync(path.join(pkgDir, "index.d.ts"), dts);
|
|
267
274
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build per-base-resource-type SearchParams interface declarations for generated types.ts.
|
|
3
|
+
*/
|
|
4
|
+
export function buildSearchParamInterfaces(resourceTypes, searchParams) {
|
|
5
|
+
if (!searchParams || searchParams.size === 0)
|
|
6
|
+
return '';
|
|
7
|
+
const baseTypes = [...new Set(resourceTypes.map(rt => rt.baseResourceType))];
|
|
8
|
+
const blocks = [];
|
|
9
|
+
for (const baseType of baseTypes) {
|
|
10
|
+
const params = searchParams.get(baseType);
|
|
11
|
+
if (!params?.length)
|
|
12
|
+
continue;
|
|
13
|
+
// Sort params alphabetically and deduplicate
|
|
14
|
+
const sorted = [...params].sort((a, b) => a.code.localeCompare(b.code));
|
|
15
|
+
const fields = sorted
|
|
16
|
+
.map(p => ` "${p.code}"?: SearchParamValue;`)
|
|
17
|
+
.join("\n");
|
|
18
|
+
blocks.push(`/**
|
|
19
|
+
* Typed search parameters for ${baseType} resources.
|
|
20
|
+
* Generated from SearchParameter definitions in the base FHIR spec and IG.
|
|
21
|
+
*/
|
|
22
|
+
export interface ${baseType}SearchParams extends CommonSearchParams {
|
|
23
|
+
${fields}
|
|
24
|
+
}`);
|
|
25
|
+
}
|
|
26
|
+
return blocks.join("\n\n") + "\n";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get the SearchParams interface name for a given base resource type,
|
|
30
|
+
* or fall back to SearchParams if none is generated.
|
|
31
|
+
*/
|
|
32
|
+
export function getSearchParamsTypeName(baseResourceType, searchParams) {
|
|
33
|
+
if (searchParams?.has(baseResourceType) && searchParams.get(baseResourceType).length > 0) {
|
|
34
|
+
return `${baseResourceType}SearchParams`;
|
|
35
|
+
}
|
|
36
|
+
return 'SearchParams';
|
|
37
|
+
}
|
|
@@ -535,6 +535,10 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
|
|
|
535
535
|
const sanitizedBaseResource = baseResource ? sanitizeIdentifier(baseResource) : undefined;
|
|
536
536
|
const isBaseAbstract = sanitizedBaseResource === 'Base';
|
|
537
537
|
const extendsClause = (sanitizedBaseResource && !isBaseAbstract && !isPrimitiveType(sanitizedBaseResource)) ? ` extends ${sanitizedBaseResource}` : '';
|
|
538
|
+
// Add resourceType literal for discriminated union support (only for actual FHIR resources, not data types)
|
|
539
|
+
if (ctx.resourceType && rules().isCoreResource(ctx.resourceType) && !deduped.some(l => /^resourceType[?:]/.test(l))) {
|
|
540
|
+
deduped.unshift(`resourceType: '${ctx.resourceType}';`);
|
|
541
|
+
}
|
|
538
542
|
debug('emit root interface', parentInterfaceName, 'extends', sanitizedBaseResource, 'fields', deduped.length);
|
|
539
543
|
interfaces.push(`export interface ${parentInterfaceName}${extendsClause} {\n${deduped.map(l => ` ${l}`).join('\n')}\n}`);
|
|
540
544
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* Generate TypeScript content for a ValueSet
|
|
7
7
|
*/
|
|
8
|
-
export function generateValueSetTypeScript(valueSet) {
|
|
8
|
+
export function generateValueSetTypeScript(valueSet, options) {
|
|
9
9
|
const sanitizedName = sanitizeValueSetName(valueSet.name);
|
|
10
10
|
const filename = `ValueSet-${sanitizedName}.ts`;
|
|
11
11
|
// Use the already-computed isSmall flag from vsParser (uses VALUESET_THRESHOLDS.UNION_TYPE)
|
|
@@ -88,6 +88,20 @@ export function generateValueSetTypeScript(valueSet) {
|
|
|
88
88
|
// For empty ValueSets, add a note
|
|
89
89
|
content += `/**\n * Note: This ValueSet has no explicitly enumerated codes.\n * It may be defined by filters or external terminology.\n * Runtime validation is not available for this ValueSet.\n */\n`;
|
|
90
90
|
}
|
|
91
|
+
// Generate multi-language display map if translations are provided
|
|
92
|
+
const translations = options?.displayTranslations;
|
|
93
|
+
if (translations && translations.size > 0) {
|
|
94
|
+
content += `\n/**\n * Multi-language display translations\n * Maps code → language → display string\n */\n`;
|
|
95
|
+
content += `export const ${sanitizedName}Displays: Record<string, Record<string, string>> = {\n`;
|
|
96
|
+
for (const [code, langs] of translations) {
|
|
97
|
+
content += ` ${JSON.stringify(code)}: ${JSON.stringify(langs)},\n`;
|
|
98
|
+
}
|
|
99
|
+
content += `};\n\n`;
|
|
100
|
+
content += `/**\n * Get the display string for a code in a specific language\n */\n`;
|
|
101
|
+
content += `export function get${sanitizedName}Display(code: string, lang: string): string | undefined {\n`;
|
|
102
|
+
content += ` return ${sanitizedName}Displays[code]?.[lang];\n`;
|
|
103
|
+
content += `}\n`;
|
|
104
|
+
}
|
|
91
105
|
return { filename, content };
|
|
92
106
|
}
|
|
93
107
|
/**
|
|
@@ -107,8 +107,12 @@ export function collectReferencedDependencyProfiles(mainSDs, dependencySDs) {
|
|
|
107
107
|
* Used by generate() and generateIntoPackage() when --tx-server is specified.
|
|
108
108
|
*/
|
|
109
109
|
export async function expandValueSetsWithTx(valueSets, valueSetCodesMap, structureDefinitions, txServer, displayLanguage) {
|
|
110
|
+
// Parse comma-separated languages: first is primary, multiple trigger display map
|
|
111
|
+
const langs = displayLanguage?.split(',').map(l => l.trim()).filter(Boolean) ?? [];
|
|
112
|
+
const primaryLang = langs[0];
|
|
113
|
+
const multiLangs = langs.length > 1 ? langs : undefined;
|
|
110
114
|
const cacheDir = getCacheConfig().rootDir;
|
|
111
|
-
const txClient = createTxClient(cacheDir, txServer,
|
|
115
|
+
const txClient = createTxClient(cacheDir, txServer, primaryLang);
|
|
112
116
|
const bindingUrls = collectValueSetBindingUrls(structureDefinitions);
|
|
113
117
|
logger.log(`Found ${bindingUrls.size} unique ValueSet bindings in StructureDefinitions`);
|
|
114
118
|
addPlaceholdersForExternalBindings(valueSets, bindingUrls, getFhirPackagesCacheDir());
|
|
@@ -123,6 +127,55 @@ export async function expandValueSetsWithTx(valueSets, valueSetCodesMap, structu
|
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
logger.log(`After tx expansion: ${expanded.size} ValueSets (${Array.from(expanded.values()).filter(vs => vs.concepts.length > 0).length} with codes)`);
|
|
130
|
+
// Multi-language display expansion
|
|
131
|
+
if (multiLangs && multiLangs.length > 0) {
|
|
132
|
+
const multiLangClient = createTxClient(cacheDir, txServer);
|
|
133
|
+
let translatedCount = 0;
|
|
134
|
+
let totalCodes = 0;
|
|
135
|
+
let untranslatedCodes = 0;
|
|
136
|
+
for (const [url, vs] of expanded) {
|
|
137
|
+
if (vs.concepts.length === 0)
|
|
138
|
+
continue;
|
|
139
|
+
const translations = await multiLangClient.expandValueSetMultiLang(url, multiLangs);
|
|
140
|
+
if (translations.size === 0)
|
|
141
|
+
continue;
|
|
142
|
+
// Build canonical display lookup from the primary expansion
|
|
143
|
+
const canonicalDisplay = new Map();
|
|
144
|
+
for (const concept of vs.concepts) {
|
|
145
|
+
if (concept.display)
|
|
146
|
+
canonicalDisplay.set(concept.code, concept.display);
|
|
147
|
+
}
|
|
148
|
+
// Strip languages where the tx server returned the same text as the
|
|
149
|
+
// canonical (English) display — this means no real translation exists.
|
|
150
|
+
for (const [code, langMap] of translations) {
|
|
151
|
+
const baseline = canonicalDisplay.get(code);
|
|
152
|
+
totalCodes++;
|
|
153
|
+
let hasRealTranslation = false;
|
|
154
|
+
for (const lang of Object.keys(langMap)) {
|
|
155
|
+
if (!baseline || langMap[lang] === baseline) {
|
|
156
|
+
delete langMap[lang];
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
hasRealTranslation = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!hasRealTranslation) {
|
|
163
|
+
translations.delete(code);
|
|
164
|
+
untranslatedCodes++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (translations.size > 0) {
|
|
168
|
+
vs.displayTranslations = translations;
|
|
169
|
+
translatedCount++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (translatedCount > 0) {
|
|
173
|
+
logger.log(`Generated multi-language display maps for ${translatedCount} ValueSets (${multiLangs.join(', ')})`);
|
|
174
|
+
}
|
|
175
|
+
if (untranslatedCodes > 0) {
|
|
176
|
+
logger.warn(`${untranslatedCodes}/${totalCodes} codes had no translations (tx server returned English for all requested languages)`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
126
179
|
return expanded;
|
|
127
180
|
}
|
|
128
181
|
/**
|
|
@@ -145,7 +198,9 @@ export function emitValueSetFiles(valueSets, outputDir, opts) {
|
|
|
145
198
|
if (generatedCount === 0) {
|
|
146
199
|
ensureDirectoryExists(valueSetOutputDir);
|
|
147
200
|
}
|
|
148
|
-
let { filename, content } = generateValueSetTypeScript(valueSet
|
|
201
|
+
let { filename, content } = generateValueSetTypeScript(valueSet, {
|
|
202
|
+
displayTranslations: valueSet.displayTranslations,
|
|
203
|
+
});
|
|
149
204
|
// Handle duplicate filenames by appending a number
|
|
150
205
|
if (usedFilenames) {
|
|
151
206
|
const baseFilename = filename.replace('.ts', '');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { extractPackage, readStructureDefinitionsFromDir, readStructureDefinitionsFromDependencies, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded } from './parser/packageParser.js';
|
|
4
|
+
import { extractPackage, readStructureDefinitionsFromDir, readStructureDefinitionsFromDependencies, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded, readSearchParametersFromDir } from './parser/packageParser.js';
|
|
5
5
|
import { fetchStructureDefinitions, fetchStructureDefinition, registerLocalStructureDefinitions, clearLocalStructureDefinitions, collectValueSetBindingUrls } from './parser/sdParser.js';
|
|
6
6
|
import { ensureDirectoryExists, downloadFile } from './core/utils.js';
|
|
7
7
|
import { getFhirPackagesCacheDir } from './core/cacheConfig.js';
|
|
@@ -9,6 +9,8 @@ import { processStructureDefinition, resetFetchFailureTracking, getFetchFailureC
|
|
|
9
9
|
import { logger } from '../logger.js';
|
|
10
10
|
import { initVersionContext, ctx, versionSlug } from './fhir/versionContext.js';
|
|
11
11
|
import { DEFAULT_FHIR_VERSION } from './fhir/types.js';
|
|
12
|
+
import { FHIR_VERSIONS } from './fhir/versionRegistry.js';
|
|
13
|
+
import { ensureCorePackage } from './fhir/corePackageResolver.js';
|
|
12
14
|
import { spawn } from 'child_process';
|
|
13
15
|
import { buildProfileRegistries, expandValueSetsWithTx, emitValueSetFiles, cleanupStaleValueSetDir, initGenerationContext, enrichValueSetsFromCodeMap, collectReferencedDependencyProfiles, } from './generationHelpers.js';
|
|
14
16
|
/** Resolve the babelfhir-ts CLI version from its own package.json (used to stamp generated packages). */
|
|
@@ -41,6 +43,44 @@ function buildFhirChildTypeMapFromJson() {
|
|
|
41
43
|
return result;
|
|
42
44
|
}
|
|
43
45
|
export { resetFetchFailureTracking, getFetchFailureCount, getFetchFailureWarning };
|
|
46
|
+
/**
|
|
47
|
+
* Load search parameters from the FHIR core package and the IG's extracted root.
|
|
48
|
+
* Merges both sets, with IG params taking precedence for deduplication by code.
|
|
49
|
+
*/
|
|
50
|
+
async function loadSearchParameters(extractedRoot) {
|
|
51
|
+
const slug = versionSlug();
|
|
52
|
+
const coreSpec = FHIR_VERSIONS[slug]?.corePackage;
|
|
53
|
+
const merged = new Map();
|
|
54
|
+
// Load base FHIR search params from the core package
|
|
55
|
+
if (coreSpec) {
|
|
56
|
+
try {
|
|
57
|
+
const coreDir = await ensureCorePackage(coreSpec);
|
|
58
|
+
const coreParams = readSearchParametersFromDir(coreDir);
|
|
59
|
+
for (const [baseType, params] of coreParams) {
|
|
60
|
+
merged.set(baseType, [...params]);
|
|
61
|
+
}
|
|
62
|
+
log.debug(`Loaded ${coreParams.size} base resource type search param groups from core package`);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
log.warn(`Could not load core search parameters: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Load IG-specific search params (may override or extend base)
|
|
69
|
+
const igParams = readSearchParametersFromDir(extractedRoot);
|
|
70
|
+
for (const [baseType, params] of igParams) {
|
|
71
|
+
const existing = merged.get(baseType) || [];
|
|
72
|
+
for (const p of params) {
|
|
73
|
+
if (!existing.some(e => e.code === p.code)) {
|
|
74
|
+
existing.push(p);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
merged.set(baseType, existing);
|
|
78
|
+
}
|
|
79
|
+
if (igParams.size > 0) {
|
|
80
|
+
log.debug(`Loaded ${igParams.size} IG-specific search param groups`);
|
|
81
|
+
}
|
|
82
|
+
return merged;
|
|
83
|
+
}
|
|
44
84
|
/**
|
|
45
85
|
* Copy the fhir-<version>.d.ts ambient module declaration into the output directory.
|
|
46
86
|
* This is required so generated code can import from 'fhir/r4' (or r4b) even when
|
|
@@ -461,9 +501,12 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
461
501
|
// Generate FHIR client (unless --no-client flag)
|
|
462
502
|
if (!flags?.noClient) {
|
|
463
503
|
logger.log('Generating FHIR client...');
|
|
504
|
+
// Load search parameters from base FHIR spec + IG for typed search param generation
|
|
505
|
+
const searchParams = await loadSearchParameters(extractedRoot);
|
|
464
506
|
const { generateClient } = await import('./emitters/client/clientGenerator.js');
|
|
465
507
|
generateClient({
|
|
466
|
-
outputDir
|
|
508
|
+
outputDir,
|
|
509
|
+
searchParams,
|
|
467
510
|
});
|
|
468
511
|
logger.log('FHIR client generated');
|
|
469
512
|
}
|
|
@@ -652,6 +695,7 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
652
695
|
...(originalPkg.canonical && { canonical: originalPkg.canonical }),
|
|
653
696
|
...(originalPkg.fhirVersions && { fhirVersions: originalPkg.fhirVersions }),
|
|
654
697
|
...(flags?.txServer && { txServer: flags.txServer }),
|
|
698
|
+
...(flags?.displayLanguage && { displayLanguage: flags.displayLanguage }),
|
|
655
699
|
...(flags?.dicomweb && { dicomweb: true }),
|
|
656
700
|
...(flags?.noClient && { noClient: true }),
|
|
657
701
|
...(flags?.noClasses && { noClasses: true }),
|
|
@@ -693,8 +737,9 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
|
|
|
693
737
|
await generateIndexFile(outputDir);
|
|
694
738
|
if (!flags?.noClient) {
|
|
695
739
|
logger.log('Generating FHIR client...');
|
|
740
|
+
const searchParams = await loadSearchParameters(extractedRoot);
|
|
696
741
|
const { generateClient } = await import('./emitters/client/clientGenerator.js');
|
|
697
|
-
generateClient({ outputDir });
|
|
742
|
+
generateClient({ outputDir, searchParams });
|
|
698
743
|
logger.log('FHIR client generated');
|
|
699
744
|
}
|
|
700
745
|
// Generate DICOMweb helpers (--dicomweb flag)
|
|
@@ -892,9 +937,11 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
892
937
|
// Generate FHIR client (unless --no-client flag)
|
|
893
938
|
if (!flags?.noClient) {
|
|
894
939
|
logger.log('Generating FHIR client...');
|
|
940
|
+
const searchParams = await loadSearchParameters(inputDir);
|
|
895
941
|
const { generateClient } = await import('./emitters/client/clientGenerator.js');
|
|
896
942
|
generateClient({
|
|
897
|
-
outputDir
|
|
943
|
+
outputDir,
|
|
944
|
+
searchParams,
|
|
898
945
|
});
|
|
899
946
|
logger.log('FHIR client generated');
|
|
900
947
|
}
|
|
@@ -806,3 +806,39 @@ export function readValueSetsFromDir(extractedRoot) {
|
|
|
806
806
|
}
|
|
807
807
|
return map;
|
|
808
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* Read all SearchParameter resources from a package directory.
|
|
811
|
+
* Returns a map of base resource type → array of search parameter definitions.
|
|
812
|
+
*/
|
|
813
|
+
export function readSearchParametersFromDir(extractedRoot) {
|
|
814
|
+
const jsonFiles = findJsonFilesByPrefix(extractedRoot, 'SearchParameter-');
|
|
815
|
+
const map = new Map();
|
|
816
|
+
for (const file of jsonFiles) {
|
|
817
|
+
try {
|
|
818
|
+
const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
819
|
+
if (content.resourceType !== 'SearchParameter')
|
|
820
|
+
continue;
|
|
821
|
+
if (!content.code || !content.type || !content.base?.length)
|
|
822
|
+
continue;
|
|
823
|
+
if (content.status === 'retired')
|
|
824
|
+
continue;
|
|
825
|
+
const param = {
|
|
826
|
+
code: content.code,
|
|
827
|
+
type: content.type,
|
|
828
|
+
base: content.base,
|
|
829
|
+
};
|
|
830
|
+
for (const baseType of param.base) {
|
|
831
|
+
const existing = map.get(baseType) || [];
|
|
832
|
+
// Avoid duplicates by code
|
|
833
|
+
if (!existing.some(p => p.code === param.code)) {
|
|
834
|
+
existing.push(param);
|
|
835
|
+
map.set(baseType, existing);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
catch (err) {
|
|
840
|
+
log.debug(`Skipping invalid JSON while reading SearchParameters ${file}: ${err.message}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return map;
|
|
844
|
+
}
|
|
@@ -159,6 +159,56 @@ export class TxClient {
|
|
|
159
159
|
}
|
|
160
160
|
return results;
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Expand a single ValueSet in multiple languages, returning a display map.
|
|
164
|
+
* @returns Map<code, Record<lang, display>>
|
|
165
|
+
*/
|
|
166
|
+
async expandValueSetMultiLang(valueSetUrl, languages) {
|
|
167
|
+
const displayMap = new Map();
|
|
168
|
+
for (const lang of languages) {
|
|
169
|
+
const langParam = `&displayLanguage=${encodeURIComponent(lang)}`;
|
|
170
|
+
const expandUrl = `${this.txServer}/ValueSet/$expand?url=${encodeURIComponent(valueSetUrl)}&count=${this.maxCodes}${langParam}`;
|
|
171
|
+
try {
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
174
|
+
const response = await fetch(expandUrl, {
|
|
175
|
+
method: 'GET',
|
|
176
|
+
headers: {
|
|
177
|
+
'Accept': 'application/fhir+json',
|
|
178
|
+
'User-Agent': USER_AGENT,
|
|
179
|
+
},
|
|
180
|
+
signal: controller.signal,
|
|
181
|
+
});
|
|
182
|
+
clearTimeout(timeoutId);
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
log.warn(`Failed to expand ${valueSetUrl} for lang=${lang}: HTTP ${response.status}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const valueSet = await response.json();
|
|
188
|
+
if (valueSet.expansion?.contains) {
|
|
189
|
+
for (const item of valueSet.expansion.contains) {
|
|
190
|
+
if (!item.display)
|
|
191
|
+
continue;
|
|
192
|
+
let entry = displayMap.get(item.code);
|
|
193
|
+
if (!entry) {
|
|
194
|
+
entry = {};
|
|
195
|
+
displayMap.set(item.code, entry);
|
|
196
|
+
}
|
|
197
|
+
entry[lang] = item.display;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
203
|
+
log.warn(`Timeout expanding ${valueSetUrl} for lang=${lang}`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
log.warn(`Error expanding ${valueSetUrl} for lang=${lang}: ${err}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return displayMap;
|
|
211
|
+
}
|
|
162
212
|
/**
|
|
163
213
|
* Get statistics about cached expansions
|
|
164
214
|
*/
|
|
@@ -129,10 +129,14 @@ export function getUniformSystem(valueSet) {
|
|
|
129
129
|
* @returns Updated map with expanded concepts
|
|
130
130
|
*/
|
|
131
131
|
export async function expandValueSetsFromTx(valueSets, txClient) {
|
|
132
|
-
// Find ValueSets that need expansion
|
|
132
|
+
// Find ValueSets that need expansion:
|
|
133
|
+
// - No concepts yet (always expand)
|
|
134
|
+
// - Has concepts but displayLanguage is set (re-expand to get translated displays)
|
|
133
135
|
const needsExpansion = [];
|
|
134
136
|
for (const [url, vs] of valueSets) {
|
|
135
|
-
if (
|
|
137
|
+
if (!url)
|
|
138
|
+
continue;
|
|
139
|
+
if (vs.concepts.length === 0 || txClient.displayLanguage) {
|
|
136
140
|
needsExpansion.push(url);
|
|
137
141
|
}
|
|
138
142
|
}
|
package/out/src/main.js
CHANGED
|
@@ -64,7 +64,9 @@ function printUsage() {
|
|
|
64
64
|
console.log(" --registry <url> FHIR package registry URL (default: https://packages.simplifier.net)");
|
|
65
65
|
console.log(" --tx-server <url> Terminology server URL for ValueSet expansion (e.g., https://tx.fhir.org/r4)");
|
|
66
66
|
console.log(" When set, expands ValueSets without explicit codes using $expand operation");
|
|
67
|
-
console.log(" --display-language <lang> BCP-47 language for display terms (e.g., de,
|
|
67
|
+
console.log(" --display-language <lang> BCP-47 language(s) for display terms (e.g., de or de,fr,en).");
|
|
68
|
+
console.log(" Single value replaces concept displays. Comma-separated values");
|
|
69
|
+
console.log(" also generate a multi-language display map with getDisplay() helper.");
|
|
68
70
|
console.log("");
|
|
69
71
|
console.log("Examples:");
|
|
70
72
|
console.log(" babelfhir-ts # Process ./input to ./output");
|
|
@@ -293,7 +295,7 @@ export function parseCliArgs(argv) {
|
|
|
293
295
|
i++;
|
|
294
296
|
continue;
|
|
295
297
|
}
|
|
296
|
-
if (arg === '--display-language') {
|
|
298
|
+
if (arg === '--display-language' || arg === '--display-languages') {
|
|
297
299
|
const val = argv[i + 1];
|
|
298
300
|
if (!val || val.startsWith('-'))
|
|
299
301
|
throw new Error('Missing value for --display-language');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.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",
|