@medplum/core 2.0.19 → 2.0.21

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 CHANGED
@@ -39,7 +39,7 @@ const medplum = new MedplumClient({
39
39
  });
40
40
  ```
41
41
 
42
- ## Authenticate with client credenials
42
+ ## Authenticate with client credentials
43
43
 
44
44
  ```ts
45
45
  const medplum = new MedplumClient();
@@ -6417,7 +6417,7 @@
6417
6417
 
6418
6418
  // PKCE auth based on:
6419
6419
  // https://aws.amazon.com/blogs/security/how-to-add-authentication-single-page-web-application-with-amazon-cognito-oauth2-implementation/
6420
- const MEDPLUM_VERSION = "2.0.19-40e6e27d" ;
6420
+ const MEDPLUM_VERSION = "2.0.21-87271fa1" ;
6421
6421
  const DEFAULT_BASE_URL = 'https://api.medplum.com/';
6422
6422
  const DEFAULT_RESOURCE_CACHE_SIZE = 1000;
6423
6423
  const DEFAULT_CACHE_TIME = 60000; // 60 seconds
@@ -6588,6 +6588,13 @@
6588
6588
  url = url.toString();
6589
6589
  this.requestCache?.delete(url);
6590
6590
  }
6591
+ /**
6592
+ * Invalidates all cached values and flushes the cache.
6593
+ * @category Caching
6594
+ */
6595
+ invalidateAll() {
6596
+ this.requestCache?.clear();
6597
+ }
6591
6598
  /**
6592
6599
  * Invalidates all cached search results or cached requests for the given resourceType.
6593
6600
  * @category Caching
@@ -7706,10 +7713,11 @@
7706
7713
  * See The FHIR "batch/transaction" section for full details: https://hl7.org/fhir/http.html#transaction
7707
7714
  * @category Batch
7708
7715
  * @param bundle The FHIR batch/transaction bundle.
7716
+ * @param options Optional fetch options.
7709
7717
  * @returns The FHIR batch/transaction response bundle.
7710
7718
  */
7711
- executeBatch(bundle) {
7712
- return this.post(this.fhirBaseUrl.slice(0, -1), bundle);
7719
+ executeBatch(bundle, options = {}) {
7720
+ return this.post(this.fhirBaseUrl.slice(0, -1), bundle, undefined, options);
7713
7721
  }
7714
7722
  /**
7715
7723
  * Sends an email using the Medplum Email API.
@@ -7939,6 +7947,37 @@
7939
7947
  },
7940
7948
  });
7941
7949
  }
7950
+ /**
7951
+ * 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
7952
+ *
7953
+ * @param exportLevel Optional export level. Defaults to system level export. 'Group/:id' - Group of Patients, 'Patient' - All Patients.
7954
+ * @param resourceTypes A string of comma-delimited FHIR resource types.
7955
+ * @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).
7956
+ * @param options Optional fetch options.
7957
+ * @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
7958
+ */
7959
+ async bulkExport(exportLevel = '', resourceTypes, since, options = {}) {
7960
+ const fhirPath = exportLevel ? `${exportLevel}/` : exportLevel;
7961
+ const url = this.fhirUrl(`${fhirPath}$export`);
7962
+ if (resourceTypes) {
7963
+ url.searchParams.set('_type', resourceTypes);
7964
+ }
7965
+ if (since) {
7966
+ url.searchParams.set('_since', since);
7967
+ }
7968
+ options.method = exportLevel ? 'GET' : 'POST';
7969
+ this.addFetchOptionsDefaults(options);
7970
+ const headers = options.headers;
7971
+ headers['Prefer'] = 'respond-async';
7972
+ const response = await this.fetchWithRetry(url.toString(), options);
7973
+ if (response.status === 202) {
7974
+ const contentLocation = response.headers.get('content-location');
7975
+ if (contentLocation) {
7976
+ return await this.pollStatus(contentLocation);
7977
+ }
7978
+ }
7979
+ return await this.parseResponse(response, 'POST', url.toString());
7980
+ }
7942
7981
  //
7943
7982
  // Private helpers
7944
7983
  //
@@ -8005,6 +8044,9 @@
8005
8044
  options.method = method;
8006
8045
  this.addFetchOptionsDefaults(options);
8007
8046
  const response = await this.fetchWithRetry(url, options);
8047
+ return await this.parseResponse(response, method, url, options);
8048
+ }
8049
+ async parseResponse(response, method, url, options = {}) {
8008
8050
  if (response.status === 401) {
8009
8051
  // Refresh and try again
8010
8052
  return this.handleUnauthenticated(method, url, options);
@@ -8037,14 +8079,37 @@
8037
8079
  const retryDelay = 200;
8038
8080
  let response = undefined;
8039
8081
  for (let retry = 0; retry < maxRetries; retry++) {
8040
- response = (await this.fetch(url, options));
8041
- if (response.status < 500) {
8042
- return response;
8082
+ try {
8083
+ response = (await this.fetch(url, options));
8084
+ if (response.status < 500) {
8085
+ return response;
8086
+ }
8087
+ }
8088
+ catch (err) {
8089
+ this.retryCatch(retry, maxRetries, err);
8043
8090
  }
8044
8091
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
8045
8092
  }
8046
8093
  return response;
8047
8094
  }
8095
+ async pollStatus(statusUrl) {
8096
+ let checkStatus = true;
8097
+ let resultResponse;
8098
+ const retryDelay = 200;
8099
+ while (checkStatus) {
8100
+ const fetchOptions = {
8101
+ method: 'GET',
8102
+ };
8103
+ this.addFetchOptionsDefaults(fetchOptions);
8104
+ const statusResponse = await this.fetchWithRetry(statusUrl, fetchOptions);
8105
+ if (statusResponse.status !== 202) {
8106
+ checkStatus = false;
8107
+ resultResponse = statusResponse;
8108
+ }
8109
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
8110
+ }
8111
+ return await this.parseResponse(resultResponse, 'POST', statusUrl);
8112
+ }
8048
8113
  /**
8049
8114
  * Executes a batch of requests that were automatically batched together.
8050
8115
  */
@@ -8102,6 +8167,7 @@
8102
8167
  headers = {};
8103
8168
  options.headers = headers;
8104
8169
  }
8170
+ headers['Accept'] = FHIR_CONTENT_TYPE;
8105
8171
  headers['X-Medplum'] = 'extended';
8106
8172
  if (options.body && !headers['Content-Type']) {
8107
8173
  headers['Content-Type'] = FHIR_CONTENT_TYPE;
@@ -8358,6 +8424,15 @@
8358
8424
  // Silently ignore if this environment does not support storage events
8359
8425
  }
8360
8426
  }
8427
+ retryCatch(retryNumber, maxRetries, err) {
8428
+ // This is for the 1st retry to avoid multiple notifications
8429
+ if (err.message === 'Failed to fetch' && retryNumber === 1) {
8430
+ this.dispatchEvent({ type: 'offline' });
8431
+ }
8432
+ if (retryNumber >= maxRetries - 1) {
8433
+ throw err;
8434
+ }
8435
+ }
8361
8436
  }
8362
8437
  /**
8363
8438
  * Returns the default fetch method.
@@ -11575,7 +11650,7 @@
11575
11650
  searchRequest.total = value;
11576
11651
  break;
11577
11652
  case '_summary':
11578
- searchRequest.total = 'estimate';
11653
+ searchRequest.total = 'accurate';
11579
11654
  searchRequest.count = 0;
11580
11655
  break;
11581
11656
  case '_include': {
@@ -12628,26 +12703,28 @@
12628
12703
  // In the future, explore returning multiple column definitions
12629
12704
  return { columnName, type: exports.SearchParameterType.TEXT };
12630
12705
  }
12631
- const defaultType = getSearchParameterType(searchParam);
12632
12706
  let baseType = resourceType;
12633
12707
  let elementDefinition = undefined;
12634
12708
  let propertyType = undefined;
12635
12709
  let array = false;
12636
12710
  for (let i = 1; i < expression.length; i++) {
12637
- const propertyName = expression[i];
12711
+ let propertyName = expression[i];
12712
+ let hasArrayIndex = false;
12713
+ const arrayIndexMatch = /\[\d+\]$/.exec(propertyName);
12714
+ if (arrayIndexMatch) {
12715
+ propertyName = propertyName.substring(0, propertyName.length - arrayIndexMatch[0].length);
12716
+ hasArrayIndex = true;
12717
+ }
12638
12718
  elementDefinition = getElementDefinition(baseType, propertyName);
12639
12719
  if (!elementDefinition) {
12640
12720
  throw new Error(`Element definition not found for ${resourceType} ${searchParam.code}`);
12641
12721
  }
12642
- if (elementDefinition.max !== '0' && elementDefinition.max !== '1') {
12722
+ if (elementDefinition.max !== '0' && elementDefinition.max !== '1' && !hasArrayIndex) {
12643
12723
  array = true;
12644
12724
  }
12725
+ // "code" is only missing when using "contentReference"
12726
+ // "contentReference" is handled above in "getElementDefinition"
12645
12727
  propertyType = elementDefinition.type?.[0].code;
12646
- if (!propertyType) {
12647
- // This happens when one of parent properties uses contentReference
12648
- // In the future, explore following the reference
12649
- return { columnName, type: defaultType, array };
12650
- }
12651
12728
  if (i < expression.length - 1) {
12652
12729
  if (isBackboneElement(propertyType)) {
12653
12730
  baseType = buildTypeName(elementDefinition.path?.split('.'));
@@ -12727,9 +12804,6 @@
12727
12804
  if (result.startsWith('(') && result.endsWith(')')) {
12728
12805
  result = result.substring(1, result.length - 1);
12729
12806
  }
12730
- if (result.includes('[0]')) {
12731
- result = result.replaceAll('[0]', '');
12732
- }
12733
12807
  const stopStrings = [' != ', ' as ', '.as(', '.exists(', '.resolve(', '.where('];
12734
12808
  for (const stopString of stopStrings) {
12735
12809
  if (result.includes(stopString)) {
@@ -12904,7 +12978,6 @@
12904
12978
  return new Promise((resolve, reject) => {
12905
12979
  stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
12906
12980
  stream.on('error', (err) => {
12907
- console.error(err.message);
12908
12981
  stream.destroy();
12909
12982
  reject(err);
12910
12983
  });