@medplum/core 2.0.20 → 2.0.22

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 (47) hide show
  1. package/dist/cjs/index.cjs +448 -289
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.min.cjs +1 -1
  4. package/dist/esm/client.mjs +199 -101
  5. package/dist/esm/client.mjs.map +1 -1
  6. package/dist/esm/crypto.mjs +3 -1
  7. package/dist/esm/crypto.mjs.map +1 -1
  8. package/dist/esm/fhirpath/functions.mjs +179 -129
  9. package/dist/esm/fhirpath/functions.mjs.map +1 -1
  10. package/dist/esm/format.mjs +6 -4
  11. package/dist/esm/format.mjs.map +1 -1
  12. package/dist/esm/hl7.mjs +1 -1
  13. package/dist/esm/hl7.mjs.map +1 -1
  14. package/dist/esm/index.min.mjs +1 -1
  15. package/dist/esm/index.mjs +1 -0
  16. package/dist/esm/index.mjs.map +1 -1
  17. package/dist/esm/jwt.mjs +4 -2
  18. package/dist/esm/jwt.mjs.map +1 -1
  19. package/dist/esm/schema.mjs +4 -10
  20. package/dist/esm/schema.mjs.map +1 -1
  21. package/dist/esm/search/details.mjs +0 -1
  22. package/dist/esm/search/details.mjs.map +1 -1
  23. package/dist/esm/search/match.mjs +1 -0
  24. package/dist/esm/search/match.mjs.map +1 -1
  25. package/dist/esm/search/search.mjs +2 -2
  26. package/dist/esm/search/search.mjs.map +1 -1
  27. package/dist/esm/sftp.mjs +0 -1
  28. package/dist/esm/sftp.mjs.map +1 -1
  29. package/dist/esm/storage.mjs +8 -0
  30. package/dist/esm/storage.mjs.map +1 -1
  31. package/dist/esm/types.mjs +1 -0
  32. package/dist/esm/types.mjs.map +1 -1
  33. package/dist/esm/utils.mjs +8 -7
  34. package/dist/esm/utils.mjs.map +1 -1
  35. package/dist/types/client.d.ts +89 -65
  36. package/dist/types/config.d.ts +7 -3
  37. package/dist/types/crypto.d.ts +3 -1
  38. package/dist/types/hl7.d.ts +1 -1
  39. package/dist/types/index.d.ts +1 -0
  40. package/dist/types/jwt.d.ts +2 -1
  41. package/dist/types/schema.d.ts +4 -10
  42. package/dist/types/search/details.d.ts +0 -1
  43. package/dist/types/search/search.d.ts +1 -1
  44. package/dist/types/storage.d.ts +8 -0
  45. package/dist/types/typeschema/types.d.ts +55 -0
  46. package/dist/types/utils.d.ts +4 -4
  47. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { encodeBase64 } from './base64.mjs';
1
2
  import { LRUCache } from './cache.mjs';
2
3
  import { getRandomString, encryptSHA256 } from './crypto.mjs';
3
4
  import { EventTarget } from './eventtarget.mjs';
@@ -7,11 +8,10 @@ import { ReadablePromise } from './readablepromise.mjs';
7
8
  import { ClientStorage } from './storage.mjs';
8
9
  import { globalSchema, indexStructureDefinition, indexSearchParameter } from './types.mjs';
9
10
  import { createReference, arrayBufferToBase64 } from './utils.mjs';
10
- import { encodeBase64 } from './base64.mjs';
11
11
 
12
12
  // PKCE auth based on:
13
13
  // https://aws.amazon.com/blogs/security/how-to-add-authentication-single-page-web-application-with-amazon-cognito-oauth2-implementation/
14
- const MEDPLUM_VERSION = "2.0.20-effeb76c" ;
14
+ const MEDPLUM_VERSION = "2.0.22-f51ac45a" ;
15
15
  const DEFAULT_BASE_URL = 'https://api.medplum.com/';
16
16
  const DEFAULT_RESOURCE_CACHE_SIZE = 1000;
17
17
  const DEFAULT_CACHE_TIME = 60000; // 60 seconds
@@ -98,7 +98,6 @@ var OAuthTokenType;
98
98
  * <head>
99
99
  * <meta name="algolia:pageRank" content="100" />
100
100
  * </head>
101
-
102
101
  */
103
102
  class MedplumClient extends EventTarget {
104
103
  constructor(options) {
@@ -165,6 +164,9 @@ class MedplumClient extends EventTarget {
165
164
  * @category Authentication
166
165
  */
167
166
  clearActiveLogin() {
167
+ if (this.basicAuth) {
168
+ return;
169
+ }
168
170
  this.storage.setString('activeLogin', undefined);
169
171
  this.requestCache?.clear();
170
172
  this.accessToken = undefined;
@@ -210,7 +212,6 @@ class MedplumClient extends EventTarget {
210
212
  * This is a lower level method for custom requests.
211
213
  * For common operations, we recommend using higher level methods
212
214
  * such as `readResource()`, `search()`, etc.
213
- *
214
215
  * @category HTTP
215
216
  * @param url The target URL.
216
217
  * @param options Optional fetch options.
@@ -250,7 +251,6 @@ class MedplumClient extends EventTarget {
250
251
  * This is a lower level method for custom requests.
251
252
  * For common operations, we recommend using higher level methods
252
253
  * such as `createResource()`.
253
- *
254
254
  * @category HTTP
255
255
  * @param url The target URL.
256
256
  * @param body The content body. Strings and `File` objects are passed directly. Other objects are converted to JSON.
@@ -275,7 +275,6 @@ class MedplumClient extends EventTarget {
275
275
  * This is a lower level method for custom requests.
276
276
  * For common operations, we recommend using higher level methods
277
277
  * such as `updateResource()`.
278
- *
279
278
  * @category HTTP
280
279
  * @param url The target URL.
281
280
  * @param body The content body. Strings and `File` objects are passed directly. Other objects are converted to JSON.
@@ -300,7 +299,6 @@ class MedplumClient extends EventTarget {
300
299
  * This is a lower level method for custom requests.
301
300
  * For common operations, we recommend using higher level methods
302
301
  * such as `patchResource()`.
303
- *
304
302
  * @category HTTP
305
303
  * @param url The target URL.
306
304
  * @param operations Array of JSONPatch operations.
@@ -321,13 +319,12 @@ class MedplumClient extends EventTarget {
321
319
  * This is a lower level method for custom requests.
322
320
  * For common operations, we recommend using higher level methods
323
321
  * such as `deleteResource()`.
324
- *
325
322
  * @category HTTP
326
323
  * @param url The target URL.
327
324
  * @param options Optional fetch options.
328
325
  * @returns Promise to the response content.
329
326
  */
330
- delete(url, options = {}) {
327
+ delete(url, options) {
331
328
  url = url.toString();
332
329
  this.invalidateUrl(url);
333
330
  return this.request('DELETE', url, options);
@@ -338,53 +335,54 @@ class MedplumClient extends EventTarget {
338
335
  * This method is part of the two different user registration flows:
339
336
  * 1) New Practitioner and new Project
340
337
  * 2) New Patient registration
341
- *
342
338
  * @category Authentication
343
339
  * @param newUserRequest Register request including email and password.
340
+ * @param options Optional fetch options.
344
341
  * @returns Promise to the authentication response.
345
342
  */
346
- async startNewUser(newUserRequest) {
343
+ async startNewUser(newUserRequest, options) {
347
344
  const { codeChallengeMethod, codeChallenge } = await this.startPkce();
348
345
  return this.post('auth/newuser', {
349
346
  ...newUserRequest,
350
347
  codeChallengeMethod,
351
348
  codeChallenge,
352
- });
349
+ }, undefined, options);
353
350
  }
354
351
  /**
355
352
  * Initiates a new project flow.
356
353
  *
357
354
  * This requires a partial login from `startNewUser` or `startNewGoogleUser`.
358
- *
359
355
  * @param newProjectRequest Register request including email and password.
356
+ * @param options Optional fetch options.
360
357
  * @returns Promise to the authentication response.
361
358
  */
362
- async startNewProject(newProjectRequest) {
363
- return this.post('auth/newproject', newProjectRequest);
359
+ async startNewProject(newProjectRequest, options) {
360
+ return this.post('auth/newproject', newProjectRequest, undefined, options);
364
361
  }
365
362
  /**
366
363
  * Initiates a new patient flow.
367
364
  *
368
365
  * This requires a partial login from `startNewUser` or `startNewGoogleUser`.
369
- *
370
366
  * @param newPatientRequest Register request including email and password.
367
+ * @param options Optional fetch options.
371
368
  * @returns Promise to the authentication response.
372
369
  */
373
- async startNewPatient(newPatientRequest) {
374
- return this.post('auth/newpatient', newPatientRequest);
370
+ async startNewPatient(newPatientRequest, options) {
371
+ return this.post('auth/newpatient', newPatientRequest, undefined, options);
375
372
  }
376
373
  /**
377
374
  * Initiates a user login flow.
378
375
  * @category Authentication
379
376
  * @param loginRequest Login request including email and password.
377
+ * @param options Optional fetch options.
380
378
  * @returns Promise to the authentication response.
381
379
  */
382
- async startLogin(loginRequest) {
380
+ async startLogin(loginRequest, options) {
383
381
  return this.post('auth/login', {
384
382
  ...(await this.ensureCodeChallenge(loginRequest)),
385
383
  clientId: loginRequest.clientId ?? this.clientId,
386
384
  scope: loginRequest.scope,
387
- });
385
+ }, undefined, options);
388
386
  }
389
387
  /**
390
388
  * Tries to sign in with Google authentication.
@@ -392,14 +390,15 @@ class MedplumClient extends EventTarget {
392
390
  * See: https://developers.google.com/identity/gsi/web/guides/handle-credential-responses-js-functions
393
391
  * @category Authentication
394
392
  * @param loginRequest Login request including Google credential response.
393
+ * @param options Optional fetch options.
395
394
  * @returns Promise to the authentication response.
396
395
  */
397
- async startGoogleLogin(loginRequest) {
396
+ async startGoogleLogin(loginRequest, options) {
398
397
  return this.post('auth/google', {
399
398
  ...(await this.ensureCodeChallenge(loginRequest)),
400
399
  clientId: loginRequest.clientId ?? this.clientId,
401
400
  scope: loginRequest.scope,
402
- });
401
+ }, undefined, options);
403
402
  }
404
403
  /**
405
404
  * Returns the PKCE code challenge and method.
@@ -430,6 +429,7 @@ class MedplumClient extends EventTarget {
430
429
  * This may result in navigating away to the sign in page.
431
430
  * @category Authentication
432
431
  * @param loginParams Optional login parameters.
432
+ * @returns The user profile resource if available.
433
433
  */
434
434
  async signInWithRedirect(loginParams) {
435
435
  const urlParams = new URLSearchParams(window.location.search);
@@ -466,6 +466,7 @@ class MedplumClient extends EventTarget {
466
466
  * Exchange an external access token for a Medplum access token.
467
467
  * @param token The access token that was generated by the external identity provider.
468
468
  * @param clientId The ID of the `ClientApplication` in your Medplum project that will be making the exchange request.
469
+ * @returns The user profile resource.
469
470
  * @category Authentication
470
471
  */
471
472
  async exchangeExternalAccessToken(token, clientId) {
@@ -564,14 +565,13 @@ class MedplumClient extends EventTarget {
564
565
  * ```
565
566
  *
566
567
  * See FHIR search for full details: https://www.hl7.org/fhir/search.html
567
- *
568
568
  * @category Search
569
569
  * @param resourceType The FHIR resource type.
570
570
  * @param query Optional FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
571
571
  * @param options Optional fetch options.
572
572
  * @returns Promise to the search result bundle.
573
573
  */
574
- search(resourceType, query, options = {}) {
574
+ search(resourceType, query, options) {
575
575
  const url = this.fhirSearchUrl(resourceType, query);
576
576
  const cacheKey = url.toString() + '-search';
577
577
  const cached = this.getCacheEntry(cacheKey, options);
@@ -605,14 +605,13 @@ class MedplumClient extends EventTarget {
605
605
  * The return value is the resource, if available; otherwise, undefined.
606
606
  *
607
607
  * See FHIR search for full details: https://www.hl7.org/fhir/search.html
608
- *
609
608
  * @category Search
610
609
  * @param resourceType The FHIR resource type.
611
610
  * @param query Optional FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
612
611
  * @param options Optional fetch options.
613
612
  * @returns Promise to the first search result.
614
613
  */
615
- searchOne(resourceType, query, options = {}) {
614
+ searchOne(resourceType, query, options) {
616
615
  const url = this.fhirSearchUrl(resourceType, query);
617
616
  url.searchParams.set('_count', '1');
618
617
  url.searchParams.sort();
@@ -640,14 +639,13 @@ class MedplumClient extends EventTarget {
640
639
  * The return value is an array of resources.
641
640
  *
642
641
  * See FHIR search for full details: https://www.hl7.org/fhir/search.html
643
- *
644
642
  * @category Search
645
643
  * @param resourceType The FHIR resource type.
646
644
  * @param query Optional FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
647
645
  * @param options Optional fetch options.
648
646
  * @returns Promise to the array of search results.
649
647
  */
650
- searchResources(resourceType, query, options = {}) {
648
+ searchResources(resourceType, query, options) {
651
649
  const url = this.fhirSearchUrl(resourceType, query);
652
650
  const cacheKey = url.toString() + '-searchResources';
653
651
  const cached = this.getCacheEntry(cacheKey, options);
@@ -672,14 +670,13 @@ class MedplumClient extends EventTarget {
672
670
  * }
673
671
  * }
674
672
  * ```
675
- *
676
673
  * @category Search
677
674
  * @param resourceType The FHIR resource type.
678
675
  * @param query Optional FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
679
676
  * @param options Optional fetch options.
680
- * @returns An async generator, where each result is an array of resources for each page.
677
+ * @yields An async generator, where each result is an array of resources for each page.
681
678
  */
682
- async *searchResourcePages(resourceType, query, options = {}) {
679
+ async *searchResourcePages(resourceType, query, options) {
683
680
  let url = this.fhirSearchUrl(resourceType, query);
684
681
  while (url) {
685
682
  const searchParams = new URL(url).searchParams;
@@ -695,14 +692,13 @@ class MedplumClient extends EventTarget {
695
692
  /**
696
693
  * Searches a ValueSet resource using the "expand" operation.
697
694
  * See: https://www.hl7.org/fhir/operation-valueset-expand.html
698
- *
699
695
  * @category Search
700
696
  * @param system The ValueSet system url.
701
697
  * @param filter The search string.
702
698
  * @param options Optional fetch options.
703
699
  * @returns Promise to expanded ValueSet.
704
700
  */
705
- searchValueSet(system, filter, options = {}) {
701
+ searchValueSet(system, filter, options) {
706
702
  const url = this.fhirUrl('ValueSet', '$expand');
707
703
  url.searchParams.set('url', system);
708
704
  url.searchParams.set('filter', filter);
@@ -722,8 +718,7 @@ class MedplumClient extends EventTarget {
722
718
  /**
723
719
  * Returns a cached resource if it is available.
724
720
  * @category Caching
725
- * @param resourceType The FHIR resource type.
726
- * @param id The FHIR resource ID.
721
+ * @param reference The FHIR reference.
727
722
  * @returns The resource if it is available in the cache; undefined otherwise.
728
723
  */
729
724
  getCachedReference(reference) {
@@ -751,14 +746,13 @@ class MedplumClient extends EventTarget {
751
746
  * ```
752
747
  *
753
748
  * See the FHIR "read" operation for full details: https://www.hl7.org/fhir/http.html#read
754
- *
755
749
  * @category Read
756
750
  * @param resourceType The FHIR resource type.
757
751
  * @param id The resource ID.
758
752
  * @param options Optional fetch options.
759
753
  * @returns The resource if available; undefined otherwise.
760
754
  */
761
- readResource(resourceType, id, options = {}) {
755
+ readResource(resourceType, id, options) {
762
756
  return this.get(this.fhirUrl(resourceType, id), options);
763
757
  }
764
758
  /**
@@ -775,13 +769,12 @@ class MedplumClient extends EventTarget {
775
769
  * ```
776
770
  *
777
771
  * See the FHIR "read" operation for full details: https://www.hl7.org/fhir/http.html#read
778
- *
779
772
  * @category Read
780
773
  * @param reference The FHIR reference object.
781
774
  * @param options Optional fetch options.
782
775
  * @returns The resource if available; undefined otherwise.
783
776
  */
784
- readReference(reference, options = {}) {
777
+ readReference(reference, options) {
785
778
  const refString = reference?.reference;
786
779
  if (!refString) {
787
780
  return new ReadablePromise(Promise.reject(new Error('Missing reference')));
@@ -877,14 +870,13 @@ class MedplumClient extends EventTarget {
877
870
  * ```
878
871
  *
879
872
  * See the FHIR "history" operation for full details: https://www.hl7.org/fhir/http.html#history
880
- *
881
873
  * @category Read
882
874
  * @param resourceType The FHIR resource type.
883
875
  * @param id The resource ID.
884
876
  * @param options Optional fetch options.
885
877
  * @returns Promise to the resource history.
886
878
  */
887
- readHistory(resourceType, id, options = {}) {
879
+ readHistory(resourceType, id, options) {
888
880
  return this.get(this.fhirUrl(resourceType, id, '_history'), options);
889
881
  }
890
882
  /**
@@ -898,7 +890,6 @@ class MedplumClient extends EventTarget {
898
890
  * ```
899
891
  *
900
892
  * See the FHIR "vread" operation for full details: https://www.hl7.org/fhir/http.html#vread
901
- *
902
893
  * @category Read
903
894
  * @param resourceType The FHIR resource type.
904
895
  * @param id The resource ID.
@@ -906,7 +897,7 @@ class MedplumClient extends EventTarget {
906
897
  * @param options Optional fetch options.
907
898
  * @returns The resource if available; undefined otherwise.
908
899
  */
909
- readVersion(resourceType, id, vid, options = {}) {
900
+ readVersion(resourceType, id, vid, options) {
910
901
  return this.get(this.fhirUrl(resourceType, id, '_history', vid), options);
911
902
  }
912
903
  /**
@@ -920,13 +911,12 @@ class MedplumClient extends EventTarget {
920
911
  * ```
921
912
  *
922
913
  * See the FHIR "patient-everything" operation for full details: https://hl7.org/fhir/operation-patient-everything.html
923
- *
924
914
  * @category Read
925
915
  * @param id The Patient Id
926
916
  * @param options Optional fetch options.
927
917
  * @returns A Bundle of all Resources related to the Patient
928
918
  */
929
- readPatientEverything(id, options = {}) {
919
+ readPatientEverything(id, options) {
930
920
  return this.get(this.fhirUrl('Patient', id, '$everything'), options);
931
921
  }
932
922
  /**
@@ -948,17 +938,17 @@ class MedplumClient extends EventTarget {
948
938
  * ```
949
939
  *
950
940
  * See the FHIR "create" operation for full details: https://www.hl7.org/fhir/http.html#create
951
- *
952
941
  * @category Create
953
942
  * @param resource The FHIR resource to create.
943
+ * @param options Optional fetch options.
954
944
  * @returns The result of the create operation.
955
945
  */
956
- createResource(resource) {
946
+ createResource(resource, options) {
957
947
  if (!resource.resourceType) {
958
948
  throw new Error('Missing resourceType');
959
949
  }
960
950
  this.invalidateSearches(resource.resourceType);
961
- return this.post(this.fhirUrl(resource.resourceType), resource);
951
+ return this.post(this.fhirUrl(resource.resourceType), resource, undefined, options);
962
952
  }
963
953
  /**
964
954
  * Conditionally create a new FHIR resource only if some equivalent resource does not already exist on the server.
@@ -994,14 +984,15 @@ class MedplumClient extends EventTarget {
994
984
  * The query parameter only contains the search parameters (what would be in the URL following the "?").
995
985
  *
996
986
  * See the FHIR "conditional create" operation for full details: https://www.hl7.org/fhir/http.html#ccreate
997
- *
998
987
  * @category Create
999
988
  * @param resource The FHIR resource to create.
1000
989
  * @param query The search query for an equivalent resource (should not include resource type or "?").
990
+ * @param options Optional fetch options.
1001
991
  * @returns The result of the create operation.
1002
992
  */
1003
- async createResourceIfNoneExist(resource, query) {
1004
- return ((await this.searchOne(resource.resourceType, query)) ?? this.createResource(resource));
993
+ async createResourceIfNoneExist(resource, query, options) {
994
+ return ((await this.searchOne(resource.resourceType, query, options)) ??
995
+ this.createResource(resource, options));
1005
996
  }
1006
997
  /**
1007
998
  * Creates a FHIR `Binary` resource with the provided data content.
@@ -1020,11 +1011,11 @@ class MedplumClient extends EventTarget {
1020
1011
  * ```
1021
1012
  *
1022
1013
  * See the FHIR "create" operation for full details: https://www.hl7.org/fhir/http.html#create
1023
- *
1024
1014
  * @category Create
1025
1015
  * @param data The binary data to upload.
1026
1016
  * @param filename Optional filename for the binary.
1027
1017
  * @param contentType Content type for the binary.
1018
+ * @param onProgress Optional callback for progress events.
1028
1019
  * @returns The result of the create operation.
1029
1020
  */
1030
1021
  createBinary(data, filename, contentType, onProgress) {
@@ -1083,9 +1074,11 @@ class MedplumClient extends EventTarget {
1083
1074
  * ```
1084
1075
  *
1085
1076
  * See the pdfmake document definition for full details: https://pdfmake.github.io/docs/0.1/document-definition-object/
1086
- *
1087
1077
  * @category Media
1088
1078
  * @param docDefinition The PDF document definition.
1079
+ * @param filename Optional filename for the PDF binary resource.
1080
+ * @param tableLayouts Optional pdfmake custom table layout.
1081
+ * @param fonts Optional pdfmake custom font dictionary.
1089
1082
  * @returns The result of the create operation.
1090
1083
  */
1091
1084
  async createPdf(docDefinition, filename, tableLayouts, fonts) {
@@ -1099,13 +1092,13 @@ class MedplumClient extends EventTarget {
1099
1092
  * Creates a FHIR `Communication` resource with the provided data content.
1100
1093
  *
1101
1094
  * This is a convenience method to handle commmon cases where a `Communication` resource is created with a `payload`.
1102
- *
1103
1095
  * @category Create
1104
1096
  * @param resource The FHIR resource to comment on.
1105
1097
  * @param text The text of the comment.
1098
+ * @param options Optional fetch options.
1106
1099
  * @returns The result of the create operation.
1107
1100
  */
1108
- createComment(resource, text) {
1101
+ createComment(resource, text, options) {
1109
1102
  const profile = this.getProfile();
1110
1103
  let encounter = undefined;
1111
1104
  let subject = undefined;
@@ -1128,7 +1121,7 @@ class MedplumClient extends EventTarget {
1128
1121
  sender: profile ? createReference(profile) : undefined,
1129
1122
  sent: new Date().toISOString(),
1130
1123
  payload: [{ contentString: text }],
1131
- });
1124
+ }, options);
1132
1125
  }
1133
1126
  /**
1134
1127
  * Updates a FHIR resource.
@@ -1150,12 +1143,12 @@ class MedplumClient extends EventTarget {
1150
1143
  * ```
1151
1144
  *
1152
1145
  * See the FHIR "update" operation for full details: https://www.hl7.org/fhir/http.html#update
1153
- *
1154
1146
  * @category Write
1155
1147
  * @param resource The FHIR resource to update.
1148
+ * @param options Optional fetch options.
1156
1149
  * @returns The result of the update operation.
1157
1150
  */
1158
- async updateResource(resource) {
1151
+ async updateResource(resource, options) {
1159
1152
  if (!resource.resourceType) {
1160
1153
  throw new Error('Missing resourceType');
1161
1154
  }
@@ -1163,7 +1156,7 @@ class MedplumClient extends EventTarget {
1163
1156
  throw new Error('Missing id');
1164
1157
  }
1165
1158
  this.invalidateSearches(resource.resourceType);
1166
- let result = await this.put(this.fhirUrl(resource.resourceType, resource.id), resource);
1159
+ let result = await this.put(this.fhirUrl(resource.resourceType, resource.id), resource, undefined, options);
1167
1160
  if (!result) {
1168
1161
  // On 304 not modified, result will be undefined
1169
1162
  // Return the user input instead
@@ -1190,16 +1183,16 @@ class MedplumClient extends EventTarget {
1190
1183
  * See the FHIR "update" operation for full details: https://www.hl7.org/fhir/http.html#patch
1191
1184
  *
1192
1185
  * See the JSONPatch specification for full details: https://tools.ietf.org/html/rfc6902
1193
- *
1194
1186
  * @category Write
1195
1187
  * @param resourceType The FHIR resource type.
1196
1188
  * @param id The resource ID.
1197
1189
  * @param operations The JSONPatch operations.
1190
+ * @param options Optional fetch options.
1198
1191
  * @returns The result of the patch operations.
1199
1192
  */
1200
- patchResource(resourceType, id, operations) {
1193
+ patchResource(resourceType, id, operations, options) {
1201
1194
  this.invalidateSearches(resourceType);
1202
- return this.patch(this.fhirUrl(resourceType, id), operations);
1195
+ return this.patch(this.fhirUrl(resourceType, id), operations, options);
1203
1196
  }
1204
1197
  /**
1205
1198
  * Deletes a FHIR resource by resource type and ID.
@@ -1211,16 +1204,16 @@ class MedplumClient extends EventTarget {
1211
1204
  * ```
1212
1205
  *
1213
1206
  * See the FHIR "delete" operation for full details: https://www.hl7.org/fhir/http.html#delete
1214
- *
1215
1207
  * @category Delete
1216
1208
  * @param resourceType The FHIR resource type.
1217
1209
  * @param id The resource ID.
1210
+ * @param options Optional fetch options.
1218
1211
  * @returns The result of the delete operation.
1219
1212
  */
1220
- deleteResource(resourceType, id) {
1213
+ deleteResource(resourceType, id, options) {
1221
1214
  this.deleteCacheEntry(this.fhirUrl(resourceType, id).toString());
1222
1215
  this.invalidateSearches(resourceType);
1223
- return this.delete(this.fhirUrl(resourceType, id));
1216
+ return this.delete(this.fhirUrl(resourceType, id), options);
1224
1217
  }
1225
1218
  /**
1226
1219
  * Executes the validate operation with the provided resource.
@@ -1235,12 +1228,12 @@ class MedplumClient extends EventTarget {
1235
1228
  * ```
1236
1229
  *
1237
1230
  * See the FHIR "$validate" operation for full details: https://www.hl7.org/fhir/resource-operation-validate.html
1238
- *
1239
1231
  * @param resource The FHIR resource.
1232
+ * @param options Optional fetch options.
1240
1233
  * @returns The validate operation outcome.
1241
1234
  */
1242
- validateResource(resource) {
1243
- return this.post(this.fhirUrl(resource.resourceType, '$validate'), resource);
1235
+ validateResource(resource, options) {
1236
+ return this.post(this.fhirUrl(resource.resourceType, '$validate'), resource, undefined, options);
1244
1237
  }
1245
1238
  /**
1246
1239
  * Executes a bot by ID or Identifier.
@@ -1250,7 +1243,7 @@ class MedplumClient extends EventTarget {
1250
1243
  * @param options Optional fetch options.
1251
1244
  * @returns The Bot return value.
1252
1245
  */
1253
- executeBot(idOrIdentifier, body, contentType, options = {}) {
1246
+ executeBot(idOrIdentifier, body, contentType, options) {
1254
1247
  let url;
1255
1248
  if (typeof idOrIdentifier === 'string') {
1256
1249
  const id = idOrIdentifier;
@@ -1307,10 +1300,11 @@ class MedplumClient extends EventTarget {
1307
1300
  * See The FHIR "batch/transaction" section for full details: https://hl7.org/fhir/http.html#transaction
1308
1301
  * @category Batch
1309
1302
  * @param bundle The FHIR batch/transaction bundle.
1303
+ * @param options Optional fetch options.
1310
1304
  * @returns The FHIR batch/transaction response bundle.
1311
1305
  */
1312
- executeBatch(bundle) {
1313
- return this.post(this.fhirBaseUrl.slice(0, -1), bundle);
1306
+ executeBatch(bundle, options) {
1307
+ return this.post(this.fhirBaseUrl.slice(0, -1), bundle, undefined, options);
1314
1308
  }
1315
1309
  /**
1316
1310
  * Sends an email using the Medplum Email API.
@@ -1346,11 +1340,12 @@ class MedplumClient extends EventTarget {
1346
1340
  *
1347
1341
  * See options here: https://nodemailer.com/extras/mailcomposer/
1348
1342
  * @category Media
1349
- * @param options The MailComposer options.
1343
+ * @param email The MailComposer options.
1344
+ * @param options Optional fetch options.
1350
1345
  * @returns Promise to the operation outcome.
1351
1346
  */
1352
- sendEmail(email) {
1353
- return this.post('email/v1/send', email, 'application/json');
1347
+ sendEmail(email, options) {
1348
+ return this.post('email/v1/send', email, 'application/json', options);
1354
1349
  }
1355
1350
  /**
1356
1351
  * Executes a GraphQL query.
@@ -1392,7 +1387,6 @@ class MedplumClient extends EventTarget {
1392
1387
  * See the GraphQL documentation for more details: https://graphql.org/learn/
1393
1388
  *
1394
1389
  * See the FHIR GraphQL documentation for FHIR specific details: https://www.hl7.org/fhir/graphql.html
1395
- *
1396
1390
  * @category Read
1397
1391
  * @param query The GraphQL query.
1398
1392
  * @param operationName Optional GraphQL operation name.
@@ -1407,15 +1401,15 @@ class MedplumClient extends EventTarget {
1407
1401
  *
1408
1402
  * Executes the $graph operation on this resource to fetch a Bundle of resources linked to the target resource
1409
1403
  * according to a graph definition
1410
-
1411
1404
  * @category Read
1412
1405
  * @param resourceType The FHIR resource type.
1413
1406
  * @param id The resource ID.
1414
1407
  * @param graphName `name` parameter of the GraphDefinition
1408
+ * @param options Optional fetch options.
1415
1409
  * @returns A Bundle
1416
1410
  */
1417
- readResourceGraph(resourceType, id, graphName) {
1418
- return this.get(`${this.fhirUrl(resourceType, id)}/$graph?graph=${graphName}`);
1411
+ readResourceGraph(resourceType, id, graphName, options) {
1412
+ return this.get(`${this.fhirUrl(resourceType, id)}/$graph?graph=${graphName}`, options);
1419
1413
  }
1420
1414
  /**
1421
1415
  * @category Authentication
@@ -1425,11 +1419,16 @@ class MedplumClient extends EventTarget {
1425
1419
  return this.storage.getObject('activeLogin');
1426
1420
  }
1427
1421
  /**
1422
+ * Sets the active login.
1423
+ * @param login The new active login state.
1428
1424
  * @category Authentication
1429
1425
  */
1430
1426
  async setActiveLogin(login) {
1431
1427
  this.clearActiveLogin();
1432
1428
  this.accessToken = login.accessToken;
1429
+ if (this.basicAuth) {
1430
+ return;
1431
+ }
1433
1432
  this.refreshToken = login.refreshToken;
1434
1433
  this.storage.setObject('activeLogin', login);
1435
1434
  this.addLogin(login);
@@ -1438,6 +1437,7 @@ class MedplumClient extends EventTarget {
1438
1437
  }
1439
1438
  /**
1440
1439
  * Returns the current access token.
1440
+ * @returns The current access token.
1441
1441
  * @category Authentication
1442
1442
  */
1443
1443
  getAccessToken() {
@@ -1445,6 +1445,7 @@ class MedplumClient extends EventTarget {
1445
1445
  }
1446
1446
  /**
1447
1447
  * Sets the current access token.
1448
+ * @param accessToken The new access token.
1448
1449
  * @category Authentication
1449
1450
  */
1450
1451
  setAccessToken(accessToken) {
@@ -1454,6 +1455,8 @@ class MedplumClient extends EventTarget {
1454
1455
  this.config = undefined;
1455
1456
  }
1456
1457
  /**
1458
+ * Returns the list of available logins.
1459
+ * @returns The list of available logins.
1457
1460
  * @category Authentication
1458
1461
  */
1459
1462
  getLogins() {
@@ -1466,6 +1469,9 @@ class MedplumClient extends EventTarget {
1466
1469
  }
1467
1470
  async refreshProfile() {
1468
1471
  this.profilePromise = new Promise((resolve, reject) => {
1472
+ if (this.basicAuth) {
1473
+ return;
1474
+ }
1469
1475
  this.get('auth/me')
1470
1476
  .then((result) => {
1471
1477
  this.profilePromise = undefined;
@@ -1479,18 +1485,26 @@ class MedplumClient extends EventTarget {
1479
1485
  return this.profilePromise;
1480
1486
  }
1481
1487
  /**
1488
+ * Returns true if the client is waiting for authentication.
1489
+ * @returns True if the client is waiting for authentication.
1482
1490
  * @category Authentication
1483
1491
  */
1484
1492
  isLoading() {
1485
1493
  return !!this.profilePromise;
1486
1494
  }
1487
1495
  /**
1496
+ * Returns the current user profile resource if available.
1497
+ * This method does not wait for loading promises.
1498
+ * @returns The current user profile resource if available.
1488
1499
  * @category User Profile
1489
1500
  */
1490
1501
  getProfile() {
1491
1502
  return this.profile;
1492
1503
  }
1493
1504
  /**
1505
+ * Returns the current user profile resource if available.
1506
+ * This method waits for loading promises.
1507
+ * @returns The current user profile resource if available.
1494
1508
  * @category User Profile
1495
1509
  */
1496
1510
  async getProfileAsync() {
@@ -1500,6 +1514,8 @@ class MedplumClient extends EventTarget {
1500
1514
  return this.getProfile();
1501
1515
  }
1502
1516
  /**
1517
+ * Returns the current user configuration if available.
1518
+ * @returns The current user configuration if available.
1503
1519
  * @category User Profile
1504
1520
  */
1505
1521
  getUserConfiguration() {
@@ -1507,9 +1523,9 @@ class MedplumClient extends EventTarget {
1507
1523
  }
1508
1524
  /**
1509
1525
  * Downloads the URL as a blob.
1510
- *
1511
1526
  * @category Read
1512
1527
  * @param url The URL to request.
1528
+ * @param options Optional fetch request init options.
1513
1529
  * @returns Promise to the response body as a blob.
1514
1530
  */
1515
1531
  async download(url, options = {}) {
@@ -1523,12 +1539,13 @@ class MedplumClient extends EventTarget {
1523
1539
  /**
1524
1540
  * Upload media to the server and create a Media instance for the uploaded content.
1525
1541
  * @param contents The contents of the media file, as a string, Uint8Array, File, or Blob.
1526
- * @param contentType The media type of the content
1527
- * @param filename The name of the file to be uploaded, or undefined if not applicable
1528
- * @param additionalFields Additional fields for Media
1542
+ * @param contentType The media type of the content.
1543
+ * @param filename The name of the file to be uploaded, or undefined if not applicable.
1544
+ * @param additionalFields Additional fields for Media.
1545
+ * @param options Optional fetch options.
1529
1546
  * @returns Promise that resolves to the created Media
1530
1547
  */
1531
- async uploadMedia(contents, contentType, filename, additionalFields) {
1548
+ async uploadMedia(contents, contentType, filename, additionalFields, options) {
1532
1549
  const binary = await this.createBinary(contents, filename, contentType);
1533
1550
  return this.createResource({
1534
1551
  ...additionalFields,
@@ -1538,7 +1555,36 @@ class MedplumClient extends EventTarget {
1538
1555
  url: 'Binary/' + binary.id,
1539
1556
  title: filename,
1540
1557
  },
1541
- });
1558
+ }, options);
1559
+ }
1560
+ /**
1561
+ * Performs Bulk Data Export operation request flow. See The FHIR "Bulk Data Export" for full details: https://build.fhir.org/ig/HL7/bulk-data/export.html#bulk-data-export
1562
+ * @param exportLevel Optional export level. Defaults to system level export. 'Group/:id' - Group of Patients, 'Patient' - All Patients.
1563
+ * @param resourceTypes A string of comma-delimited FHIR resource types.
1564
+ * @param since Resources will be included in the response if their state has changed after the supplied time (e.g. if Resource.meta.lastUpdated is later than the supplied _since time).
1565
+ * @param options Optional fetch options.
1566
+ * @returns Bulk Data Response containing links to Bulk Data files. See "Response - Complete Status" for full details: https://build.fhir.org/ig/HL7/bulk-data/export.html#response---complete-status
1567
+ */
1568
+ async bulkExport(exportLevel = '', resourceTypes, since, options = {}) {
1569
+ const fhirPath = exportLevel ? `${exportLevel}/` : exportLevel;
1570
+ const url = this.fhirUrl(`${fhirPath}$export`);
1571
+ if (resourceTypes) {
1572
+ url.searchParams.set('_type', resourceTypes);
1573
+ }
1574
+ if (since) {
1575
+ url.searchParams.set('_since', since);
1576
+ }
1577
+ this.addFetchOptionsDefaults(options);
1578
+ const headers = options.headers;
1579
+ headers['Prefer'] = 'respond-async';
1580
+ const response = await this.fetchWithRetry(url.toString(), options);
1581
+ if (response.status === 202) {
1582
+ const contentLocation = response.headers.get('content-location');
1583
+ if (contentLocation) {
1584
+ return await this.pollStatus(contentLocation);
1585
+ }
1586
+ }
1587
+ return await this.parseResponse(response, 'POST', url.toString());
1542
1588
  }
1543
1589
  //
1544
1590
  // Private helpers
@@ -1591,10 +1637,10 @@ class MedplumClient extends EventTarget {
1591
1637
  }
1592
1638
  /**
1593
1639
  * Makes an HTTP request.
1594
- * @param {string} method
1595
- * @param {string} url
1596
- * @param {string=} contentType
1597
- * @param {Object=} body
1640
+ * @param method The HTTP method (GET, POST, etc).
1641
+ * @param url The target URL.
1642
+ * @param options Optional fetch request init options.
1643
+ * @returns The JSON content body if available.
1598
1644
  */
1599
1645
  async request(method, url, options = {}) {
1600
1646
  if (this.refreshPromise) {
@@ -1606,6 +1652,9 @@ class MedplumClient extends EventTarget {
1606
1652
  options.method = method;
1607
1653
  this.addFetchOptionsDefaults(options);
1608
1654
  const response = await this.fetchWithRetry(url, options);
1655
+ return await this.parseResponse(response, method, url, options);
1656
+ }
1657
+ async parseResponse(response, method, url, options = {}) {
1609
1658
  if (response.status === 401) {
1610
1659
  // Refresh and try again
1611
1660
  return this.handleUnauthenticated(method, url, options);
@@ -1638,14 +1687,35 @@ class MedplumClient extends EventTarget {
1638
1687
  const retryDelay = 200;
1639
1688
  let response = undefined;
1640
1689
  for (let retry = 0; retry < maxRetries; retry++) {
1641
- response = (await this.fetch(url, options));
1642
- if (response.status < 500) {
1643
- return response;
1690
+ try {
1691
+ response = (await this.fetch(url, options));
1692
+ if (response.status < 500) {
1693
+ return response;
1694
+ }
1695
+ }
1696
+ catch (err) {
1697
+ this.retryCatch(retry, maxRetries, err);
1644
1698
  }
1645
1699
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
1646
1700
  }
1647
1701
  return response;
1648
1702
  }
1703
+ async pollStatus(statusUrl) {
1704
+ let checkStatus = true;
1705
+ let resultResponse;
1706
+ const retryDelay = 200;
1707
+ while (checkStatus) {
1708
+ const fetchOptions = {};
1709
+ this.addFetchOptionsDefaults(fetchOptions);
1710
+ const statusResponse = await this.fetchWithRetry(statusUrl, fetchOptions);
1711
+ if (statusResponse.status !== 202) {
1712
+ checkStatus = false;
1713
+ resultResponse = statusResponse;
1714
+ }
1715
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
1716
+ }
1717
+ return await this.parseResponse(resultResponse, 'POST', statusUrl);
1718
+ }
1649
1719
  /**
1650
1720
  * Executes a batch of requests that were automatically batched together.
1651
1721
  */
@@ -1703,6 +1773,7 @@ class MedplumClient extends EventTarget {
1703
1773
  headers = {};
1704
1774
  options.headers = headers;
1705
1775
  }
1776
+ headers['Accept'] = FHIR_CONTENT_TYPE;
1706
1777
  headers['X-Medplum'] = 'extended';
1707
1778
  if (options.body && !headers['Content-Type']) {
1708
1779
  headers['Content-Type'] = FHIR_CONTENT_TYPE;
@@ -1710,7 +1781,7 @@ class MedplumClient extends EventTarget {
1710
1781
  if (this.accessToken) {
1711
1782
  headers['Authorization'] = 'Bearer ' + this.accessToken;
1712
1783
  }
1713
- if (this.basicAuth) {
1784
+ else if (this.basicAuth) {
1714
1785
  headers['Authorization'] = 'Basic ' + this.basicAuth;
1715
1786
  }
1716
1787
  if (!options.cache) {
@@ -1754,8 +1825,8 @@ class MedplumClient extends EventTarget {
1754
1825
  * Otherwise, calls unauthenticated callbacks and rejects.
1755
1826
  * @param method The HTTP method of the original request.
1756
1827
  * @param url The URL of the original request.
1757
- * @param contentType The content type of the original request.
1758
- * @param body The body of the original request.
1828
+ * @param options Optional fetch request init options.
1829
+ * @returns The result of the retry.
1759
1830
  */
1760
1831
  handleUnauthenticated(method, url, options) {
1761
1832
  if (this.refresh()) {
@@ -1771,6 +1842,7 @@ class MedplumClient extends EventTarget {
1771
1842
  * Starts a new PKCE flow.
1772
1843
  * These PKCE values are stateful, and must survive redirects and page refreshes.
1773
1844
  * @category Authentication
1845
+ * @returns The PKCE code challenge details.
1774
1846
  */
1775
1847
  async startPkce() {
1776
1848
  const pkceState = getRandomString();
@@ -1785,7 +1857,8 @@ class MedplumClient extends EventTarget {
1785
1857
  /**
1786
1858
  * Redirects the user to the login screen for authorization.
1787
1859
  * Clears all auth state including local storage and session storage.
1788
- * See: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
1860
+ * @param loginParams The authorization login parameters.
1861
+ * @see https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
1789
1862
  */
1790
1863
  async requestAuthorization(loginParams) {
1791
1864
  const loginRequest = await this.ensureCodeChallenge(loginParams || {});
@@ -1804,6 +1877,7 @@ class MedplumClient extends EventTarget {
1804
1877
  * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
1805
1878
  * @param code The authorization code received by URL parameter.
1806
1879
  * @param loginParams Optional login parameters.
1880
+ * @returns The user profile resource.
1807
1881
  * @category Authentication
1808
1882
  */
1809
1883
  processCode(code, loginParams) {
@@ -1822,7 +1896,8 @@ class MedplumClient extends EventTarget {
1822
1896
  }
1823
1897
  /**
1824
1898
  * Tries to refresh the auth tokens.
1825
- * See: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
1899
+ * @returns The refresh promise if available; otherwise undefined.
1900
+ * @see https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
1826
1901
  */
1827
1902
  refresh() {
1828
1903
  if (this.refreshPromise) {
@@ -1875,7 +1950,6 @@ class MedplumClient extends EventTarget {
1875
1950
  * // Example Search
1876
1951
  * await medplum.searchResources('Patient')
1877
1952
  * ```
1878
- *
1879
1953
  * @category Authentication
1880
1954
  * @param clientId The client ID.
1881
1955
  * @param clientSecret The client secret.
@@ -1898,14 +1972,20 @@ class MedplumClient extends EventTarget {
1898
1972
  * Makes a POST request to the tokens endpoint.
1899
1973
  * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
1900
1974
  * @param formBody Token parameters in URL encoded format.
1975
+ * @returns The user profile resource.
1901
1976
  */
1902
1977
  async fetchTokens(formBody) {
1903
- const response = await this.fetch(this.tokenUrl, {
1978
+ const options = {
1904
1979
  method: 'POST',
1905
1980
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1906
1981
  body: formBody,
1907
1982
  credentials: 'include',
1908
- });
1983
+ };
1984
+ const headers = options.headers;
1985
+ if (this.basicAuth) {
1986
+ headers['Authorization'] = `Basic ${this.basicAuth}`;
1987
+ }
1988
+ const response = await this.fetch(this.tokenUrl, options);
1909
1989
  if (!response.ok) {
1910
1990
  this.clearActiveLogin();
1911
1991
  throw new Error('Failed to fetch tokens');
@@ -1918,7 +1998,8 @@ class MedplumClient extends EventTarget {
1918
1998
  * Verifies the tokens received from the auth server.
1919
1999
  * Validates the JWT against the JWKS.
1920
2000
  * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
1921
- * @param tokens
2001
+ * @param tokens The token response.
2002
+ * @returns Promise to complete.
1922
2003
  */
1923
2004
  async verifyTokens(tokens) {
1924
2005
  const token = tokens.access_token;
@@ -1929,7 +2010,14 @@ class MedplumClient extends EventTarget {
1929
2010
  throw new Error('Token expired');
1930
2011
  }
1931
2012
  // Verify app_client_id
1932
- if (this.clientId && tokenPayload.client_id !== this.clientId) {
2013
+ // external tokenPayload
2014
+ if (tokenPayload.cid) {
2015
+ if (tokenPayload.cid !== this.clientId) {
2016
+ this.clearActiveLogin();
2017
+ throw new Error('Token was not issued for this audience');
2018
+ }
2019
+ }
2020
+ else if (this.clientId && tokenPayload.client_id !== this.clientId) {
1933
2021
  this.clearActiveLogin();
1934
2022
  throw new Error('Token was not issued for this audience');
1935
2023
  }
@@ -1959,6 +2047,15 @@ class MedplumClient extends EventTarget {
1959
2047
  // Silently ignore if this environment does not support storage events
1960
2048
  }
1961
2049
  }
2050
+ retryCatch(retryNumber, maxRetries, err) {
2051
+ // This is for the 1st retry to avoid multiple notifications
2052
+ if (err.message === 'Failed to fetch' && retryNumber === 1) {
2053
+ this.dispatchEvent({ type: 'offline' });
2054
+ }
2055
+ if (retryNumber >= maxRetries - 1) {
2056
+ throw err;
2057
+ }
2058
+ }
1962
2059
  }
1963
2060
  /**
1964
2061
  * Returns the default fetch method.
@@ -1974,6 +2071,7 @@ function getDefaultFetch() {
1974
2071
  }
1975
2072
  /**
1976
2073
  * Returns the base URL for the current page.
2074
+ * @returns The window origin string.
1977
2075
  * @category HTTP
1978
2076
  */
1979
2077
  function getWindowOrigin() {