@atomic-ehr/codegen 0.0.1-canary.20250830224431.6d211a5 → 0.0.1-canary.20250831211734.bb1536b

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.
@@ -1,847 +0,0 @@
1
- /**
2
- * REST Client Generator
3
- *
4
- * Generates a fetch-based FHIR REST client with TypeScript autocompletion
5
- * and type safety for all FHIR resources.
6
- */
7
- import { mkdir, writeFile } from "node:fs/promises";
8
- import { dirname, join } from "node:path";
9
- import { createLogger } from "../../utils/codegen-logger.js";
10
- import { SearchParameterEnhancer } from "./search-parameter-enhancer.js";
11
- import { ValidationGenerator } from "./validation-generator.js";
12
- /**
13
- * REST Client Generator
14
- *
15
- * Generates a type-safe FHIR REST client with autocompletion for all
16
- * available resource types.
17
- */
18
- export class RestClientGenerator {
19
- options;
20
- resourceTypes = new Set();
21
- searchParameterEnhancer;
22
- validationGenerator;
23
- logger;
24
- constructor(options) {
25
- this.options = {
26
- clientName: "FHIRClient",
27
- includeValidation: false,
28
- includeErrorHandling: true,
29
- includeRequestInterceptors: false,
30
- baseUrlOverride: "",
31
- enhancedSearch: false,
32
- includeUtilities: true,
33
- generateValidators: false,
34
- useCanonicalManager: true,
35
- defaultTimeout: 30000,
36
- defaultRetries: 0,
37
- includeDocumentation: true,
38
- generateExamples: false,
39
- chainedSearchBuilder: false,
40
- searchAutocomplete: true,
41
- generateValueSetEnums: true,
42
- ...options,
43
- };
44
- this.logger = options.logger || createLogger({ prefix: "REST" });
45
- this.logger.debug(`REST client configured: ${this.options.clientName}`);
46
- this.searchParameterEnhancer = new SearchParameterEnhancer({
47
- autocomplete: this.options.searchAutocomplete ?? false,
48
- valueSetEnums: this.options.generateValueSetEnums ?? false,
49
- logger: this.logger.child("Search"),
50
- });
51
- this.validationGenerator = new ValidationGenerator();
52
- }
53
- /**
54
- * Collect resource types from schemas
55
- */
56
- collectResourceTypes(schemas) {
57
- this.resourceTypes.clear();
58
- for (const schema of schemas) {
59
- if (schema.identifier.kind === "resource" &&
60
- schema.identifier.name !== "DomainResource" &&
61
- schema.identifier.name !== "Resource") {
62
- this.resourceTypes.add(schema.identifier.name);
63
- }
64
- }
65
- // Also collect search parameter data if enhanced search is enabled
66
- if (this.options.enhancedSearch) {
67
- this.searchParameterEnhancer.collectResourceData(schemas);
68
- }
69
- // Collect validation data if validation is enabled
70
- if (this.options.includeValidation || this.options.generateValidators) {
71
- this.validationGenerator.collectResourceData(schemas);
72
- }
73
- }
74
- /**
75
- * Generate the REST client from TypeSchema documents
76
- */
77
- async generate(schemas) {
78
- this.collectResourceTypes(schemas);
79
- // Ensure output directory exists
80
- await mkdir(this.options.outputDir, { recursive: true });
81
- const generatedFiles = [];
82
- // Generate main client file
83
- const clientFile = await this.generateClientFile();
84
- const clientPath = join(this.options.outputDir, clientFile.filename);
85
- await this.ensureDirectoryExists(clientPath);
86
- await writeFile(clientPath, clientFile.content, "utf-8");
87
- generatedFiles.push({
88
- ...clientFile,
89
- path: clientPath,
90
- });
91
- // Generate types file
92
- const typesFile = await this.generateTypesFile();
93
- const typesPath = join(this.options.outputDir, typesFile.filename);
94
- await writeFile(typesPath, typesFile.content, "utf-8");
95
- generatedFiles.push({
96
- ...typesFile,
97
- path: typesPath,
98
- });
99
- // Generate enhanced search parameters file if enabled
100
- if (this.options.enhancedSearch) {
101
- const searchParamsFile = await this.generateEnhancedSearchParamsFile();
102
- const searchParamsPath = join(this.options.outputDir, searchParamsFile.filename);
103
- await writeFile(searchParamsPath, searchParamsFile.content, "utf-8");
104
- generatedFiles.push({
105
- ...searchParamsFile,
106
- path: searchParamsPath,
107
- });
108
- }
109
- // Generate validation files if validation is enabled
110
- if (this.options.includeValidation || this.options.generateValidators) {
111
- const validationTypesFile = await this.generateValidationTypesFile();
112
- const validationTypesPath = join(this.options.outputDir, validationTypesFile.filename);
113
- await writeFile(validationTypesPath, validationTypesFile.content, "utf-8");
114
- generatedFiles.push({
115
- ...validationTypesFile,
116
- path: validationTypesPath,
117
- });
118
- const validatorsFile = await this.generateValidatorsFile();
119
- const validatorsPath = join(this.options.outputDir, validatorsFile.filename);
120
- await writeFile(validatorsPath, validatorsFile.content, "utf-8");
121
- generatedFiles.push({
122
- ...validatorsFile,
123
- path: validatorsPath,
124
- });
125
- }
126
- // Generate utility file with ResourceTypeMap
127
- const utilityFile = await this.generateUtilityFile();
128
- const utilityPath = join(this.options.outputDir, utilityFile.filename);
129
- await writeFile(utilityPath, utilityFile.content, "utf-8");
130
- generatedFiles.push({
131
- ...utilityFile,
132
- path: utilityPath,
133
- });
134
- return generatedFiles;
135
- }
136
- /**
137
- * Generate the main client file
138
- */
139
- async generateClientFile() {
140
- const resourceTypesArray = Array.from(this.resourceTypes).sort();
141
- const clientName = this.options.clientName;
142
- // Generate imports conditionally
143
- const enhancedSearchImports = this.options.enhancedSearch
144
- ? `import type {
145
- EnhancedSearchParams,
146
- SearchParameterValidator${this.options.searchAutocomplete ? ",\n\tSearchParamName,\n\tBaseEnhancedSearchParams" : ""}
147
- } from './enhanced-search-params.js';`
148
- : "";
149
- const validationImports = this.options.includeValidation || this.options.generateValidators
150
- ? `import type {
151
- ValidationOptions,
152
- ValidationResult,
153
- ValidationException
154
- } from './validation-types.js';
155
- import { ResourceValidator } from './resource-validators.js';`
156
- : "";
157
- // Always import SearchParams to keep backward compatibility typings
158
- const searchParamType = "SearchParams,";
159
- const content = `/**
160
- * FHIR REST Client
161
- *
162
- * Type-safe FHIR REST client with autocompletion for all resource types.
163
- * Generated automatically from FHIR schemas.
164
- */
165
-
166
- import type {
167
- ResourceTypes,
168
- Bundle,
169
- OperationOutcome,
170
- ${resourceTypesArray.join(",\n\t")}
171
- } from '../types';
172
- import type {
173
- ${clientName}Config,
174
- ${searchParamType}
175
- CreateResponse,
176
- UpdateResponse,
177
- DeleteResponse,
178
- ReadResponse,
179
- SearchResponse,
180
- RequestOptions,
181
- HTTPMethod
182
- } from './client-types.js';
183
- ${enhancedSearchImports}
184
- ${validationImports}
185
- import type { ResourceTypeMap } from './utility.js';
186
-
187
- /**
188
- * Main FHIR REST Client
189
- *
190
- * Provides type-safe operations for all FHIR resources with autocompletion.
191
- */
192
- export class ${clientName} {
193
- private baseUrl: string;
194
- private config: Required<${clientName}Config>;
195
-
196
- constructor(baseUrl: string, config: ${clientName}Config = {}) {
197
- this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
198
- this.config = {
199
- timeout: 30000,
200
- retries: 0,
201
- headers: {},
202
- validateResponses: false,
203
- ...config
204
- };
205
- }
206
-
207
- ${this.generateCRUDMethods()}
208
-
209
- ${this.generateSearchMethod()}
210
-
211
- /**
212
- * Get server capability statement
213
- */
214
- async getCapabilities(): Promise<any> {
215
- const url = \`\${this.baseUrl}/metadata\`;
216
- return this.request('GET', url);
217
- }
218
-
219
- /**
220
- * Execute raw HTTP request with full control
221
- */
222
- async request<T = any>(
223
- method: HTTPMethod,
224
- url: string,
225
- body?: any,
226
- options?: RequestOptions
227
- ): Promise<T> {
228
- const requestOptions: RequestInit = {
229
- method,
230
- headers: {
231
- 'Content-Type': 'application/fhir+json',
232
- 'Accept': 'application/fhir+json',
233
- ...this.config.headers,
234
- ...options?.headers
235
- },
236
- signal: options?.signal || (this.config.timeout > 0
237
- ? AbortSignal.timeout(this.config.timeout)
238
- : undefined
239
- )
240
- };
241
-
242
- if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
243
- requestOptions.body = JSON.stringify(body);
244
- }
245
-
246
- ${this.options.includeErrorHandling ? this.generateErrorHandling() : "const response = await fetch(url, requestOptions);"}
247
-
248
- if (!response.ok) {
249
- await this.handleErrorResponse(response);
250
- }
251
-
252
- // Handle different response types
253
- const contentType = response.headers.get('content-type');
254
- if (contentType?.includes('application/json') || contentType?.includes('application/fhir+json')) {
255
- return response.json();
256
- } else if (method === 'DELETE') {
257
- return undefined as T;
258
- } else {
259
- return response.text() as T;
260
- }
261
- }
262
-
263
- ${this.options.includeErrorHandling ? this.generateErrorHandlingMethods() : ""}
264
-
265
- /**
266
- * Update client configuration
267
- */
268
- updateConfig(config: Partial<${clientName}Config>): void {
269
- this.config = { ...this.config, ...config };
270
- }
271
-
272
- /**
273
- * Get current configuration
274
- */
275
- getConfig(): Required<${clientName}Config> {
276
- return { ...this.config };
277
- }
278
-
279
- /**
280
- * Get base URL
281
- */
282
- getBaseUrl(): string {
283
- return this.baseUrl;
284
- }${this.generateValidationMethods()}
285
- }
286
-
287
- export default ${clientName};`;
288
- return {
289
- filename: `${clientName.toLowerCase()}.ts`,
290
- content,
291
- exports: [clientName],
292
- };
293
- }
294
- /**
295
- * Generate the types file
296
- */
297
- async generateTypesFile() {
298
- const content = `/**
299
- * FHIR REST Client Types
300
- *
301
- * Type definitions for the FHIR REST client.
302
- */
303
-
304
- import type { Bundle } from '../types';
305
- import type { ResourceTypeMap } from './utility.js';
306
-
307
- /**
308
- * HTTP methods supported by the client
309
- */
310
- export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
311
-
312
- /**
313
- * Client configuration options
314
- */
315
- export interface ${this.options.clientName}Config {
316
- /** Request timeout in milliseconds (default: 30000) */
317
- timeout?: number;
318
- /** Number of retries for failed requests (default: 0) */
319
- retries?: number;
320
- /** Default headers to include with all requests */
321
- headers?: Record<string, string>;
322
- /** Whether to validate response schemas (default: false) */
323
- validateResponses?: boolean;${this.generateValidationConfigFields()}
324
- }
325
-
326
- /**
327
- * Request options for individual operations
328
- */
329
- export interface RequestOptions {
330
- /** Additional headers for this request */
331
- headers?: Record<string, string>;
332
- /** AbortSignal to cancel the request */
333
- signal?: AbortSignal;
334
- }
335
-
336
- /**
337
- * Generic search parameters
338
- */
339
- export interface SearchParams {
340
- /** Number of results to return */
341
- _count?: number;
342
- /** Pagination offset */
343
- _offset?: number;
344
- /** Include related resources */
345
- _include?: string | string[];
346
- /** Reverse include */
347
- _revinclude?: string | string[];
348
- /** Summary mode */
349
- _summary?: 'true' | 'false' | 'text' | 'data' | 'count';
350
- /** Elements to include */
351
- _elements?: string | string[];
352
- /** Any other FHIR search parameters */
353
- [key: string]: any;
354
- }
355
-
356
- /**
357
- * Response type for create operations
358
- */
359
- export interface CreateResponse<T extends keyof ResourceTypeMap> {
360
- /** The created resource */
361
- resource: ResourceTypeMap[T];
362
- /** Response status code */
363
- status: number;
364
- /** Response headers */
365
- headers: Headers;
366
- }
367
-
368
- /**
369
- * Response type for read operations
370
- */
371
- export interface ReadResponse<T extends keyof ResourceTypeMap> {
372
- /** The retrieved resource */
373
- resource: ResourceTypeMap[T];
374
- /** Response status code */
375
- status: number;
376
- /** Response headers */
377
- headers: Headers;
378
- }
379
-
380
- /**
381
- * Response type for update operations
382
- */
383
- export interface UpdateResponse<T extends keyof ResourceTypeMap> {
384
- /** The updated resource */
385
- resource: ResourceTypeMap[T];
386
- /** Response status code */
387
- status: number;
388
- /** Response headers */
389
- headers: Headers;
390
- }
391
-
392
- /**
393
- * Response type for delete operations
394
- */
395
- export interface DeleteResponse {
396
- /** Response status code */
397
- status: number;
398
- /** Response headers */
399
- headers: Headers;
400
- }
401
-
402
- /**
403
- * Response type for search operations
404
- */
405
- export interface SearchResponse<T extends keyof ResourceTypeMap> {
406
- /** The search result bundle */
407
- bundle: Bundle<ResourceTypeMap[T]>;
408
- /** Response status code */
409
- status: number;
410
- /** Response headers */
411
- headers: Headers;
412
- }
413
-
414
- /**
415
- * FHIR operation outcome for errors
416
- */
417
- export interface FHIRError extends Error {
418
- /** FHIR OperationOutcome */
419
- operationOutcome?: import('../types').OperationOutcome;
420
- /** HTTP status code */
421
- status?: number;
422
- /** Response headers */
423
- headers?: Headers;
424
- }`;
425
- return {
426
- filename: "client-types.ts",
427
- content,
428
- exports: [
429
- "HTTPMethod",
430
- `${this.options.clientName}Config`,
431
- "RequestOptions",
432
- "SearchParams",
433
- "CreateResponse",
434
- "ReadResponse",
435
- "UpdateResponse",
436
- "DeleteResponse",
437
- "SearchResponse",
438
- "FHIRError",
439
- ],
440
- };
441
- }
442
- /**
443
- * Generate error handling code
444
- */
445
- generateErrorHandling() {
446
- return `let response: Response;
447
- let retryCount = 0;
448
-
449
- while (retryCount <= this.config.retries) {
450
- try {
451
- response = await fetch(url, requestOptions);
452
- break;
453
- } catch (error) {
454
- if (retryCount === this.config.retries) {
455
- throw error;
456
- }
457
- retryCount++;
458
- await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
459
- }
460
- }`;
461
- }
462
- /**
463
- * Generate error handling methods
464
- */
465
- generateErrorHandlingMethods() {
466
- return `
467
- /**
468
- * Handle error responses from the FHIR server
469
- */
470
- private async handleErrorResponse(response: Response): Promise<never> {
471
- let operationOutcome: any;
472
-
473
- try {
474
- const contentType = response.headers.get('content-type');
475
- if (contentType?.includes('application/json') || contentType?.includes('application/fhir+json')) {
476
- operationOutcome = await response.json();
477
- }
478
- } catch {
479
- // Ignore JSON parsing errors
480
- }
481
-
482
- const error = new Error(\`HTTP \${response.status}: \${response.statusText}\`) as any;
483
- error.status = response.status;
484
- error.headers = response.headers;
485
- error.operationOutcome = operationOutcome;
486
-
487
- throw error;
488
- }`;
489
- }
490
- /**
491
- * Set output directory
492
- */
493
- setOutputDir(directory) {
494
- this.options.outputDir = directory;
495
- }
496
- /**
497
- * Update generator options
498
- */
499
- setOptions(options) {
500
- this.options = { ...this.options, ...options };
501
- }
502
- /**
503
- * Get current options
504
- */
505
- getOptions() {
506
- return { ...this.options };
507
- }
508
- /**
509
- * Generate enhanced search parameters file
510
- */
511
- async generateEnhancedSearchParamsFile() {
512
- const content = this.searchParameterEnhancer.generateEnhancedSearchTypes();
513
- const baseExports = [
514
- "EnhancedSearchParams",
515
- "SearchParameterValidator",
516
- "SearchModifiers",
517
- "BaseEnhancedSearchParams",
518
- // Add all resource-specific search param interfaces
519
- ...Array.from(this.resourceTypes)
520
- .sort()
521
- .map((type) => `${type}SearchParams`),
522
- ];
523
- // If autocomplete is enabled, also export the search param name unions
524
- const autocompleteExports = this.options.searchAutocomplete
525
- ? [
526
- "BaseSearchParamName",
527
- "SearchParamName",
528
- ...Array.from(this.resourceTypes)
529
- .sort()
530
- .map((type) => `${type}SearchParamName`),
531
- ]
532
- : [];
533
- const valueSetEnumExports = this.options.generateValueSetEnums
534
- ? ["PatientGender", "ObservationStatus", "ImmunizationStatus"]
535
- : [];
536
- return {
537
- filename: "enhanced-search-params.ts",
538
- content,
539
- exports: [...baseExports, ...autocompleteExports, ...valueSetEnumExports],
540
- };
541
- }
542
- /**
543
- * Generate validation configuration fields
544
- */
545
- generateValidationConfigFields() {
546
- if (this.options.includeValidation || this.options.generateValidators) {
547
- return `
548
- /** Client-side validation options */
549
- validation?: {
550
- /** Enable client-side validation (default: false) */
551
- enabled?: boolean;
552
- /** Validation profile to use (default: 'strict') */
553
- profile?: 'strict' | 'lenient' | 'minimal';
554
- /** Whether to throw on validation errors (default: false) */
555
- throwOnError?: boolean;
556
- /** Whether to validate before sending requests (default: true) */
557
- validateBeforeRequest?: boolean;
558
- /** Whether to validate received responses (default: false) */
559
- validateResponses?: boolean;
560
- };`;
561
- }
562
- return "";
563
- }
564
- /**
565
- * Generate validation types file
566
- */
567
- async generateValidationTypesFile() {
568
- const content = this.validationGenerator.generateValidationTypes();
569
- return {
570
- filename: "validation-types.ts",
571
- content,
572
- exports: [
573
- "ValidationOptions",
574
- "ValidationError",
575
- "ValidationWarning",
576
- "ValidationResult",
577
- "ValidationException",
578
- ],
579
- };
580
- }
581
- /**
582
- * Generate resource validators file
583
- */
584
- async generateValidatorsFile() {
585
- const content = this.validationGenerator.generateResourceValidators();
586
- return {
587
- filename: "resource-validators.ts",
588
- content,
589
- exports: [
590
- "ResourceValidator",
591
- ...Array.from(this.resourceTypes)
592
- .sort()
593
- .map((type) => `validate${type}`),
594
- ],
595
- };
596
- }
597
- /**
598
- * Generate CRUD methods with conditional validation support
599
- */
600
- generateCRUDMethods() {
601
- const validationEnabled = this.options.includeValidation || this.options.generateValidators;
602
- return ` /**
603
- * Create a new FHIR resource
604
- */
605
- async create<T extends ResourceTypes>(
606
- resource: ResourceTypeMap[T],
607
- options?: RequestOptions
608
- ): Promise<CreateResponse<ResourceTypeMap[T]>> {
609
- const resourceType = resource.resourceType as T;
610
- const url = \`\${this.baseUrl}/\${resourceType}\`;
611
-
612
- ${validationEnabled ? this.generateValidationCode("create", "resource") : ""}
613
-
614
- return this.request<ResourceTypeMap[T]>('POST', url, resource, options);
615
- }
616
-
617
- /**
618
- * Read a FHIR resource by ID
619
- */
620
- async read<T extends ResourceTypes>(
621
- resourceType: T,
622
- id: string,
623
- options?: RequestOptions
624
- ): Promise<ReadResponse<ResourceTypeMap[T]>> {
625
- const url = \`\${this.baseUrl}/\${resourceType}/\${id}\`;
626
-
627
- return this.request<ResourceTypeMap[T]>('GET', url, undefined, options);
628
- }
629
-
630
- /**
631
- * Update a FHIR resource
632
- */
633
- async update<T extends ResourceTypes>(
634
- resource: ResourceTypeMap[T],
635
- options?: RequestOptions
636
- ): Promise<UpdateResponse<ResourceTypeMap[T]>> {
637
- const resourceType = resource.resourceType as T;
638
- const id = (resource as any).id;
639
-
640
- if (!id) {
641
- throw new Error('Resource must have an id to be updated');
642
- }
643
-
644
- const url = \`\${this.baseUrl}/\${resourceType}/\${id}\`;
645
-
646
- ${validationEnabled ? this.generateValidationCode("update", "resource") : ""}
647
-
648
- return this.request<ResourceTypeMap[T]>('PUT', url, resource, options);
649
- }
650
-
651
- /**
652
- * Delete a FHIR resource
653
- */
654
- async delete<T extends ResourceTypes>(
655
- resourceType: T,
656
- id: string,
657
- options?: RequestOptions
658
- ): Promise<DeleteResponse> {
659
- const url = \`\${this.baseUrl}/\${resourceType}/\${id}\`;
660
-
661
- return this.request<void>('DELETE', url, undefined, options);
662
- }`;
663
- }
664
- /**
665
- * Generate validation code for CRUD operations
666
- */
667
- generateValidationCode(_operation, resourceVar) {
668
- return `// Client-side validation if enabled
669
- if (this.config.validation?.enabled && this.config.validation?.validateBeforeRequest) {
670
- const validationResult = ResourceValidator.validate(${resourceVar}, {
671
- profile: this.config.validation.profile || 'strict',
672
- throwOnError: this.config.validation.throwOnError || false,
673
- validateRequired: true,
674
- validateTypes: true,
675
- validateConstraints: true
676
- });
677
-
678
- if (!validationResult.valid && this.config.validation.throwOnError) {
679
- throw new ValidationException(validationResult);
680
- } else if (!validationResult.valid) {
681
- console.warn(\`Validation warnings for \${operation}:\`, validationResult.errors);
682
- }
683
- }`;
684
- }
685
- /**
686
- * Generate the search method with conditional enhanced search support
687
- */
688
- generateSearchMethod() {
689
- if (this.options.enhancedSearch) {
690
- const autocompleteOverload = this.options.searchAutocomplete
691
- ? `\n\tasync search<T extends ResourceTypes>(\n\t\tresourceType: T,\n\t\tparams?: Partial<Record<SearchParamName<T>, any>> & BaseEnhancedSearchParams,\n\t\toptions?: RequestOptions\n\t): Promise<SearchResponse<ResourceTypeMap[T]>>;`
692
- : "";
693
- return `\t/**
694
- \t * Search for FHIR resources
695
- \t */
696
- \tasync search<T extends ResourceTypes>(
697
- \t\tresourceType: T,
698
- \t\tparams?: EnhancedSearchParams<T>,
699
- \t\toptions?: RequestOptions
700
- \t): Promise<SearchResponse<ResourceTypeMap[T]>>;${autocompleteOverload}
701
- \tasync search<T extends ResourceTypes>(
702
- \t\tresourceType: T,
703
- \t\tparams?: SearchParams,
704
- \t\toptions?: RequestOptions
705
- \t): Promise<SearchResponse<ResourceTypeMap[T]>>;
706
- \tasync search<T extends ResourceTypes>(
707
- \t\tresourceType: T,
708
- \t\tparams?: any,
709
- \t\toptions?: RequestOptions
710
- \t): Promise<SearchResponse<ResourceTypeMap[T]>> {
711
- \t\tlet url = \`\${this.baseUrl}/\${resourceType}\`;
712
- \t\t
713
- \t\tif (params && Object.keys(params).length > 0) {
714
- \t\t\tlet searchParams: URLSearchParams | undefined;
715
- \t\t\ttry {
716
- \t\t\t\tconst validation = SearchParameterValidator.validate(resourceType, params);
717
- \t\t\t\tif (validation.valid) {
718
- \t\t\t\t\tsearchParams = SearchParameterValidator.buildSearchParams(resourceType, params);
719
- \t\t\t\t}
720
- \t\t\t} catch {}
721
- \t\t\tif (!searchParams) {
722
- \t\t\t\tsearchParams = new URLSearchParams();
723
- \t\t\t\tfor (const [key, value] of Object.entries(params)) {
724
- \t\t\t\t\tif (Array.isArray(value)) {
725
- \t\t\t\t\t\tvalue.forEach((v) => searchParams!.append(key, String(v)));
726
- \t\t\t\t\t} else if (value !== undefined) {
727
- \t\t\t\t\t\tsearchParams.append(key, String(value));
728
- \t\t\t\t\t}
729
- \t\t\t\t}
730
- \t\t\t}
731
- \t\t\turl += \`?\${searchParams.toString()}\`;
732
- \t\t}
733
- \t\t
734
- \t\treturn this.request<Bundle<ResourceTypeMap[T]>>('GET', url, undefined, options);
735
- \t}`;
736
- }
737
- // Non-enhanced search: keep original behavior
738
- const paramHandling = this.generateSearchParameterHandlingCode();
739
- return `\t/**
740
- \t * Search for FHIR resources
741
- \t */
742
- \tasync search<T extends ResourceTypes>(
743
- \t\tresourceType: T,
744
- \t\tparams?: SearchParams,
745
- \t\toptions?: RequestOptions
746
- \t): Promise<SearchResponse<ResourceTypeMap[T]>> {
747
- \t\tlet url = \`\${this.baseUrl}/\${resourceType}\`;
748
- \t\t
749
- \t\tif (params && Object.keys(params).length > 0) {
750
- \t\t\t${paramHandling}
751
- \t\t\turl += \`?\${searchParams.toString()}\`;
752
- \t\t}
753
- \t\t
754
- \t\treturn this.request<Bundle<ResourceTypeMap[T]>>('GET', url, undefined, options);
755
- \t}`;
756
- }
757
- /**
758
- * Generate validation methods for the client
759
- */
760
- generateValidationMethods() {
761
- if (this.options.includeValidation || this.options.generateValidators) {
762
- return `
763
-
764
- /**
765
- * Validate a FHIR resource without sending it to the server
766
- */
767
- validate<T extends ResourceTypes>(
768
- resource: ResourceTypeMap[T],
769
- options?: ValidationOptions
770
- ): ValidationResult {
771
- return ResourceValidator.validate(resource, options);
772
- }
773
-
774
- /**
775
- * Check if validation is enabled for this client
776
- */
777
- isValidationEnabled(): boolean {
778
- return this.config.validation?.enabled || false;
779
- }
780
-
781
- /**
782
- * Update validation configuration
783
- */
784
- updateValidationConfig(validationConfig: NonNullable<${this.options.clientName}Config['validation']>): void {
785
- this.config.validation = { ...this.config.validation, ...validationConfig };
786
- }`;
787
- }
788
- return "";
789
- }
790
- /**
791
- * Generate search parameter handling code based on configuration
792
- */
793
- generateSearchParameterHandlingCode() {
794
- if (this.options.enhancedSearch) {
795
- return `// Use enhanced search parameter validation and building
796
- const validation = SearchParameterValidator.validate(resourceType, params);
797
- if (!validation.valid) {
798
- throw new Error(\`Invalid search parameters: \${validation.errors.join(', ')}\`);
799
- }
800
-
801
- const searchParams = SearchParameterValidator.buildSearchParams(resourceType, params);`;
802
- }
803
- else {
804
- return `const searchParams = new URLSearchParams();
805
- for (const [key, value] of Object.entries(params)) {
806
- if (Array.isArray(value)) {
807
- value.forEach(v => searchParams.append(key, String(v)));
808
- } else if (value !== undefined) {
809
- searchParams.append(key, String(value));
810
- }
811
- }`;
812
- }
813
- }
814
- /**
815
- * Generate utility file with ResourceTypeMap
816
- */
817
- async generateUtilityFile() {
818
- const resourceTypesArray = Array.from(this.resourceTypes).sort();
819
- const content = `/**
820
- * Utility types for FHIR REST Client
821
- *
822
- * Shared type definitions and utilities.
823
- */
824
-
825
- // Import all the resource types
826
- ${resourceTypesArray.map((type) => `import type { ${type} } from '../types/${type}';`).join("\n")}
827
-
828
- export type ResourceTypes = ${resourceTypesArray.map((type) => `'${type}'`).join(" | ")};
829
-
830
- /**
831
- * Resource type mapping from resource type strings to interfaces
832
- */
833
- export type ResourceTypeMap = {
834
- ${resourceTypesArray.map((type) => ` '${type}': ${type};`).join("\n")}
835
- };
836
- `;
837
- return {
838
- filename: "utility.ts",
839
- content: content,
840
- exports: ["ResourceTypes", "ResourceTypeMap", ...resourceTypesArray],
841
- };
842
- }
843
- async ensureDirectoryExists(filePath) {
844
- const dir = dirname(filePath);
845
- await mkdir(dir, { recursive: true });
846
- }
847
- }