@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.
- package/dist/api/builder.d.ts +1 -9
- package/dist/api/builder.js +1 -41
- package/dist/api/generators/typescript.d.ts +12 -0
- package/dist/api/generators/typescript.js +46 -3
- package/dist/api/index.d.ts +0 -2
- package/dist/api/index.js +0 -1
- package/dist/cli/commands/generate.js +2 -12
- package/dist/cli/index.js +57 -1217
- package/dist/cli/utils/prompts.js +0 -22
- package/dist/config.d.ts +0 -39
- package/dist/config.js +0 -108
- package/package.json +1 -1
- package/dist/api/generators/rest-client.d.ts +0 -117
- package/dist/api/generators/rest-client.js +0 -847
- package/dist/api/generators/search-parameter-enhancer.d.ts +0 -185
- package/dist/api/generators/search-parameter-enhancer.js +0 -801
- package/dist/api/generators/validation-generator.d.ts +0 -126
- package/dist/api/generators/validation-generator.js +0 -632
|
@@ -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
|
-
}
|