@oystehr/sdk 4.3.8 → 4.3.10
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 +105 -41
- package/dist/cjs/client/client.cjs +137 -5
- package/dist/cjs/client/client.cjs.map +1 -1
- package/dist/cjs/client/client.d.ts +28 -3
- package/dist/cjs/index.min.cjs +1 -1
- package/dist/cjs/index.min.cjs.map +1 -1
- package/dist/cjs/resources/classes/fhir-ext.cjs +277 -17
- package/dist/cjs/resources/classes/fhir-ext.cjs.map +1 -1
- package/dist/cjs/resources/classes/fhir-ext.d.ts +114 -12
- package/dist/cjs/resources/classes/fhir.cjs +14 -0
- package/dist/cjs/resources/classes/fhir.cjs.map +1 -1
- package/dist/cjs/resources/classes/fhir.d.ts +14 -0
- package/dist/cjs/resources/types/FaxSendParams.d.ts +1 -1
- package/dist/cjs/resources/types/ZambdaCreateParams.d.ts +1 -1
- package/dist/cjs/resources/types/ZambdaUpdateParams.d.ts +1 -1
- package/dist/cjs/resources/types/fhir.d.ts +74 -0
- package/dist/esm/client/client.d.ts +28 -3
- package/dist/esm/client/client.js +137 -5
- package/dist/esm/client/client.js.map +1 -1
- package/dist/esm/index.min.js +1 -1
- package/dist/esm/index.min.js.map +1 -1
- package/dist/esm/resources/classes/fhir-ext.d.ts +114 -12
- package/dist/esm/resources/classes/fhir-ext.js +273 -19
- package/dist/esm/resources/classes/fhir-ext.js.map +1 -1
- package/dist/esm/resources/classes/fhir.d.ts +14 -0
- package/dist/esm/resources/classes/fhir.js +15 -1
- package/dist/esm/resources/classes/fhir.js.map +1 -1
- package/dist/esm/resources/types/FaxSendParams.d.ts +1 -1
- package/dist/esm/resources/types/ZambdaCreateParams.d.ts +1 -1
- package/dist/esm/resources/types/ZambdaUpdateParams.d.ts +1 -1
- package/dist/esm/resources/types/fhir.d.ts +74 -0
- package/package.json +1 -1
- package/src/client/client.ts +214 -7
- package/src/resources/classes/fhir-ext.ts +483 -38
- package/src/resources/classes/fhir.ts +14 -0
- package/src/resources/types/FaxSendParams.ts +1 -1
- package/src/resources/types/ZambdaCreateParams.ts +1 -1
- package/src/resources/types/ZambdaUpdateParams.ts +1 -1
- package/src/resources/types/fhir.ts +97 -0
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
Bundle,
|
|
10
10
|
BundleEntry,
|
|
11
11
|
Coding,
|
|
12
|
+
FhirAsyncBulkOutputResult,
|
|
13
|
+
FhirAsyncJobHandle,
|
|
14
|
+
FhirAsyncJobStatus,
|
|
15
|
+
FhirAsyncWaitOptions,
|
|
12
16
|
FhirBundle,
|
|
13
17
|
FhirCreateParams,
|
|
14
18
|
FhirDeleteParams,
|
|
@@ -18,6 +22,7 @@ import {
|
|
|
18
22
|
FhirPatchParams,
|
|
19
23
|
FhirResource,
|
|
20
24
|
FhirResourceReturnValue,
|
|
25
|
+
FhirResponseMode,
|
|
21
26
|
FhirSearchParams,
|
|
22
27
|
FhirUpdateParams,
|
|
23
28
|
GenerateFriendlyPatientIdParams,
|
|
@@ -262,6 +267,10 @@ export interface OystehrFHIRUpdateClientRequest extends OystehrClientRequest {
|
|
|
262
267
|
optimisticLockingVersionId?: string;
|
|
263
268
|
}
|
|
264
269
|
|
|
270
|
+
function isAsyncRequestMode(mode: FhirResponseMode | undefined): mode is Exclude<FhirResponseMode, 'sync'> {
|
|
271
|
+
return mode === 'async-bundle' || mode === 'async-bulk';
|
|
272
|
+
}
|
|
273
|
+
|
|
265
274
|
/**
|
|
266
275
|
* Performs a FHIR search and returns the results as a Bundle resource
|
|
267
276
|
*
|
|
@@ -269,11 +278,21 @@ export interface OystehrFHIRUpdateClientRequest extends OystehrClientRequest {
|
|
|
269
278
|
* @param request optional OystehrClientRequest object
|
|
270
279
|
* @returns FHIR Bundle resource
|
|
271
280
|
*/
|
|
281
|
+
export async function search<T extends FhirResource>(
|
|
282
|
+
this: SDKResource,
|
|
283
|
+
params: FhirSearchParams<T>,
|
|
284
|
+
request: OystehrClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
285
|
+
): Promise<FhirAsyncJobHandle>;
|
|
286
|
+
export async function search<T extends FhirResource>(
|
|
287
|
+
this: SDKResource,
|
|
288
|
+
params: FhirSearchParams<T>,
|
|
289
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
290
|
+
): Promise<FhirFetcherResponse<Bundle<T>>>;
|
|
272
291
|
export async function search<T extends FhirResource>(
|
|
273
292
|
this: SDKResource,
|
|
274
293
|
params: FhirSearchParams<T>,
|
|
275
294
|
request?: OystehrClientRequest
|
|
276
|
-
): Promise<FhirFetcherResponse<Bundle<T
|
|
295
|
+
): Promise<FhirFetcherResponse<Bundle<T>> | FhirAsyncJobHandle> {
|
|
277
296
|
const { resourceType } = params;
|
|
278
297
|
const taggedParams = applyTagSearchParams(this.config, params.params);
|
|
279
298
|
let paramMap: Record<string, (string | number)[]> | undefined;
|
|
@@ -286,6 +305,15 @@ export async function search<T extends FhirResource>(
|
|
|
286
305
|
return acc;
|
|
287
306
|
}, {} as Record<string, (string | number)[]>);
|
|
288
307
|
}
|
|
308
|
+
|
|
309
|
+
const requestMode = request?.mode;
|
|
310
|
+
if (isAsyncRequestMode(requestMode)) {
|
|
311
|
+
return await this.startAsyncJob(`/${resourceType}/_search`, 'POST', paramMap ?? {}, requestMode, {
|
|
312
|
+
...request,
|
|
313
|
+
contentType: 'application/x-www-form-urlencoded',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
289
317
|
const requestBundle = await this.fhirRequest<FhirBundle<T>>(`/${resourceType}/_search`, 'POST')(paramMap, {
|
|
290
318
|
...request,
|
|
291
319
|
contentType: 'application/x-www-form-urlencoded',
|
|
@@ -302,76 +330,459 @@ export async function search<T extends FhirResource>(
|
|
|
302
330
|
return bundle;
|
|
303
331
|
}
|
|
304
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Performs an iterative FHIR search over initial request and following "next" urls,
|
|
335
|
+
* collecting all pages into a single Bundle.
|
|
336
|
+
*
|
|
337
|
+
* @param params FHIR search parameters plus optional pageSize that will overwrite _count in params
|
|
338
|
+
* @param request optional OystehrClientRequest object
|
|
339
|
+
* @returns FHIR Bundle resource that contains all entries across all pages. Bundle-level metadata
|
|
340
|
+
* (id, meta, total, etc.) is taken from the first page.
|
|
341
|
+
*/
|
|
342
|
+
export async function searchAndGetAllPages<T extends FhirResource>(
|
|
343
|
+
this: SDKResource,
|
|
344
|
+
params: FhirSearchParams<T> & { pageSize?: number },
|
|
345
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
346
|
+
): Promise<Bundle<T>> {
|
|
347
|
+
const { pageSize, ...searchParams } = params;
|
|
348
|
+
|
|
349
|
+
let firstPageParams: FhirSearchParams<T> = searchParams;
|
|
350
|
+
if (pageSize) {
|
|
351
|
+
const baseParams = (searchParams.params ?? []).filter((p) => p.name !== '_count') ?? [];
|
|
352
|
+
firstPageParams = { ...searchParams, params: [...baseParams, { name: '_count', value: pageSize }] };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const allEntries: Array<BundleEntry<T>> = [];
|
|
356
|
+
|
|
357
|
+
const typedSearch = search<T>;
|
|
358
|
+
// search returns Bundle, and fhirRequest in the while block returns FhirBundle
|
|
359
|
+
let currentBundle: Bundle<T> | FhirBundle<T> = await typedSearch.call(this, firstPageParams, request);
|
|
360
|
+
const firstBundle = { ...currentBundle, link: currentBundle.link?.filter((link) => link.relation !== 'next') };
|
|
361
|
+
|
|
362
|
+
// eslint-disable-next-line no-constant-condition
|
|
363
|
+
while (true) {
|
|
364
|
+
const entries = currentBundle.entry as Array<BundleEntry<T>> | undefined;
|
|
365
|
+
if (entries) {
|
|
366
|
+
allEntries.push(...entries);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const nextLink: string | undefined = currentBundle.link?.find((link) => link.relation === 'next')?.url;
|
|
370
|
+
if (!nextLink) {
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
currentBundle = await this.fhirRequest<FhirBundle<T>>(nextLink, 'GET')({}, request);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
...firstBundle,
|
|
378
|
+
entry: allEntries.length ? allEntries : undefined,
|
|
379
|
+
unbundle: function (this: { entry?: Array<BundleEntry<T>> }) {
|
|
380
|
+
return this.entry?.map((e) => e.resource).filter((r): r is T => r !== undefined) ?? [];
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export async function create<T extends FhirResource>(
|
|
386
|
+
this: SDKResource,
|
|
387
|
+
params: FhirCreateParams<T>,
|
|
388
|
+
request: OystehrClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
389
|
+
): Promise<FhirAsyncJobHandle>;
|
|
390
|
+
export async function create<T extends FhirResource>(
|
|
391
|
+
this: SDKResource,
|
|
392
|
+
params: FhirCreateParams<T>,
|
|
393
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
394
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>>;
|
|
305
395
|
export async function create<T extends FhirResource>(
|
|
306
396
|
this: SDKResource,
|
|
307
397
|
params: FhirCreateParams<T>,
|
|
308
398
|
request?: OystehrClientRequest
|
|
309
|
-
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T
|
|
399
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>> | FhirAsyncJobHandle> {
|
|
310
400
|
const tagged = applyTagToResource(this.config, params);
|
|
311
401
|
const { resourceType } = tagged;
|
|
312
|
-
|
|
402
|
+
const requestMode = request?.mode;
|
|
403
|
+
if (isAsyncRequestMode(requestMode)) {
|
|
404
|
+
return await this.startAsyncJob(
|
|
405
|
+
`/${resourceType}`,
|
|
406
|
+
'POST',
|
|
407
|
+
tagged as unknown as Record<string, unknown>,
|
|
408
|
+
requestMode,
|
|
409
|
+
request
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return await this.fhirRequest<FhirResourceReturnValue<T>>(`/${resourceType}`, 'POST')(
|
|
414
|
+
tagged as unknown as Record<string, unknown>,
|
|
415
|
+
request
|
|
416
|
+
);
|
|
313
417
|
}
|
|
314
418
|
|
|
419
|
+
export async function get<T extends FhirResource>(
|
|
420
|
+
this: SDKResource,
|
|
421
|
+
{ resourceType, id }: FhirGetParams<T>,
|
|
422
|
+
request: OystehrClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
423
|
+
): Promise<FhirAsyncJobHandle>;
|
|
424
|
+
export async function get<T extends FhirResource>(
|
|
425
|
+
this: SDKResource,
|
|
426
|
+
{ resourceType, id }: FhirGetParams<T>,
|
|
427
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
428
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>>;
|
|
315
429
|
export async function get<T extends FhirResource>(
|
|
316
430
|
this: SDKResource,
|
|
317
431
|
{ resourceType, id }: FhirGetParams<T>,
|
|
318
432
|
request?: OystehrClientRequest
|
|
319
|
-
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T
|
|
433
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>> | FhirAsyncJobHandle> {
|
|
434
|
+
const requestMode = request?.mode;
|
|
435
|
+
if (isAsyncRequestMode(requestMode)) {
|
|
436
|
+
return await this.startAsyncJob(`/${resourceType}/${id}`, 'GET', {}, requestMode, request);
|
|
437
|
+
}
|
|
438
|
+
|
|
320
439
|
const result = await this.fhirRequest<FhirResourceReturnValue<T>>(`/${resourceType}/${id}`, 'GET')({}, request);
|
|
321
440
|
assertRetrievedResource(this.config, result);
|
|
322
441
|
return result;
|
|
323
442
|
}
|
|
324
443
|
|
|
444
|
+
export async function update<T extends FhirResource>(
|
|
445
|
+
this: SDKResource,
|
|
446
|
+
params: FhirUpdateParams<T>,
|
|
447
|
+
request: OystehrFHIRUpdateClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
448
|
+
): Promise<FhirAsyncJobHandle>;
|
|
449
|
+
export async function update<T extends FhirResource>(
|
|
450
|
+
this: SDKResource,
|
|
451
|
+
params: FhirUpdateParams<T>,
|
|
452
|
+
request?: OystehrFHIRUpdateClientRequest & { mode?: 'sync' | undefined }
|
|
453
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>>;
|
|
325
454
|
export async function update<T extends FhirResource>(
|
|
326
455
|
this: SDKResource,
|
|
327
456
|
params: FhirUpdateParams<T>,
|
|
328
457
|
request?: OystehrFHIRUpdateClientRequest
|
|
329
|
-
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T
|
|
458
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>> | FhirAsyncJobHandle> {
|
|
330
459
|
const tagged = applyTagToResource(this.config, params);
|
|
331
460
|
const { id, resourceType } = tagged;
|
|
332
|
-
|
|
461
|
+
const requestMode = request?.mode;
|
|
462
|
+
const ifMatchRequest = {
|
|
333
463
|
...request,
|
|
334
464
|
ifMatch: request?.optimisticLockingVersionId ? `W/"${request.optimisticLockingVersionId}"` : undefined,
|
|
335
|
-
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
if (isAsyncRequestMode(requestMode)) {
|
|
468
|
+
return await this.startAsyncJob(
|
|
469
|
+
`/${resourceType}/${id}`,
|
|
470
|
+
'PUT',
|
|
471
|
+
tagged as unknown as Record<string, unknown>,
|
|
472
|
+
requestMode,
|
|
473
|
+
ifMatchRequest
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return await this.fhirRequest<FhirResourceReturnValue<T>>(`/${resourceType}/${id}`, 'PUT')(
|
|
478
|
+
tagged as unknown as Record<string, unknown>,
|
|
479
|
+
ifMatchRequest
|
|
480
|
+
);
|
|
336
481
|
}
|
|
337
482
|
|
|
483
|
+
export async function patch<T extends FhirResource>(
|
|
484
|
+
this: SDKResource,
|
|
485
|
+
params: FhirPatchParams<T>,
|
|
486
|
+
request: OystehrFHIRUpdateClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
487
|
+
): Promise<FhirAsyncJobHandle>;
|
|
488
|
+
export async function patch<T extends FhirResource>(
|
|
489
|
+
this: SDKResource,
|
|
490
|
+
params: FhirPatchParams<T>,
|
|
491
|
+
request?: OystehrFHIRUpdateClientRequest & { mode?: 'sync' | undefined }
|
|
492
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>>;
|
|
338
493
|
export async function patch<T extends FhirResource>(
|
|
339
494
|
this: SDKResource,
|
|
340
495
|
{ resourceType, id, operations }: FhirPatchParams<T>,
|
|
341
496
|
request?: OystehrFHIRUpdateClientRequest
|
|
342
|
-
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T
|
|
497
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>> | FhirAsyncJobHandle> {
|
|
343
498
|
const taggedOperations = applyTagToPatchOperations(this.config, operations);
|
|
344
|
-
|
|
499
|
+
const requestMode = request?.mode;
|
|
500
|
+
const ifMatchRequest = {
|
|
345
501
|
...request,
|
|
346
|
-
contentType: 'application/json-patch+json',
|
|
347
502
|
ifMatch: request?.optimisticLockingVersionId ? `W/"${request.optimisticLockingVersionId}"` : undefined,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
if (isAsyncRequestMode(requestMode)) {
|
|
506
|
+
return await this.startAsyncJob(
|
|
507
|
+
`/${resourceType}/${id}`,
|
|
508
|
+
'PATCH',
|
|
509
|
+
taggedOperations as unknown as Record<string, unknown>,
|
|
510
|
+
requestMode,
|
|
511
|
+
{
|
|
512
|
+
...ifMatchRequest,
|
|
513
|
+
contentType: 'application/json-patch+json',
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return this.fhirRequest<FhirResourceReturnValue<T>>(`/${resourceType}/${id}`, 'PATCH')(taggedOperations, {
|
|
519
|
+
...ifMatchRequest,
|
|
520
|
+
contentType: 'application/json-patch+json',
|
|
348
521
|
});
|
|
349
522
|
}
|
|
350
523
|
|
|
524
|
+
async function del<T extends FhirResource>(
|
|
525
|
+
this: SDKResource,
|
|
526
|
+
params: FhirDeleteParams<T>,
|
|
527
|
+
request: OystehrClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
528
|
+
): Promise<FhirAsyncJobHandle>;
|
|
529
|
+
async function del<T extends FhirResource>(
|
|
530
|
+
this: SDKResource,
|
|
531
|
+
params: FhirDeleteParams<T>,
|
|
532
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
533
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>>;
|
|
351
534
|
async function del<T extends FhirResource>(
|
|
352
535
|
this: SDKResource,
|
|
353
536
|
{ resourceType, id }: FhirDeleteParams<T>,
|
|
354
537
|
request?: OystehrClientRequest
|
|
355
|
-
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T
|
|
356
|
-
|
|
538
|
+
): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>> | FhirAsyncJobHandle> {
|
|
539
|
+
const requestMode = request?.mode;
|
|
540
|
+
if (isAsyncRequestMode(requestMode)) {
|
|
541
|
+
return await this.startAsyncJob(`/${resourceType}/${id}`, 'DELETE', {}, requestMode, request);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return await this.fhirRequest<FhirResourceReturnValue<T>>(`/${resourceType}/${id}`, 'DELETE')({}, request);
|
|
357
545
|
}
|
|
358
546
|
export { del as delete };
|
|
359
547
|
|
|
548
|
+
function getRetryDelayMs(retryAfter: string | undefined, fallbackMs: number): number {
|
|
549
|
+
if (!retryAfter) {
|
|
550
|
+
return fallbackMs;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const asSeconds = Number(retryAfter);
|
|
554
|
+
if (Number.isFinite(asSeconds) && asSeconds >= 0) {
|
|
555
|
+
return Math.max(0, Math.floor(asSeconds * 1000));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const asTimestamp = Date.parse(retryAfter);
|
|
559
|
+
if (Number.isFinite(asTimestamp)) {
|
|
560
|
+
return Math.max(0, asTimestamp - Date.now());
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return fallbackMs;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Fetches the status of an async job. If the job is still in progress, returns an object with status 202. If the job is completed, returns the job result with status 200. If the job has failed, returns an object with status 500 and an OperationOutcome resource describing the failure. If the job has expired, returns an object with status 410.
|
|
568
|
+
* @param this The SDKResource context (this is an extension method and should be called with the SDKResource instance as the context, e.g. `sdkResource.getAsyncJob(jobId)`)
|
|
569
|
+
* @param jobId The ID of the async job to fetch
|
|
570
|
+
* @param request Optional OystehrClientRequest for authentication and headers
|
|
571
|
+
* @returns A Promise that resolves to the FhirAsyncJobStatus
|
|
572
|
+
*/
|
|
573
|
+
export async function getAsyncJob<T extends FhirResource>(
|
|
574
|
+
this: SDKResource,
|
|
575
|
+
jobId: string,
|
|
576
|
+
request?: OystehrClientRequest
|
|
577
|
+
): Promise<FhirAsyncJobStatus<T>> {
|
|
578
|
+
return await this.fetchAsyncJobStatus<T>(jobId, request);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Waits for an async job to complete by polling its status until it reaches a terminal state (success, failure, or expiration) or the specified timeout is reached. Returns the final job status. Throws if the job fails, expires, or does not complete within the timeout.
|
|
583
|
+
* @param this The SDKResource context (this is an extension method and should be called with the SDKResource instance as the context, e.g. `sdkResource.waitForAsyncJob(jobId)`)
|
|
584
|
+
* @param jobId The ID of the async job to wait for
|
|
585
|
+
* @param options Optional FhirAsyncWaitOptions to configure polling behavior
|
|
586
|
+
* @param request Optional OystehrClientRequest for authentication and headers
|
|
587
|
+
* @returns A Promise that resolves to the final FhirAsyncJobStatus
|
|
588
|
+
*/
|
|
589
|
+
export async function waitForAsyncJob<T extends FhirResource>(
|
|
590
|
+
this: SDKResource,
|
|
591
|
+
jobId: string,
|
|
592
|
+
options?: FhirAsyncWaitOptions,
|
|
593
|
+
request?: OystehrClientRequest
|
|
594
|
+
): Promise<FhirAsyncJobStatus<T>> {
|
|
595
|
+
// 5 seconds poll interval by default
|
|
596
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 5000;
|
|
597
|
+
// 15 minutes timout by default
|
|
598
|
+
const timeoutMs = options?.timeoutMs ?? 900000;
|
|
599
|
+
const attempts = Math.max(1, Math.ceil(timeoutMs / pollIntervalMs));
|
|
600
|
+
|
|
601
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
602
|
+
const status = await this.fetchAsyncJobStatus<T>(jobId, request);
|
|
603
|
+
if (status.status !== 202) {
|
|
604
|
+
return status;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (attempt < attempts - 1) {
|
|
608
|
+
const retryAfter = 'retryAfter' in status ? status.retryAfter : undefined;
|
|
609
|
+
await new Promise((resolve) => setTimeout(resolve, getRetryDelayMs(retryAfter, pollIntervalMs)));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
throw new OystehrSdkError({
|
|
614
|
+
message: `Async job ${jobId} did not complete within ${timeoutMs} ms`,
|
|
615
|
+
code: 408,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function parseNdjsonResources<T extends FhirResource>(ndjson: string, sourceUrl: string): T[] {
|
|
620
|
+
const resources: T[] = [];
|
|
621
|
+
const lines = ndjson.split('\n');
|
|
622
|
+
for (let index = 0; index < lines.length; index++) {
|
|
623
|
+
const line = lines[index].trim();
|
|
624
|
+
if (line.length === 0) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
resources.push(JSON.parse(line) as T);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
throw new OystehrSdkError({
|
|
631
|
+
message: `Failed to parse NDJSON line ${index + 1} from ${sourceUrl}`,
|
|
632
|
+
code: 500,
|
|
633
|
+
cause: error,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return resources;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Waits for an async job to complete and retrieves the bulk output manifest and files. Throws if the job fails, expires, or does not complete within the specified timeout.
|
|
642
|
+
* @param this The SDKResource context (this is an extension method and should be called with the SDKResource instance as the context, e.g. `sdkResource.waitForAsyncBulkOutput(jobId)`)
|
|
643
|
+
* @param jobId The ID of the async job to wait for
|
|
644
|
+
* @param options Optional FhirAsyncWaitOptions to configure polling behavior
|
|
645
|
+
* @param request Optional OystehrClientRequest for authentication and headers
|
|
646
|
+
* @returns A Promise that resolves to a FhirAsyncBulkOutputResult containing the bulk output manifest and files
|
|
647
|
+
*/
|
|
648
|
+
export async function waitForAsyncBulkOutput<T extends FhirResource>(
|
|
649
|
+
this: SDKResource,
|
|
650
|
+
jobId: string,
|
|
651
|
+
options?: FhirAsyncWaitOptions,
|
|
652
|
+
request?: OystehrClientRequest
|
|
653
|
+
): Promise<FhirAsyncBulkOutputResult<T>> {
|
|
654
|
+
const status = await waitForAsyncJob.call(this, jobId, options, request);
|
|
655
|
+
|
|
656
|
+
if (status.status === 404) {
|
|
657
|
+
throw new OystehrSdkError({
|
|
658
|
+
message: `Async job ${jobId} not found`,
|
|
659
|
+
code: 404,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (status.status === 410) {
|
|
664
|
+
throw new OystehrSdkError({
|
|
665
|
+
message: `Async job ${jobId} expired`,
|
|
666
|
+
code: 410,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (status.status !== 200 || !('mode' in status) || status.mode !== 'bulk') {
|
|
671
|
+
throw new OystehrSdkError({
|
|
672
|
+
message: `Async job ${jobId} did not complete in bulk mode`,
|
|
673
|
+
code: status.status,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const accessToken = request?.accessToken ?? this.config.accessToken;
|
|
678
|
+
const projectId = request?.projectId ?? this.config.projectId;
|
|
679
|
+
if (status.manifest.requiresAccessToken && !accessToken) {
|
|
680
|
+
throw new OystehrSdkError({
|
|
681
|
+
message: `Bulk output for async job ${jobId} requires an access token`,
|
|
682
|
+
code: 401,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const fetchImpl = this.config.fetch ?? fetch;
|
|
687
|
+
const headers: Record<string, string> = {};
|
|
688
|
+
if (projectId) {
|
|
689
|
+
headers['x-zapehr-project-id'] = projectId;
|
|
690
|
+
headers['x-oystehr-project-id'] = projectId;
|
|
691
|
+
}
|
|
692
|
+
if (status.manifest.requiresAccessToken && accessToken) {
|
|
693
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
694
|
+
}
|
|
695
|
+
const requestHeaders = Object.keys(headers).length > 0 ? headers : undefined;
|
|
696
|
+
|
|
697
|
+
const output = await Promise.all(
|
|
698
|
+
status.manifest.output.map(async (file) => {
|
|
699
|
+
const response = await fetchImpl(
|
|
700
|
+
new Request(file.url, {
|
|
701
|
+
method: 'GET',
|
|
702
|
+
headers: requestHeaders,
|
|
703
|
+
})
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
throw new OystehrSdkError({
|
|
708
|
+
message: `Failed to download bulk output (${file.type}): HTTP ${response.status}`,
|
|
709
|
+
code: response.status,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const ndjson = await response.text();
|
|
714
|
+
return {
|
|
715
|
+
...file,
|
|
716
|
+
resources: parseNdjsonResources<T>(ndjson, file.url),
|
|
717
|
+
};
|
|
718
|
+
})
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
manifest: status.manifest,
|
|
723
|
+
output,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Wrapper around waitForAsyncBulkOutput that transforms the retrieved bulk output files into a single Bundle resource containing all the output resources as entries. This is a convenience method for use cases where you want to work with the bulk output as a Bundle, but it may not be efficient for large outputs due to the overhead of downloading and parsing all files and constructing the Bundle in memory.
|
|
729
|
+
* Can be slow due to downloading and parsing potentially large NDJSON files, so use only if you need the full output as a Bundle resource. For more efficient processing of large bulk outputs, use waitForAsyncBulkOutput directly.
|
|
730
|
+
* @param jobId the ID of the async job to wait for
|
|
731
|
+
* @param options optional FhirAsyncWaitOptions to configure polling behavior
|
|
732
|
+
* @param request optional OystehrClientRequest for authentication and headers
|
|
733
|
+
* @returns a Promise that resolves to a Bundle containing all resources from the bulk output
|
|
734
|
+
*/
|
|
735
|
+
export async function waitForAsyncBulkBundle<T extends FhirResource>(
|
|
736
|
+
this: SDKResource,
|
|
737
|
+
jobId: string,
|
|
738
|
+
options?: FhirAsyncWaitOptions,
|
|
739
|
+
request?: OystehrClientRequest
|
|
740
|
+
): Promise<Bundle<T>> {
|
|
741
|
+
const bulkOutput = await waitForAsyncBulkOutput.call(this, jobId, options, request);
|
|
742
|
+
const resources = bulkOutput.output.flatMap((file) => file.resources);
|
|
743
|
+
|
|
744
|
+
const bundle = {
|
|
745
|
+
resourceType: 'Bundle',
|
|
746
|
+
type: 'collection',
|
|
747
|
+
entry: resources.map((resource) => ({ resource } as BundleEntry<T>)),
|
|
748
|
+
unbundle: function (this: { entry?: Array<BundleEntry<T>> | undefined }) {
|
|
749
|
+
return this.entry?.map((entry) => entry.resource).filter((value): value is T => value !== undefined) ?? [];
|
|
750
|
+
},
|
|
751
|
+
} as unknown as Bundle<T>;
|
|
752
|
+
|
|
753
|
+
return bundle;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export async function cancelAsyncJob(this: SDKResource, jobId: string, request?: OystehrClientRequest): Promise<void> {
|
|
757
|
+
await this.fhirRequest(`/async-job/${jobId}`, 'DELETE')({}, request);
|
|
758
|
+
}
|
|
759
|
+
|
|
360
760
|
export async function history<T extends FhirResource>(
|
|
361
761
|
this: SDKResource,
|
|
362
762
|
{ resourceType, id }: FhirHistorySearchParams<T>,
|
|
363
|
-
request
|
|
364
|
-
): Promise<
|
|
763
|
+
request: OystehrClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
764
|
+
): Promise<FhirAsyncJobHandle>;
|
|
365
765
|
export async function history<T extends FhirResource>(
|
|
366
766
|
this: SDKResource,
|
|
367
767
|
{ resourceType, id, versionId }: FhirHistoryGetParams<T>,
|
|
368
|
-
request
|
|
768
|
+
request: OystehrClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
769
|
+
): Promise<FhirAsyncJobHandle>;
|
|
770
|
+
export async function history<T extends FhirResource>(
|
|
771
|
+
this: SDKResource,
|
|
772
|
+
{ resourceType, id, versionId }: FhirHistoryGetParams<T>,
|
|
773
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
369
774
|
): Promise<FhirFetcherResponse<T>>;
|
|
775
|
+
export async function history<T extends FhirResource>(
|
|
776
|
+
this: SDKResource,
|
|
777
|
+
{ resourceType, id }: FhirHistorySearchParams<T>,
|
|
778
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
779
|
+
): Promise<FhirFetcherResponse<Bundle<T>>>;
|
|
370
780
|
export async function history<T extends FhirResource>(
|
|
371
781
|
this: SDKResource,
|
|
372
782
|
{ resourceType, id, count, offset }: FhirHistorySearchParams<T>,
|
|
373
|
-
request?: OystehrClientRequest
|
|
783
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
374
784
|
): Promise<FhirFetcherResponse<Bundle<T>>>;
|
|
785
|
+
|
|
375
786
|
export async function history<T extends FhirResource>(
|
|
376
787
|
this: SDKResource,
|
|
377
788
|
{
|
|
@@ -382,18 +793,26 @@ export async function history<T extends FhirResource>(
|
|
|
382
793
|
offset,
|
|
383
794
|
}: { resourceType: string; id: string; versionId?: string; count?: number; offset?: number },
|
|
384
795
|
request?: OystehrClientRequest
|
|
385
|
-
): Promise<FhirFetcherResponse<Bundle<T>> | FhirFetcherResponse<T
|
|
796
|
+
): Promise<FhirFetcherResponse<Bundle<T>> | FhirFetcherResponse<T> | FhirAsyncJobHandle> {
|
|
797
|
+
const requestMode = request?.mode;
|
|
798
|
+
if (isAsyncRequestMode(requestMode)) {
|
|
799
|
+
if (versionId) {
|
|
800
|
+
return await this.startAsyncJob(`/${resourceType}/${id}/_history/${versionId}`, 'GET', {}, requestMode, request);
|
|
801
|
+
}
|
|
802
|
+
return await this.startAsyncJob(`/${resourceType}/${id}/_history`, 'GET', {}, requestMode, request);
|
|
803
|
+
}
|
|
804
|
+
|
|
386
805
|
if (versionId) {
|
|
387
|
-
return this.fhirRequest(`/${resourceType}/${id}/_history/${versionId}`, 'GET')({}, request);
|
|
806
|
+
return this.fhirRequest<T>(`/${resourceType}/${id}/_history/${versionId}`, 'GET')({}, request);
|
|
388
807
|
}
|
|
389
808
|
if (count) {
|
|
390
|
-
return this.fhirRequest(
|
|
809
|
+
return this.fhirRequest<Bundle<T>>(
|
|
391
810
|
`/${resourceType}/${id}/_history?_total=accurate&_count=${count}
|
|
392
811
|
${offset ? `&_offset=${offset}` : ''}`,
|
|
393
812
|
'GET'
|
|
394
813
|
)({}, request);
|
|
395
814
|
}
|
|
396
|
-
return this.fhirRequest(`/${resourceType}/${id}/_history?_total=accurate`, 'GET')({}, request);
|
|
815
|
+
return this.fhirRequest<Bundle<T>>(`/${resourceType}/${id}/_history?_total=accurate`, 'GET')({}, request);
|
|
397
816
|
}
|
|
398
817
|
|
|
399
818
|
/**
|
|
@@ -546,19 +965,32 @@ function batchInputRequestToBundleEntryItem<T extends FhirResource>(
|
|
|
546
965
|
throw new Error('Unrecognized method');
|
|
547
966
|
}
|
|
548
967
|
|
|
968
|
+
export async function batch<BundleContentType extends FhirResource>(
|
|
969
|
+
this: SDKResource,
|
|
970
|
+
input: BatchInput<BundleContentType>,
|
|
971
|
+
request: OystehrClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
972
|
+
): Promise<FhirAsyncJobHandle>;
|
|
973
|
+
export async function batch<BundleContentType extends FhirResource>(
|
|
974
|
+
this: SDKResource,
|
|
975
|
+
input: BatchInput<BundleContentType>,
|
|
976
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
977
|
+
): Promise<FhirFetcherResponse<BatchBundle<BundleContentType>>>;
|
|
549
978
|
export async function batch<BundleContentType extends FhirResource>(
|
|
550
979
|
this: SDKResource,
|
|
551
980
|
input: BatchInput<BundleContentType>,
|
|
552
981
|
request?: OystehrClientRequest
|
|
553
|
-
): Promise<FhirFetcherResponse<BatchBundle<BundleContentType
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
982
|
+
): Promise<FhirFetcherResponse<BatchBundle<BundleContentType>> | FhirAsyncJobHandle> {
|
|
983
|
+
const requestPayload = {
|
|
984
|
+
resourceType: 'Bundle',
|
|
985
|
+
type: 'batch',
|
|
986
|
+
entry: input.requests.map((req) => batchInputRequestToBundleEntryItem(req, this.config)),
|
|
987
|
+
};
|
|
988
|
+
const requestMode = request?.mode;
|
|
989
|
+
if (isAsyncRequestMode(requestMode)) {
|
|
990
|
+
return await this.startAsyncJob('/', 'POST', requestPayload, requestMode, request);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const resp = await this.fhirRequest<BatchBundle<BundleContentType>>('/', 'POST')(requestPayload, request);
|
|
562
994
|
// Validate each GET/HEAD retrieval entry against the tag config.
|
|
563
995
|
// Violations are replaced with a synthetic 404 OperationOutcome entry; batch entries are independent.
|
|
564
996
|
const rawEntries = resp.entry as Array<BundleEntry<BundleContentType>> | undefined;
|
|
@@ -601,19 +1033,32 @@ export async function batch<BundleContentType extends FhirResource>(
|
|
|
601
1033
|
return bundle;
|
|
602
1034
|
}
|
|
603
1035
|
|
|
1036
|
+
export async function transaction<BundleContentType extends FhirResource>(
|
|
1037
|
+
this: SDKResource,
|
|
1038
|
+
input: BatchInput<BundleContentType>,
|
|
1039
|
+
request: OystehrClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
|
|
1040
|
+
): Promise<FhirAsyncJobHandle>;
|
|
1041
|
+
export async function transaction<BundleContentType extends FhirResource>(
|
|
1042
|
+
this: SDKResource,
|
|
1043
|
+
input: BatchInput<BundleContentType>,
|
|
1044
|
+
request?: OystehrClientRequest & { mode?: 'sync' | undefined }
|
|
1045
|
+
): Promise<FhirFetcherResponse<TransactionBundle<BundleContentType>>>;
|
|
604
1046
|
export async function transaction<BundleContentType extends FhirResource>(
|
|
605
1047
|
this: SDKResource,
|
|
606
1048
|
input: BatchInput<BundleContentType>,
|
|
607
1049
|
request?: OystehrClientRequest
|
|
608
|
-
): Promise<FhirFetcherResponse<TransactionBundle<BundleContentType
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1050
|
+
): Promise<FhirFetcherResponse<TransactionBundle<BundleContentType>> | FhirAsyncJobHandle> {
|
|
1051
|
+
const requestPayload = {
|
|
1052
|
+
resourceType: 'Bundle',
|
|
1053
|
+
type: 'transaction',
|
|
1054
|
+
entry: input.requests.map((req) => batchInputRequestToBundleEntryItem(req, this.config)),
|
|
1055
|
+
};
|
|
1056
|
+
const requestMode = request?.mode;
|
|
1057
|
+
if (isAsyncRequestMode(requestMode)) {
|
|
1058
|
+
return await this.startAsyncJob('/', 'POST', requestPayload, requestMode, request);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const resp = await this.fhirRequest<TransactionBundle<BundleContentType>>('/', 'POST')(requestPayload, request);
|
|
617
1062
|
// Validate each GET/HEAD retrieval entry against the tag config.
|
|
618
1063
|
// A violation throws OystehrFHIRError(404) — transactions are all-or-nothing.
|
|
619
1064
|
if (this.config.workspaceTag || this.config.ignoreTags?.length) {
|
|
@@ -18,11 +18,25 @@ export class Fhir extends SDKResource {
|
|
|
18
18
|
* @returns FHIR Bundle resource
|
|
19
19
|
*/
|
|
20
20
|
search = ext.search;
|
|
21
|
+
/**
|
|
22
|
+
* Performs an iterative FHIR search over initial request and following "next" urls,
|
|
23
|
+
* collecting all pages into a single Bundle.
|
|
24
|
+
* @param params FHIR search parameters plus optional pageSize that will overwrite _count in params
|
|
25
|
+
* @param request optional OystehrClientRequest object
|
|
26
|
+
* @returns FHIR Bundle resource that contains all entries across all pages. Bundle-level metadata
|
|
27
|
+
* (id, meta, total, etc.) is taken from the first page.
|
|
28
|
+
*/
|
|
29
|
+
searchAndGetAllPages = ext.searchAndGetAllPages;
|
|
21
30
|
create = ext.create;
|
|
22
31
|
get = ext.get;
|
|
23
32
|
update = ext.update;
|
|
24
33
|
patch = ext.patch;
|
|
25
34
|
delete = ext.delete;
|
|
35
|
+
getAsyncJob = ext.getAsyncJob;
|
|
36
|
+
waitForAsyncJob = ext.waitForAsyncJob;
|
|
37
|
+
waitForAsyncBulkOutput = ext.waitForAsyncBulkOutput;
|
|
38
|
+
waitForAsyncBulkBundle = ext.waitForAsyncBulkBundle;
|
|
39
|
+
cancelAsyncJob = ext.cancelAsyncJob;
|
|
26
40
|
history = ext.history;
|
|
27
41
|
batch = ext.batch;
|
|
28
42
|
transaction = ext.transaction;
|