@oystehr/sdk 4.3.9 → 4.3.11

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.
Files changed (38) hide show
  1. package/dist/cjs/client/client.cjs +12 -5
  2. package/dist/cjs/client/client.cjs.map +1 -1
  3. package/dist/cjs/client/client.d.ts +5 -0
  4. package/dist/cjs/index.min.cjs +1 -1
  5. package/dist/cjs/index.min.cjs.map +1 -1
  6. package/dist/cjs/resources/classes/fhir-ext.cjs +196 -4
  7. package/dist/cjs/resources/classes/fhir-ext.cjs.map +1 -1
  8. package/dist/cjs/resources/classes/fhir-ext.d.ts +70 -3
  9. package/dist/cjs/resources/classes/fhir.cjs +14 -3
  10. package/dist/cjs/resources/classes/fhir.cjs.map +1 -1
  11. package/dist/cjs/resources/classes/fhir.d.ts +14 -3
  12. package/dist/cjs/resources/types/FaxSendParams.d.ts +1 -1
  13. package/dist/cjs/resources/types/ZambdaCreateParams.d.ts +1 -1
  14. package/dist/cjs/resources/types/ZambdaUpdateParams.d.ts +1 -1
  15. package/dist/cjs/resources/types/fhir.d.ts +20 -0
  16. package/dist/esm/client/client.d.ts +5 -0
  17. package/dist/esm/client/client.js +12 -5
  18. package/dist/esm/client/client.js.map +1 -1
  19. package/dist/esm/index.min.js +1 -1
  20. package/dist/esm/index.min.js.map +1 -1
  21. package/dist/esm/resources/classes/fhir-ext.d.ts +70 -3
  22. package/dist/esm/resources/classes/fhir-ext.js +194 -5
  23. package/dist/esm/resources/classes/fhir-ext.js.map +1 -1
  24. package/dist/esm/resources/classes/fhir.d.ts +14 -3
  25. package/dist/esm/resources/classes/fhir.js +15 -4
  26. package/dist/esm/resources/classes/fhir.js.map +1 -1
  27. package/dist/esm/resources/types/FaxSendParams.d.ts +1 -1
  28. package/dist/esm/resources/types/ZambdaCreateParams.d.ts +1 -1
  29. package/dist/esm/resources/types/ZambdaUpdateParams.d.ts +1 -1
  30. package/dist/esm/resources/types/fhir.d.ts +20 -0
  31. package/package.json +1 -1
  32. package/src/client/client.ts +22 -8
  33. package/src/resources/classes/fhir-ext.ts +257 -7
  34. package/src/resources/classes/fhir.ts +14 -3
  35. package/src/resources/types/FaxSendParams.ts +1 -1
  36. package/src/resources/types/ZambdaCreateParams.ts +1 -1
  37. package/src/resources/types/ZambdaUpdateParams.ts +1 -1
  38. package/src/resources/types/fhir.ts +22 -0
@@ -9,6 +9,7 @@ import {
9
9
  Bundle,
10
10
  BundleEntry,
11
11
  Coding,
12
+ FhirAsyncBulkOutputResult,
12
13
  FhirAsyncJobHandle,
13
14
  FhirAsyncJobStatus,
14
15
  FhirAsyncWaitOptions,
@@ -266,6 +267,43 @@ export interface OystehrFHIRUpdateClientRequest extends OystehrClientRequest {
266
267
  optimisticLockingVersionId?: string;
267
268
  }
268
269
 
270
+ /**
271
+ * Optional parameter that can be passed to the FHIR create method. In addition
272
+ * to the standard request options, it supports FHIR conditional create via the
273
+ * 'If-None-Exist' header.
274
+ */
275
+ export interface OystehrFHIRCreateClientRequest extends OystehrClientRequest {
276
+ /**
277
+ * Perform a FHIR conditional create using the 'If-None-Exist' header. The value is a
278
+ * FHIR search query that identifies whether a matching resource already exists:
279
+ * - if no resource matches, the resource is created as normal;
280
+ * - if exactly one resource matches, no resource is created and the existing one is returned;
281
+ * - if more than one resource matches, the request fails with a 412 Precondition Failed error.
282
+ *
283
+ * Accepts either a raw search query string (e.g. `'identifier=http://acme.org|1234'`) or an
284
+ * array of SearchParam objects (e.g. `[{ name: 'identifier', value: 'http://acme.org|1234' }]`).
285
+ *
286
+ * @see https://www.hl7.org/fhir/http.html#cond-update for the conditional create specification.
287
+ */
288
+ ifNoneExist?: string | SearchParam[];
289
+ }
290
+
291
+ /**
292
+ * Serializes an If-None-Exist value into a FHIR search query string. A raw string is
293
+ * returned unchanged; an array of SearchParam objects is encoded as a query string
294
+ * (e.g. "identifier=sys|123&active=true").
295
+ */
296
+ function ifNoneExistToString(value: string | SearchParam[]): string {
297
+ if (typeof value === 'string') {
298
+ return value;
299
+ }
300
+ const search = new URLSearchParams();
301
+ for (const param of value) {
302
+ search.append(param.name, String(param.value));
303
+ }
304
+ return search.toString();
305
+ }
306
+
269
307
  function isAsyncRequestMode(mode: FhirResponseMode | undefined): mode is Exclude<FhirResponseMode, 'sync'> {
270
308
  return mode === 'async-bundle' || mode === 'async-bulk';
271
309
  }
@@ -329,37 +367,93 @@ export async function search<T extends FhirResource>(
329
367
  return bundle;
330
368
  }
331
369
 
370
+ /**
371
+ * Performs an iterative FHIR search over initial request and following "next" urls,
372
+ * collecting all pages into a single Bundle.
373
+ *
374
+ * @param params FHIR search parameters plus optional pageSize that will overwrite _count in params
375
+ * @param request optional OystehrClientRequest object
376
+ * @returns FHIR Bundle resource that contains all entries across all pages. Bundle-level metadata
377
+ * (id, meta, total, etc.) is taken from the first page.
378
+ */
379
+ export async function searchAndGetAllPages<T extends FhirResource>(
380
+ this: SDKResource,
381
+ params: FhirSearchParams<T> & { pageSize?: number },
382
+ request?: OystehrClientRequest & { mode?: 'sync' | undefined }
383
+ ): Promise<Bundle<T>> {
384
+ const { pageSize, ...searchParams } = params;
385
+
386
+ let firstPageParams: FhirSearchParams<T> = searchParams;
387
+ if (pageSize) {
388
+ const baseParams = (searchParams.params ?? []).filter((p) => p.name !== '_count') ?? [];
389
+ firstPageParams = { ...searchParams, params: [...baseParams, { name: '_count', value: pageSize }] };
390
+ }
391
+
392
+ const allEntries: Array<BundleEntry<T>> = [];
393
+
394
+ const typedSearch = search<T>;
395
+ // search returns Bundle, and fhirRequest in the while block returns FhirBundle
396
+ let currentBundle: Bundle<T> | FhirBundle<T> = await typedSearch.call(this, firstPageParams, request);
397
+ const firstBundle = { ...currentBundle, link: currentBundle.link?.filter((link) => link.relation !== 'next') };
398
+
399
+ // eslint-disable-next-line no-constant-condition
400
+ while (true) {
401
+ const entries = currentBundle.entry as Array<BundleEntry<T>> | undefined;
402
+ if (entries) {
403
+ allEntries.push(...entries);
404
+ }
405
+
406
+ const nextLink: string | undefined = currentBundle.link?.find((link) => link.relation === 'next')?.url;
407
+ if (!nextLink) {
408
+ break;
409
+ }
410
+ currentBundle = await this.fhirRequest<FhirBundle<T>>(nextLink, 'GET')({}, request);
411
+ }
412
+
413
+ return {
414
+ ...firstBundle,
415
+ entry: allEntries.length ? allEntries : undefined,
416
+ unbundle: function (this: { entry?: Array<BundleEntry<T>> }) {
417
+ return this.entry?.map((e) => e.resource).filter((r): r is T => r !== undefined) ?? [];
418
+ },
419
+ };
420
+ }
421
+
332
422
  export async function create<T extends FhirResource>(
333
423
  this: SDKResource,
334
424
  params: FhirCreateParams<T>,
335
- request: OystehrClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
425
+ request: OystehrFHIRCreateClientRequest & { mode: Exclude<FhirResponseMode, 'sync'> }
336
426
  ): Promise<FhirAsyncJobHandle>;
337
427
  export async function create<T extends FhirResource>(
338
428
  this: SDKResource,
339
429
  params: FhirCreateParams<T>,
340
- request?: OystehrClientRequest & { mode?: 'sync' | undefined }
430
+ request?: OystehrFHIRCreateClientRequest & { mode?: 'sync' | undefined }
341
431
  ): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>>>;
342
432
  export async function create<T extends FhirResource>(
343
433
  this: SDKResource,
344
434
  params: FhirCreateParams<T>,
345
- request?: OystehrClientRequest
435
+ request?: OystehrFHIRCreateClientRequest
346
436
  ): Promise<FhirFetcherResponse<FhirResourceReturnValue<T>> | FhirAsyncJobHandle> {
347
437
  const tagged = applyTagToResource(this.config, params);
348
438
  const { resourceType } = tagged;
349
439
  const requestMode = request?.mode;
440
+ const ifNoneExistRequest = {
441
+ ...request,
442
+ ifNoneExist: request?.ifNoneExist !== undefined ? ifNoneExistToString(request.ifNoneExist) : undefined,
443
+ };
350
444
  if (isAsyncRequestMode(requestMode)) {
351
445
  return await this.startAsyncJob(
352
446
  `/${resourceType}`,
353
447
  'POST',
354
448
  tagged as unknown as Record<string, unknown>,
355
449
  requestMode,
356
- request
450
+ ifNoneExistRequest
357
451
  );
358
452
  }
359
453
 
360
454
  return await this.fhirRequest<FhirResourceReturnValue<T>>(`/${resourceType}`, 'POST')(
361
455
  tagged as unknown as Record<string, unknown>,
362
- request
456
+ ifNoneExistRequest
363
457
  );
364
458
  }
365
459
 
@@ -510,6 +604,13 @@ function getRetryDelayMs(retryAfter: string | undefined, fallbackMs: number): nu
510
604
  return fallbackMs;
511
605
  }
512
606
 
607
+ /**
608
+ * 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.
609
+ * @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)`)
610
+ * @param jobId The ID of the async job to fetch
611
+ * @param request Optional OystehrClientRequest for authentication and headers
612
+ * @returns A Promise that resolves to the FhirAsyncJobStatus
613
+ */
513
614
  export async function getAsyncJob<T extends FhirResource>(
514
615
  this: SDKResource,
515
616
  jobId: string,
@@ -518,6 +619,14 @@ export async function getAsyncJob<T extends FhirResource>(
518
619
  return await this.fetchAsyncJobStatus<T>(jobId, request);
519
620
  }
520
621
 
622
+ /**
623
+ * 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.
624
+ * @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)`)
625
+ * @param jobId The ID of the async job to wait for
626
+ * @param options Optional FhirAsyncWaitOptions to configure polling behavior
627
+ * @param request Optional OystehrClientRequest for authentication and headers
628
+ * @returns A Promise that resolves to the final FhirAsyncJobStatus
629
+ */
521
630
  export async function waitForAsyncJob<T extends FhirResource>(
522
631
  this: SDKResource,
523
632
  jobId: string,
@@ -548,6 +657,143 @@ export async function waitForAsyncJob<T extends FhirResource>(
548
657
  });
549
658
  }
550
659
 
660
+ function parseNdjsonResources<T extends FhirResource>(ndjson: string, sourceUrl: string): T[] {
661
+ const resources: T[] = [];
662
+ const lines = ndjson.split('\n');
663
+ for (let index = 0; index < lines.length; index++) {
664
+ const line = lines[index].trim();
665
+ if (line.length === 0) {
666
+ continue;
667
+ }
668
+ try {
669
+ resources.push(JSON.parse(line) as T);
670
+ } catch (error) {
671
+ throw new OystehrSdkError({
672
+ message: `Failed to parse NDJSON line ${index + 1} from ${sourceUrl}`,
673
+ code: 500,
674
+ cause: error,
675
+ });
676
+ }
677
+ }
678
+ return resources;
679
+ }
680
+
681
+ /**
682
+ * 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.
683
+ * @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)`)
684
+ * @param jobId The ID of the async job to wait for
685
+ * @param options Optional FhirAsyncWaitOptions to configure polling behavior
686
+ * @param request Optional OystehrClientRequest for authentication and headers
687
+ * @returns A Promise that resolves to a FhirAsyncBulkOutputResult containing the bulk output manifest and files
688
+ */
689
+ export async function waitForAsyncBulkOutput<T extends FhirResource>(
690
+ this: SDKResource,
691
+ jobId: string,
692
+ options?: FhirAsyncWaitOptions,
693
+ request?: OystehrClientRequest
694
+ ): Promise<FhirAsyncBulkOutputResult<T>> {
695
+ const status = await waitForAsyncJob.call(this, jobId, options, request);
696
+
697
+ if (status.status === 404) {
698
+ throw new OystehrSdkError({
699
+ message: `Async job ${jobId} not found`,
700
+ code: 404,
701
+ });
702
+ }
703
+
704
+ if (status.status === 410) {
705
+ throw new OystehrSdkError({
706
+ message: `Async job ${jobId} expired`,
707
+ code: 410,
708
+ });
709
+ }
710
+
711
+ if (status.status !== 200 || !('mode' in status) || status.mode !== 'bulk') {
712
+ throw new OystehrSdkError({
713
+ message: `Async job ${jobId} did not complete in bulk mode`,
714
+ code: status.status,
715
+ });
716
+ }
717
+
718
+ const accessToken = request?.accessToken ?? this.config.accessToken;
719
+ const projectId = request?.projectId ?? this.config.projectId;
720
+ if (status.manifest.requiresAccessToken && !accessToken) {
721
+ throw new OystehrSdkError({
722
+ message: `Bulk output for async job ${jobId} requires an access token`,
723
+ code: 401,
724
+ });
725
+ }
726
+
727
+ const fetchImpl = this.config.fetch ?? fetch;
728
+ const headers: Record<string, string> = {};
729
+ if (projectId) {
730
+ headers['x-zapehr-project-id'] = projectId;
731
+ headers['x-oystehr-project-id'] = projectId;
732
+ }
733
+ if (status.manifest.requiresAccessToken && accessToken) {
734
+ headers.Authorization = `Bearer ${accessToken}`;
735
+ }
736
+ const requestHeaders = Object.keys(headers).length > 0 ? headers : undefined;
737
+
738
+ const output = await Promise.all(
739
+ status.manifest.output.map(async (file) => {
740
+ const response = await fetchImpl(
741
+ new Request(file.url, {
742
+ method: 'GET',
743
+ headers: requestHeaders,
744
+ })
745
+ );
746
+
747
+ if (!response.ok) {
748
+ throw new OystehrSdkError({
749
+ message: `Failed to download bulk output (${file.type}): HTTP ${response.status}`,
750
+ code: response.status,
751
+ });
752
+ }
753
+
754
+ const ndjson = await response.text();
755
+ return {
756
+ ...file,
757
+ resources: parseNdjsonResources<T>(ndjson, file.url),
758
+ };
759
+ })
760
+ );
761
+
762
+ return {
763
+ manifest: status.manifest,
764
+ output,
765
+ };
766
+ }
767
+
768
+ /**
769
+ * 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.
770
+ * 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.
771
+ * @param jobId the ID of the async job to wait for
772
+ * @param options optional FhirAsyncWaitOptions to configure polling behavior
773
+ * @param request optional OystehrClientRequest for authentication and headers
774
+ * @returns a Promise that resolves to a Bundle containing all resources from the bulk output
775
+ */
776
+ export async function waitForAsyncBulkBundle<T extends FhirResource>(
777
+ this: SDKResource,
778
+ jobId: string,
779
+ options?: FhirAsyncWaitOptions,
780
+ request?: OystehrClientRequest
781
+ ): Promise<Bundle<T>> {
782
+ const bulkOutput = await waitForAsyncBulkOutput.call(this, jobId, options, request);
783
+ const resources = bulkOutput.output.flatMap((file) => file.resources);
784
+
785
+ const bundle = {
786
+ resourceType: 'Bundle',
787
+ type: 'collection',
788
+ entry: resources.map((resource) => ({ resource } as BundleEntry<T>)),
789
+ unbundle: function (this: { entry?: Array<BundleEntry<T>> | undefined }) {
790
+ return this.entry?.map((entry) => entry.resource).filter((value): value is T => value !== undefined) ?? [];
791
+ },
792
+ } as unknown as Bundle<T>;
793
+
794
+ return bundle;
795
+ }
796
+
551
797
  export async function cancelAsyncJob(this: SDKResource, jobId: string, request?: OystehrClientRequest): Promise<void> {
552
798
  await this.fhirRequest(`/async-job/${jobId}`, 'DELETE')({}, request);
553
799
  }
@@ -741,9 +987,13 @@ function batchInputRequestToBundleEntryItem<T extends FhirResource>(
741
987
  // POST creates require a full resource
742
988
  if (method === 'POST' && 'resource' in request) {
743
989
  const resource = applyTagToResource(config, request.resource);
744
- const { fullUrl } = request;
990
+ const { fullUrl, ifNoneExist } = request;
745
991
  return {
746
- ...baseRequest,
992
+ request: {
993
+ ...baseRequest.request,
994
+ // Conditional create: only create the resource if no existing resource matches the query.
995
+ ifNoneExist: ifNoneExist !== undefined ? ifNoneExistToString(ifNoneExist) : undefined,
996
+ },
747
997
  resource: resource as T,
748
998
  fullUrl,
749
999
  } as BundleEntry<T>;
@@ -18,14 +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
- getAsyncJob = ext.getAsyncJob;
24
- waitForAsyncJob = ext.waitForAsyncJob;
25
- cancelAsyncJob = ext.cancelAsyncJob;
26
32
  update = ext.update;
27
33
  patch = ext.patch;
28
34
  delete = ext.delete;
35
+ getAsyncJob = ext.getAsyncJob;
36
+ waitForAsyncJob = ext.waitForAsyncJob;
37
+ waitForAsyncBulkOutput = ext.waitForAsyncBulkOutput;
38
+ waitForAsyncBulkBundle = ext.waitForAsyncBulkBundle;
39
+ cancelAsyncJob = ext.cancelAsyncJob;
29
40
  history = ext.history;
30
41
  batch = ext.batch;
31
42
  transaction = ext.transaction;
@@ -2,7 +2,7 @@
2
2
 
3
3
  export interface FaxSendParams {
4
4
  /**
5
- * A Z3 URL of the document you want to send. Your user must have access to this document.
5
+ * A Z3 URL of the PDF document you want to send. Your user must have access to this document.
6
6
  */
7
7
  media: string;
8
8
  /**
@@ -15,7 +15,7 @@ export interface ZambdaCreateParams {
15
15
  /**
16
16
  * The runtime to use for the Zambda Function.
17
17
  */
18
- runtime: 'nodejs20.x' | 'nodejs22.x' | 'nodejs24.x' | 'python3.13' | 'python3.12' | 'java21' | 'dotnet8' | 'ruby3.3';
18
+ runtime: 'nodejs22.x' | 'nodejs24.x' | 'python3.13' | 'python3.12' | 'java21' | 'dotnet8' | 'ruby3.3';
19
19
  /**
20
20
  * The amount of memory in MB to allocate to the Zambda Function. If not specified, a system default (1024MB) will be used. Min: 128MB, Max: 10240MB.
21
21
  */
@@ -15,7 +15,7 @@ export interface ZambdaUpdateParams {
15
15
  /**
16
16
  * The runtime to use for the Zambda Function.
17
17
  */
18
- runtime?: 'nodejs20.x' | 'nodejs22.x' | 'nodejs24.x' | 'python3.13' | 'python3.12' | 'java21' | 'dotnet8' | 'ruby3.3';
18
+ runtime?: 'nodejs22.x' | 'nodejs24.x' | 'python3.13' | 'python3.12' | 'java21' | 'dotnet8' | 'ruby3.3';
19
19
  /**
20
20
  * The amount of memory in MB to allocate to the Zambda Function. If not specified, a system default (1024MB) will be used. Min: 128MB, Max: 10240MB.
21
21
  */
@@ -127,6 +127,19 @@ export interface BatchInputPostRequest<F extends FhirResource> extends BatchInpu
127
127
  method: 'POST';
128
128
  resource: F;
129
129
  fullUrl?: string;
130
+ /**
131
+ * Perform a FHIR conditional create for this entry using the bundle entry's `ifNoneExist`
132
+ * field. The value is a FHIR search query that identifies whether a matching resource already
133
+ * exists: if no resource matches, the resource is created; if exactly one matches, no resource
134
+ * is created and the existing one is returned; if more than one matches, the entry fails with a
135
+ * 412 Precondition Failed error.
136
+ *
137
+ * Accepts either a raw search query string (e.g. `'identifier=http://acme.org|1234'`) or an
138
+ * array of SearchParam objects (e.g. `[{ name: 'identifier', value: 'http://acme.org|1234' }]`).
139
+ *
140
+ * @see https://www.hl7.org/fhir/http.html#cond-update
141
+ */
142
+ ifNoneExist?: string | SearchParam[];
130
143
  }
131
144
 
132
145
  /**
@@ -227,6 +240,15 @@ export interface FhirAsyncJobCompletedBulk {
227
240
  manifest: FhirAsyncBulkManifest;
228
241
  }
229
242
 
243
+ export interface FhirAsyncBulkOutputFileResult<T extends FhirResource = FhirResource> extends FhirAsyncBulkOutputFile {
244
+ resources: T[];
245
+ }
246
+
247
+ export interface FhirAsyncBulkOutputResult<T extends FhirResource = FhirResource> {
248
+ manifest: FhirAsyncBulkManifest;
249
+ output: FhirAsyncBulkOutputFileResult<T>[];
250
+ }
251
+
230
252
  export interface FhirAsyncJobExpired {
231
253
  status: 410;
232
254
  }