@medplum/core 2.0.15 → 2.0.17

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.
@@ -322,8 +322,9 @@
322
322
  return capitalize(builder.join(' ').trim());
323
323
  }
324
324
  /**
325
- * Returns a human-readable string for a FHIR Range datatype, taking into account comparators and one-sided ranges
325
+ * Returns a human-readable string for a FHIR Range datatype, taking into account one-sided ranges
326
326
  * @param range A FHIR Range element
327
+ * @param precision Number of decimal places to display in the rendered quantity values
327
328
  * @param exclusive If true, one-sided ranges will be rendered with the '>' or '<' bounds rather than '>=' or '<='
328
329
  * @returns A human-readable string representation of the Range
329
330
  */
@@ -331,33 +332,41 @@
331
332
  if (exclusive && precision === undefined) {
332
333
  throw new Error('Precision must be specified for exclusive ranges');
333
334
  }
334
- const low = range?.low && { ...range.low };
335
- const high = range?.high && { ...range.high };
336
- if (!range || (low?.value === undefined && high?.value === undefined)) {
335
+ // Extract high and low range endpoints, explicitly ignoring any comparator
336
+ // since Range uses SimpleQuantity variants (see http://www.hl7.org/fhir/datatypes.html#Range)
337
+ const low = range?.low && { ...range.low, comparator: undefined };
338
+ const high = range?.high && { ...range.high, comparator: undefined };
339
+ if (low?.value === undefined && high?.value === undefined) {
337
340
  return '';
338
341
  }
339
- if (range.low?.value !== undefined && range.high?.value === undefined) {
342
+ if (low?.value !== undefined && high?.value === undefined) {
343
+ // Lower bound only
340
344
  if (exclusive && precision !== undefined) {
341
- range.low.value = preciseDecrement(range.low.value, precision);
342
- return `> ${formatQuantity(range.low, precision)}`;
345
+ low.value = preciseDecrement(low.value, precision);
346
+ return `> ${formatQuantity(low, precision)}`;
343
347
  }
344
- return `>= ${formatQuantity(range.low, precision)}`;
348
+ return `>= ${formatQuantity(low, precision)}`;
345
349
  }
346
- if (range.low?.value === undefined && range.high?.value !== undefined) {
350
+ else if (low?.value === undefined && high?.value !== undefined) {
351
+ // Upper bound only
347
352
  if (exclusive && precision !== undefined) {
348
- range.high.value = preciseIncrement(range.high.value, precision);
349
- return `< ${formatQuantity(range.high, precision)}`;
353
+ high.value = preciseIncrement(high.value, precision);
354
+ return `< ${formatQuantity(high, precision)}`;
350
355
  }
351
- return `<= ${formatQuantity(range.high, precision)}`;
356
+ return `<= ${formatQuantity(high, precision)}`;
352
357
  }
353
- if (low?.unit === high?.unit) {
354
- delete low?.unit;
358
+ else {
359
+ // Double-sided range
360
+ if (low?.unit === high?.unit) {
361
+ delete low?.unit; // Format like "X - Y units" instead of "X units - Y units"
362
+ }
363
+ return `${formatQuantity(low, precision)} - ${formatQuantity(high, precision)}`;
355
364
  }
356
- return `${formatQuantity(low, precision)} - ${formatQuantity(high, precision)}`;
357
365
  }
358
366
  /**
359
367
  * Returns a human-readable string for a FHIR Quantity datatype, taking into account units and comparators
360
368
  * @param quantity A FHIR Quantity element
369
+ * @param precision Number of decimal places to display in the rendered quantity values
361
370
  * @returns A human-readable string representation of the Quantity
362
371
  */
363
372
  function formatQuantity(quantity, precision) {
@@ -1203,6 +1212,7 @@
1203
1212
  const UNAUTHORIZED_ID = 'unauthorized';
1204
1213
  const FORBIDDEN_ID = 'forbidden';
1205
1214
  const TOO_MANY_REQUESTS_ID = 'too-many-requests';
1215
+ const ACCEPTED_ID = 'accepted';
1206
1216
  const allOk = {
1207
1217
  resourceType: 'OperationOutcome',
1208
1218
  id: OK_ID,
@@ -1307,6 +1317,19 @@
1307
1317
  },
1308
1318
  ],
1309
1319
  };
1320
+ const accepted = {
1321
+ resourceType: 'OperationOutcome',
1322
+ id: ACCEPTED_ID,
1323
+ issue: [
1324
+ {
1325
+ severity: 'information',
1326
+ code: 'informational',
1327
+ details: {
1328
+ text: 'Accepted',
1329
+ },
1330
+ },
1331
+ ],
1332
+ };
1310
1333
  function badRequest(details, expression) {
1311
1334
  return {
1312
1335
  resourceType: 'OperationOutcome',
@@ -6381,7 +6404,7 @@
6381
6404
  const globalSchema = baseSchema;
6382
6405
 
6383
6406
  // PKCE auth based on:
6384
- const MEDPLUM_VERSION = "2.0.15-025c3c04";
6407
+ const MEDPLUM_VERSION = "2.0.17-5c5ebbda";
6385
6408
  const DEFAULT_BASE_URL = 'https://api.medplum.com/';
6386
6409
  const DEFAULT_RESOURCE_CACHE_SIZE = 1000;
6387
6410
  const DEFAULT_CACHE_TIME = 60000; // 60 seconds
@@ -6389,6 +6412,35 @@
6389
6412
  const FHIR_CONTENT_TYPE = 'application/fhir+json';
6390
6413
  const PATCH_CONTENT_TYPE = 'application/json-patch+json';
6391
6414
  const system = { resourceType: 'Device', id: 'system', deviceName: [{ name: 'System' }] };
6415
+ /**
6416
+ * OAuth 2.0 Grant Type Identifiers
6417
+ * Standard identifiers defined here: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#name-grant-types
6418
+ * Token exchange extension defined here: https://datatracker.ietf.org/doc/html/rfc8693
6419
+ */
6420
+ exports.OAuthGrantType = void 0;
6421
+ (function (OAuthGrantType) {
6422
+ OAuthGrantType["ClientCredentials"] = "client_credentials";
6423
+ OAuthGrantType["AuthorizationCode"] = "authorization_code";
6424
+ OAuthGrantType["RefreshToken"] = "refresh_token";
6425
+ OAuthGrantType["TokenExchange"] = "urn:ietf:params:oauth:grant-type:token-exchange";
6426
+ })(exports.OAuthGrantType || (exports.OAuthGrantType = {}));
6427
+ /**
6428
+ * OAuth 2.0 Token Type Identifiers
6429
+ * See: https://datatracker.ietf.org/doc/html/rfc8693#name-token-type-identifiers
6430
+ */
6431
+ exports.OAuthTokenType = void 0;
6432
+ (function (OAuthTokenType) {
6433
+ /** Indicates that the token is an OAuth 2.0 access token issued by the given authorization server. */
6434
+ OAuthTokenType["AccessToken"] = "urn:ietf:params:oauth:token-type:access_token";
6435
+ /** Indicates that the token is an OAuth 2.0 refresh token issued by the given authorization server. */
6436
+ OAuthTokenType["RefreshToken"] = "urn:ietf:params:oauth:token-type:refresh_token";
6437
+ /** Indicates that the token is an ID Token as defined in Section 2 of [OpenID.Core]. */
6438
+ OAuthTokenType["IdToken"] = "urn:ietf:params:oauth:token-type:id_token";
6439
+ /** Indicates that the token is a base64url-encoded SAML 1.1 [OASIS.saml-core-1.1] assertion. */
6440
+ OAuthTokenType["Saml1Token"] = "urn:ietf:params:oauth:token-type:saml1";
6441
+ /** Indicates that the token is a base64url-encoded SAML 2.0 [OASIS.saml-core-2.0-os] assertion. */
6442
+ OAuthTokenType["Saml2Token"] = "urn:ietf:params:oauth:token-type:saml2";
6443
+ })(exports.OAuthTokenType || (exports.OAuthTokenType = {}));
6392
6444
  /**
6393
6445
  * The MedplumClient class provides a client for the Medplum FHIR server.
6394
6446
  *
@@ -6458,7 +6510,6 @@
6458
6510
  this.authorizeUrl = options?.authorizeUrl || this.baseUrl + 'oauth2/authorize';
6459
6511
  this.tokenUrl = options?.tokenUrl || this.baseUrl + 'oauth2/token';
6460
6512
  this.logoutUrl = options?.logoutUrl || this.baseUrl + 'oauth2/logout';
6461
- this.exchangeUrl = this.baseUrl + 'auth/exchange';
6462
6513
  this.onUnauthenticated = options?.onUnauthenticated;
6463
6514
  this.cacheTime = options?.cacheTime ?? DEFAULT_CACHE_TIME;
6464
6515
  if (this.cacheTime > 0) {
@@ -6530,7 +6581,7 @@
6530
6581
  * @param resourceType The resource type to invalidate.
6531
6582
  */
6532
6583
  invalidateSearches(resourceType) {
6533
- const url = 'fhir/R4/' + resourceType;
6584
+ const url = this.fhirBaseUrl + resourceType;
6534
6585
  if (this.requestCache) {
6535
6586
  for (const key of this.requestCache.keys()) {
6536
6587
  if (key.endsWith(url) || key.includes(url + '?')) {
@@ -6808,22 +6859,12 @@
6808
6859
  if (!clientId) {
6809
6860
  throw new Error('MedplumClient is missing clientId');
6810
6861
  }
6811
- const response = await this.fetch(this.exchangeUrl, {
6812
- method: 'POST',
6813
- headers: { 'Content-Type': 'application/json' },
6814
- body: JSON.stringify({
6815
- clientId: this.clientId,
6816
- externalAccessToken: token,
6817
- }),
6818
- credentials: 'include',
6819
- });
6820
- if (!response.ok) {
6821
- this.clearActiveLogin();
6822
- throw new Error('Failed to fetch tokens');
6823
- }
6824
- const tokens = await response.json();
6825
- await this.verifyTokens(tokens);
6826
- return this.getProfile();
6862
+ const formBody = new URLSearchParams();
6863
+ formBody.set('grant_type', exports.OAuthGrantType.TokenExchange);
6864
+ formBody.set('subject_token_type', exports.OAuthTokenType.AccessToken);
6865
+ formBody.set('client_id', clientId);
6866
+ formBody.set('subject_token', token);
6867
+ return this.fetchTokens(formBody);
6827
6868
  }
6828
6869
  /**
6829
6870
  * Builds the external identity provider redirect URI.
@@ -7655,7 +7696,7 @@
7655
7696
  * @returns The FHIR batch/transaction response bundle.
7656
7697
  */
7657
7698
  executeBatch(bundle) {
7658
- return this.post('fhir/R4', bundle);
7699
+ return this.post(this.fhirBaseUrl.slice(0, -1), bundle);
7659
7700
  }
7660
7701
  /**
7661
7702
  * Sends an email using the Medplum Email API.
@@ -7999,7 +8040,7 @@
7999
8040
  })),
8000
8041
  };
8001
8042
  // Execute the batch request
8002
- const response = (await this.post('fhir/R4', batch));
8043
+ const response = (await this.post(this.fhirBaseUrl.slice(0, -1), batch));
8003
8044
  // Process the response
8004
8045
  for (let i = 0; i < entries.length; i++) {
8005
8046
  const entry = entries[i];
@@ -8127,7 +8168,7 @@
8127
8168
  */
8128
8169
  processCode(code, loginParams) {
8129
8170
  const formBody = new URLSearchParams();
8130
- formBody.set('grant_type', 'authorization_code');
8171
+ formBody.set('grant_type', exports.OAuthGrantType.AuthorizationCode);
8131
8172
  formBody.set('code', code);
8132
8173
  formBody.set('client_id', loginParams?.clientId || this.clientId);
8133
8174
  formBody.set('redirect_uri', loginParams?.redirectUri || getWindowOrigin());
@@ -8149,7 +8190,7 @@
8149
8190
  }
8150
8191
  if (this.refreshToken) {
8151
8192
  const formBody = new URLSearchParams();
8152
- formBody.set('grant_type', 'refresh_token');
8193
+ formBody.set('grant_type', exports.OAuthGrantType.RefreshToken);
8153
8194
  formBody.set('client_id', this.clientId);
8154
8195
  formBody.set('refresh_token', this.refreshToken);
8155
8196
  this.refreshPromise = this.fetchTokens(formBody);
@@ -8173,7 +8214,7 @@
8173
8214
  this.clientId = clientId;
8174
8215
  this.clientSecret = clientSecret;
8175
8216
  const formBody = new URLSearchParams();
8176
- formBody.set('grant_type', 'client_credentials');
8217
+ formBody.set('grant_type', exports.OAuthGrantType.ClientCredentials);
8177
8218
  formBody.set('client_id', clientId);
8178
8219
  formBody.set('client_secret', clientSecret);
8179
8220
  return this.fetchTokens(formBody);
@@ -12278,7 +12319,18 @@
12278
12319
  * @returns A parsed SearchRequest.
12279
12320
  */
12280
12321
  function parseSearchRequest(resourceType, query) {
12281
- return parseSearchImpl(resourceType, query);
12322
+ const queryArray = [];
12323
+ for (const [key, value] of Object.entries(query)) {
12324
+ if (Array.isArray(value)) {
12325
+ for (let i = 0; i < value.length; i++) {
12326
+ queryArray.push([key, value[i]]);
12327
+ }
12328
+ }
12329
+ else {
12330
+ queryArray.push([key, value || '']);
12331
+ }
12332
+ }
12333
+ return parseSearchImpl(resourceType, queryArray);
12282
12334
  }
12283
12335
  /**
12284
12336
  * Parses a search URL into a search request.
@@ -12287,7 +12339,7 @@
12287
12339
  */
12288
12340
  function parseSearchUrl(url) {
12289
12341
  const resourceType = url.pathname.split('/').filter(Boolean).pop();
12290
- return parseSearchImpl(resourceType, Object.fromEntries(url.searchParams.entries()));
12342
+ return parseSearchImpl(resourceType, url.searchParams.entries());
12291
12343
  }
12292
12344
  /**
12293
12345
  * Parses a URL string into a SearchRequest.
@@ -12301,13 +12353,8 @@
12301
12353
  const searchRequest = {
12302
12354
  resourceType,
12303
12355
  };
12304
- for (const [key, value] of Object.entries(query)) {
12305
- if (Array.isArray(value)) {
12306
- value.forEach((element) => parseKeyValue(searchRequest, key, element));
12307
- }
12308
- else {
12309
- parseKeyValue(searchRequest, key, value ?? '');
12310
- }
12356
+ for (const [key, value] of query) {
12357
+ parseKeyValue(searchRequest, key, value);
12311
12358
  }
12312
12359
  return searchRequest;
12313
12360
  }
@@ -12708,6 +12755,7 @@
12708
12755
  exports.UnaryOperatorAtom = UnaryOperatorAtom;
12709
12756
  exports.UnionAtom = UnionAtom;
12710
12757
  exports.XorAtom = XorAtom;
12758
+ exports.accepted = accepted;
12711
12759
  exports.allOk = allOk;
12712
12760
  exports.arrayBufferToBase64 = arrayBufferToBase64;
12713
12761
  exports.arrayBufferToHex = arrayBufferToHex;