@medplum/core 2.0.2 → 2.0.4

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 (43) hide show
  1. package/README.md +7 -3
  2. package/dist/cjs/client.d.ts +16 -3
  3. package/dist/cjs/index.cjs +785 -438
  4. package/dist/cjs/index.cjs.map +1 -1
  5. package/dist/cjs/index.d.ts +4 -3
  6. package/dist/cjs/index.min.cjs +1 -1
  7. package/dist/cjs/outcomes.d.ts +1 -0
  8. package/dist/cjs/{searchparams.d.ts → search/details.d.ts} +0 -0
  9. package/dist/cjs/{match.d.ts → search/match.d.ts} +0 -0
  10. package/dist/cjs/search/parse.d.ts +17 -0
  11. package/dist/cjs/{search.d.ts → search/search.d.ts} +0 -0
  12. package/dist/cjs/types.d.ts +30 -7
  13. package/dist/esm/base-schema.json.mjs +7 -0
  14. package/dist/esm/base-schema.json.mjs.map +1 -1
  15. package/dist/esm/client.d.ts +16 -3
  16. package/dist/esm/client.mjs +114 -26
  17. package/dist/esm/client.mjs.map +1 -1
  18. package/dist/esm/index.d.ts +4 -3
  19. package/dist/esm/index.min.mjs +1 -1
  20. package/dist/esm/index.mjs +6 -5
  21. package/dist/esm/index.mjs.map +1 -1
  22. package/dist/esm/outcomes.d.ts +1 -0
  23. package/dist/esm/outcomes.mjs +6 -4
  24. package/dist/esm/outcomes.mjs.map +1 -1
  25. package/dist/esm/{searchparams.d.ts → search/details.d.ts} +0 -0
  26. package/dist/esm/{searchparams.mjs → search/details.mjs} +9 -11
  27. package/dist/esm/search/details.mjs.map +1 -0
  28. package/dist/esm/{match.d.ts → search/match.d.ts} +0 -0
  29. package/dist/esm/{match.mjs → search/match.mjs} +7 -7
  30. package/dist/esm/search/match.mjs.map +1 -0
  31. package/dist/esm/search/parse.d.ts +17 -0
  32. package/dist/esm/search/parse.mjs +218 -0
  33. package/dist/esm/search/parse.mjs.map +1 -0
  34. package/dist/esm/{search.d.ts → search/search.d.ts} +0 -0
  35. package/dist/esm/{search.mjs → search/search.mjs} +0 -3
  36. package/dist/esm/search/search.mjs.map +1 -0
  37. package/dist/esm/types.d.ts +30 -7
  38. package/dist/esm/types.mjs +63 -25
  39. package/dist/esm/types.mjs.map +1 -1
  40. package/package.json +1 -1
  41. package/dist/esm/match.mjs.map +0 -1
  42. package/dist/esm/search.mjs.map +0 -1
  43. package/dist/esm/searchparams.mjs.map +0 -1
@@ -1174,6 +1174,211 @@
1174
1174
  return decodePayload(payload);
1175
1175
  }
1176
1176
 
1177
+ const OK_ID = 'ok';
1178
+ const CREATED_ID = 'created';
1179
+ const GONE_ID = 'gone';
1180
+ const NOT_MODIFIED_ID = 'not-modified';
1181
+ const NOT_FOUND_ID = 'not-found';
1182
+ const UNAUTHORIZED_ID = 'unauthorized';
1183
+ const FORBIDDEN_ID = 'forbidden';
1184
+ const TOO_MANY_REQUESTS_ID = 'too-many-requests';
1185
+ const allOk = {
1186
+ resourceType: 'OperationOutcome',
1187
+ id: OK_ID,
1188
+ issue: [
1189
+ {
1190
+ severity: 'information',
1191
+ code: 'informational',
1192
+ details: {
1193
+ text: 'All OK',
1194
+ },
1195
+ },
1196
+ ],
1197
+ };
1198
+ const created = {
1199
+ resourceType: 'OperationOutcome',
1200
+ id: CREATED_ID,
1201
+ issue: [
1202
+ {
1203
+ severity: 'information',
1204
+ code: 'informational',
1205
+ details: {
1206
+ text: 'Created',
1207
+ },
1208
+ },
1209
+ ],
1210
+ };
1211
+ const notModified = {
1212
+ resourceType: 'OperationOutcome',
1213
+ id: NOT_MODIFIED_ID,
1214
+ issue: [
1215
+ {
1216
+ severity: 'information',
1217
+ code: 'informational',
1218
+ details: {
1219
+ text: 'Not Modified',
1220
+ },
1221
+ },
1222
+ ],
1223
+ };
1224
+ const notFound = {
1225
+ resourceType: 'OperationOutcome',
1226
+ id: NOT_FOUND_ID,
1227
+ issue: [
1228
+ {
1229
+ severity: 'error',
1230
+ code: 'not-found',
1231
+ details: {
1232
+ text: 'Not found',
1233
+ },
1234
+ },
1235
+ ],
1236
+ };
1237
+ const unauthorized = {
1238
+ resourceType: 'OperationOutcome',
1239
+ id: UNAUTHORIZED_ID,
1240
+ issue: [
1241
+ {
1242
+ severity: 'error',
1243
+ code: 'login',
1244
+ details: {
1245
+ text: 'Unauthorized',
1246
+ },
1247
+ },
1248
+ ],
1249
+ };
1250
+ const forbidden = {
1251
+ resourceType: 'OperationOutcome',
1252
+ id: FORBIDDEN_ID,
1253
+ issue: [
1254
+ {
1255
+ severity: 'error',
1256
+ code: 'forbidden',
1257
+ details: {
1258
+ text: 'Forbidden',
1259
+ },
1260
+ },
1261
+ ],
1262
+ };
1263
+ const gone = {
1264
+ resourceType: 'OperationOutcome',
1265
+ id: GONE_ID,
1266
+ issue: [
1267
+ {
1268
+ severity: 'error',
1269
+ code: 'deleted',
1270
+ details: {
1271
+ text: 'Gone',
1272
+ },
1273
+ },
1274
+ ],
1275
+ };
1276
+ const tooManyRequests = {
1277
+ resourceType: 'OperationOutcome',
1278
+ id: TOO_MANY_REQUESTS_ID,
1279
+ issue: [
1280
+ {
1281
+ severity: 'error',
1282
+ code: 'throttled',
1283
+ details: {
1284
+ text: 'Too Many Requests',
1285
+ },
1286
+ },
1287
+ ],
1288
+ };
1289
+ function badRequest(details, expression) {
1290
+ return {
1291
+ resourceType: 'OperationOutcome',
1292
+ issue: [
1293
+ {
1294
+ severity: 'error',
1295
+ code: 'invalid',
1296
+ details: {
1297
+ text: details,
1298
+ },
1299
+ expression: expression ? [expression] : undefined,
1300
+ },
1301
+ ],
1302
+ };
1303
+ }
1304
+ function isOperationOutcome(value) {
1305
+ return typeof value === 'object' && value !== null && value.resourceType === 'OperationOutcome';
1306
+ }
1307
+ function isOk(outcome) {
1308
+ return outcome.id === OK_ID || outcome.id === CREATED_ID || outcome.id === NOT_MODIFIED_ID;
1309
+ }
1310
+ function isNotFound(outcome) {
1311
+ return outcome.id === NOT_FOUND_ID;
1312
+ }
1313
+ function isGone(outcome) {
1314
+ return outcome.id === GONE_ID;
1315
+ }
1316
+ function getStatus(outcome) {
1317
+ if (outcome.id === OK_ID) {
1318
+ return 200;
1319
+ }
1320
+ else if (outcome.id === CREATED_ID) {
1321
+ return 201;
1322
+ }
1323
+ else if (outcome.id === NOT_MODIFIED_ID) {
1324
+ return 304;
1325
+ }
1326
+ else if (outcome.id === UNAUTHORIZED_ID) {
1327
+ return 401;
1328
+ }
1329
+ else if (outcome.id === FORBIDDEN_ID) {
1330
+ return 403;
1331
+ }
1332
+ else if (outcome.id === NOT_FOUND_ID) {
1333
+ return 404;
1334
+ }
1335
+ else if (outcome.id === GONE_ID) {
1336
+ return 410;
1337
+ }
1338
+ else if (outcome.id === TOO_MANY_REQUESTS_ID) {
1339
+ return 429;
1340
+ }
1341
+ else {
1342
+ return 400;
1343
+ }
1344
+ }
1345
+ /**
1346
+ * Asserts that the operation completed successfully and that the resource is defined.
1347
+ * @param outcome The operation outcome.
1348
+ * @param resource The resource that may or may not have been returned.
1349
+ */
1350
+ function assertOk(outcome, resource) {
1351
+ if (!isOk(outcome) || resource === undefined) {
1352
+ throw new OperationOutcomeError(outcome);
1353
+ }
1354
+ }
1355
+ class OperationOutcomeError extends Error {
1356
+ constructor(outcome) {
1357
+ super(outcome?.issue?.[0].details?.text);
1358
+ this.outcome = outcome;
1359
+ }
1360
+ }
1361
+ /**
1362
+ * Normalizes an error object into a displayable error string.
1363
+ * @param error The error value which could be a string, Error, OperationOutcome, or other unknown type.
1364
+ * @returns A display string for the error.
1365
+ */
1366
+ function normalizeErrorString(error) {
1367
+ if (!error) {
1368
+ return 'Unknown error';
1369
+ }
1370
+ if (typeof error === 'string') {
1371
+ return error;
1372
+ }
1373
+ if (error instanceof Error) {
1374
+ return error.message;
1375
+ }
1376
+ if (isOperationOutcome(error)) {
1377
+ return error.issue?.[0]?.details?.text ?? 'Unknown error';
1378
+ }
1379
+ return JSON.stringify(error);
1380
+ }
1381
+
1177
1382
  var _ReadablePromise_suspender, _ReadablePromise_status, _ReadablePromise_response, _ReadablePromise_error, _a;
1178
1383
  /**
1179
1384
  * The ReadablePromise class wraps a request promise suitable for React Suspense.
@@ -5736,6 +5941,13 @@
5736
5941
  code: "string"
5737
5942
  }
5738
5943
  ]
5944
+ },
5945
+ useSubject: {
5946
+ type: [
5947
+ {
5948
+ code: "boolean"
5949
+ }
5950
+ ]
5739
5951
  }
5740
5952
  }
5741
5953
  }
@@ -5811,23 +6023,6 @@
5811
6023
  PropertyType["url"] = "url";
5812
6024
  PropertyType["uuid"] = "uuid";
5813
6025
  })(exports.PropertyType || (exports.PropertyType = {}));
5814
- /**
5815
- * Creates a new empty IndexedStructureDefinition.
5816
- * @returns The empty IndexedStructureDefinition.
5817
- * @deprecated Use globalSchema
5818
- */
5819
- function createSchema() {
5820
- return { types: {} };
5821
- }
5822
- function createTypeSchema(typeName, structureDefinition, elementDefinition) {
5823
- return {
5824
- structureDefinition,
5825
- elementDefinition,
5826
- display: typeName,
5827
- description: elementDefinition.definition,
5828
- properties: {},
5829
- };
5830
- }
5831
6026
  /**
5832
6027
  * Indexes a bundle of StructureDefinitions for faster lookup.
5833
6028
  * @param bundle A FHIR bundle StructureDefinition resources.
@@ -5875,8 +6070,16 @@
5875
6070
  }
5876
6071
  const parts = path.split('.');
5877
6072
  const typeName = buildTypeName(parts);
5878
- globalSchema.types[typeName] = createTypeSchema(typeName, structureDefinition, elementDefinition);
5879
- globalSchema.types[typeName].parentType = buildTypeName(parts.slice(0, parts.length - 1));
6073
+ let typeSchema = globalSchema.types[typeName];
6074
+ if (!typeSchema) {
6075
+ globalSchema.types[typeName] = typeSchema = {};
6076
+ }
6077
+ typeSchema.parentType = typeSchema.parentType || buildTypeName(parts.slice(0, parts.length - 1));
6078
+ typeSchema.display = typeSchema.display || typeName;
6079
+ typeSchema.structureDefinition = typeSchema.structureDefinition || structureDefinition;
6080
+ typeSchema.elementDefinition = typeSchema.elementDefinition || elementDefinition;
6081
+ typeSchema.description = typeSchema.description || elementDefinition.definition;
6082
+ typeSchema.properties = typeSchema.properties || {};
5880
6083
  }
5881
6084
  /**
5882
6085
  * Indexes PropertySchema from an ElementDefinition.
@@ -5991,15 +6194,54 @@
5991
6194
  }
5992
6195
  return components.map(capitalize).join('');
5993
6196
  }
6197
+ /**
6198
+ * Returns true if the type schema is a DomainResource.
6199
+ * @param typeSchema The type schema to check.
6200
+ * @returns True if the type schema is a DomainResource.
6201
+ */
6202
+ function isResourceType(typeSchema) {
6203
+ return typeSchema.structureDefinition?.baseDefinition === 'http://hl7.org/fhir/StructureDefinition/DomainResource';
6204
+ }
6205
+ /**
6206
+ * Returns an array of all resource types.
6207
+ * Note that this is based on globalSchema, and will only return resource types that are currently in memory.
6208
+ * @returns An array of all resource types.
6209
+ */
6210
+ function getResourceTypes() {
6211
+ const result = [];
6212
+ for (const [resourceType, typeSchema] of Object.entries(globalSchema.types)) {
6213
+ if (isResourceType(typeSchema)) {
6214
+ result.push(resourceType);
6215
+ }
6216
+ }
6217
+ return result;
6218
+ }
6219
+ /**
6220
+ * Returns the type schema for the resource type.
6221
+ * @param resourceType The resource type.
6222
+ * @returns The type schema for the resource type.
6223
+ */
6224
+ function getResourceTypeSchema(resourceType) {
6225
+ return globalSchema.types[resourceType];
6226
+ }
6227
+ /**
6228
+ * Returns the search parameters for the resource type indexed by search code.
6229
+ * @param resourceType The resource type.
6230
+ * @returns The search parameters for the resource type indexed by search code.
6231
+ */
6232
+ function getSearchParameters(resourceType) {
6233
+ return globalSchema.types[resourceType].searchParams;
6234
+ }
6235
+ /**
6236
+ * Returns a human friendly display name for a FHIR element definition path.
6237
+ * @param path The FHIR element definition path.
6238
+ * @returns The best guess of the display name.
6239
+ */
5994
6240
  function getPropertyDisplayName(path) {
5995
6241
  // Get the property name, which is the remainder after the last period
5996
6242
  // For example, for path "Patient.birthDate"
5997
6243
  // the property name is "birthDate"
5998
6244
  const propertyName = path.replaceAll('[x]', '').split('.').pop();
5999
- // Special case for ID
6000
- if (propertyName === 'id') {
6001
- return 'ID';
6002
- }
6003
6245
  // Split by capital letters
6004
6246
  // Capitalize the first letter of each word
6005
6247
  // Join together with spaces in between
@@ -6008,11 +6250,19 @@
6008
6250
  // the display name is "Birth Date".
6009
6251
  return propertyName
6010
6252
  .split(/(?=[A-Z])/)
6011
- .map(capitalize)
6253
+ .map(capitalizeDisplayWord)
6012
6254
  .join(' ')
6013
6255
  .replace('_', ' ')
6014
6256
  .replace(/\s+/g, ' ');
6015
6257
  }
6258
+ const capitalizedWords = new Set(['ID', 'PKCE', 'JWKS', 'URI', 'URL']);
6259
+ function capitalizeDisplayWord(word) {
6260
+ const upper = word.toUpperCase();
6261
+ if (capitalizedWords.has(upper)) {
6262
+ return upper;
6263
+ }
6264
+ return upper.charAt(0) + word.slice(1);
6265
+ }
6016
6266
  /**
6017
6267
  * Returns an element definition by type and property name.
6018
6268
  * Handles content references.
@@ -6046,18 +6296,19 @@
6046
6296
 
6047
6297
  // PKCE auth based on:
6048
6298
  // https://aws.amazon.com/blogs/security/how-to-add-authentication-single-page-web-application-with-amazon-cognito-oauth2-implementation/
6049
- var _MedplumClient_instances, _MedplumClient_fetch, _MedplumClient_createPdf, _MedplumClient_storage, _MedplumClient_requestCache, _MedplumClient_cacheTime, _MedplumClient_baseUrl, _MedplumClient_fhirBaseUrl, _MedplumClient_authorizeUrl, _MedplumClient_tokenUrl, _MedplumClient_logoutUrl, _MedplumClient_onUnauthenticated, _MedplumClient_autoBatchTime, _MedplumClient_autoBatchQueue, _MedplumClient_clientId, _MedplumClient_clientSecret, _MedplumClient_autoBatchTimerId, _MedplumClient_accessToken, _MedplumClient_refreshToken, _MedplumClient_refreshPromise, _MedplumClient_profilePromise, _MedplumClient_profile, _MedplumClient_config, _MedplumClient_addLogin, _MedplumClient_refreshProfile, _MedplumClient_getCacheEntry, _MedplumClient_setCacheEntry, _MedplumClient_request, _MedplumClient_executeAutoBatch, _MedplumClient_addFetchOptionsDefaults, _MedplumClient_setRequestContentType, _MedplumClient_setRequestBody, _MedplumClient_handleUnauthenticated, _MedplumClient_requestAuthorization, _MedplumClient_refresh, _MedplumClient_fetchTokens, _MedplumClient_verifyTokens, _MedplumClient_setupStorageListener;
6050
- const MEDPLUM_VERSION = "2.0.2-2d479f42";
6299
+ var _MedplumClient_instances, _MedplumClient_fetch, _MedplumClient_createPdf, _MedplumClient_storage, _MedplumClient_requestCache, _MedplumClient_cacheTime, _MedplumClient_baseUrl, _MedplumClient_fhirBaseUrl, _MedplumClient_authorizeUrl, _MedplumClient_tokenUrl, _MedplumClient_logoutUrl, _MedplumClient_onUnauthenticated, _MedplumClient_autoBatchTime, _MedplumClient_autoBatchQueue, _MedplumClient_clientId, _MedplumClient_clientSecret, _MedplumClient_autoBatchTimerId, _MedplumClient_accessToken, _MedplumClient_refreshToken, _MedplumClient_refreshPromise, _MedplumClient_profilePromise, _MedplumClient_profile, _MedplumClient_config, _MedplumClient_addLogin, _MedplumClient_refreshProfile, _MedplumClient_getCacheEntry, _MedplumClient_setCacheEntry, _MedplumClient_cacheResource, _MedplumClient_deleteCacheEntry, _MedplumClient_request, _MedplumClient_fetchWithRetry, _MedplumClient_executeAutoBatch, _MedplumClient_addFetchOptionsDefaults, _MedplumClient_setRequestContentType, _MedplumClient_setRequestBody, _MedplumClient_handleUnauthenticated, _MedplumClient_requestAuthorization, _MedplumClient_refresh, _MedplumClient_fetchTokens, _MedplumClient_verifyTokens, _MedplumClient_setupStorageListener;
6300
+ const MEDPLUM_VERSION = "2.0.4-c4747b4e";
6051
6301
  const DEFAULT_BASE_URL = 'https://api.medplum.com/';
6052
6302
  const DEFAULT_RESOURCE_CACHE_SIZE = 1000;
6053
6303
  const DEFAULT_CACHE_TIME = 60000; // 60 seconds
6054
6304
  const JSON_CONTENT_TYPE = 'application/json';
6055
6305
  const FHIR_CONTENT_TYPE = 'application/fhir+json';
6056
6306
  const PATCH_CONTENT_TYPE = 'application/json-patch+json';
6307
+ const system = { resourceType: 'Device', id: 'system', deviceName: [{ name: 'System' }] };
6057
6308
  /**
6058
6309
  * The MedplumClient class provides a client for the Medplum FHIR server.
6059
6310
  *
6060
- * The client can be used in the browser, in a NodeJS application, or in a Medplum Bot.
6311
+ * The client can be used in the browser, in a Node.js application, or in a Medplum Bot.
6061
6312
  *
6062
6313
  * The client provides helpful methods for common operations such as:
6063
6314
  * 1) Authenticating
@@ -6465,16 +6716,29 @@
6465
6716
  * @param clientId The external client ID.
6466
6717
  * @param redirectUri The external identity provider redirect URI.
6467
6718
  * @param baseLogin The Medplum login request.
6719
+ * @category Authentication
6468
6720
  */
6469
6721
  async signInWithExternalAuth(authorizeUrl, clientId, redirectUri, baseLogin) {
6470
6722
  const loginRequest = await this.ensureCodeChallenge(baseLogin);
6723
+ window.location.assign(this.getExternalAuthRedirectUri(authorizeUrl, clientId, redirectUri, loginRequest));
6724
+ }
6725
+ /**
6726
+ * Builds the external identity provider redirect URI.
6727
+ * @param authorizeUrl The external authorization URL.
6728
+ * @param clientId The external client ID.
6729
+ * @param redirectUri The external identity provider redirect URI.
6730
+ * @param loginRequest The Medplum login request.
6731
+ * @returns The external identity provider redirect URI.
6732
+ * @category Authentication
6733
+ */
6734
+ getExternalAuthRedirectUri(authorizeUrl, clientId, redirectUri, loginRequest) {
6471
6735
  const url = new URL(authorizeUrl);
6472
6736
  url.searchParams.set('response_type', 'code');
6473
6737
  url.searchParams.set('client_id', clientId);
6474
6738
  url.searchParams.set('redirect_uri', redirectUri);
6475
6739
  url.searchParams.set('scope', 'openid profile email');
6476
6740
  url.searchParams.set('state', JSON.stringify(loginRequest));
6477
- window.location.assign(url.toString());
6741
+ return url.toString();
6478
6742
  }
6479
6743
  /**
6480
6744
  * Builds a FHIR URL from a collection of URL path components.
@@ -6549,7 +6813,23 @@
6549
6813
  * @returns Promise to the search result bundle.
6550
6814
  */
6551
6815
  search(resourceType, query, options = {}) {
6552
- return this.get(this.fhirSearchUrl(resourceType, query), options);
6816
+ const url = this.fhirSearchUrl(resourceType, query);
6817
+ const cacheKey = url.toString() + '-search';
6818
+ const cached = __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_getCacheEntry).call(this, cacheKey, options);
6819
+ if (cached) {
6820
+ return cached.value;
6821
+ }
6822
+ const promise = new ReadablePromise((async () => {
6823
+ const bundle = await this.get(url, options);
6824
+ if (bundle.entry) {
6825
+ for (const entry of bundle.entry) {
6826
+ __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_cacheResource).call(this, entry.resource);
6827
+ }
6828
+ }
6829
+ return bundle;
6830
+ })());
6831
+ __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_setCacheEntry).call(this, cacheKey, promise);
6832
+ return promise;
6553
6833
  }
6554
6834
  /**
6555
6835
  * Sends a FHIR search request for a single resource.
@@ -6658,6 +6938,9 @@
6658
6938
  if (!refString) {
6659
6939
  return undefined;
6660
6940
  }
6941
+ if (refString === 'system') {
6942
+ return system;
6943
+ }
6661
6944
  const [resourceType, id] = refString.split('/');
6662
6945
  if (!resourceType || !id) {
6663
6946
  return undefined;
@@ -6710,6 +6993,9 @@
6710
6993
  if (!refString) {
6711
6994
  return new ReadablePromise(Promise.reject(new Error('Missing reference')));
6712
6995
  }
6996
+ if (refString === 'system') {
6997
+ return new ReadablePromise(Promise.resolve(system));
6998
+ }
6713
6999
  const [resourceType, id] = refString.split('/');
6714
7000
  if (!resourceType || !id) {
6715
7001
  return new ReadablePromise(Promise.reject(new Error('Invalid reference')));
@@ -6734,11 +7020,17 @@
6734
7020
  * @param resourceType The FHIR resource type.
6735
7021
  * @returns Promise to a schema with the requested resource type.
6736
7022
  */
6737
- async requestSchema(resourceType) {
7023
+ requestSchema(resourceType) {
6738
7024
  if (resourceType in globalSchema.types) {
6739
- return globalSchema;
7025
+ return Promise.resolve(globalSchema);
6740
7026
  }
6741
- const query = `{
7027
+ const cacheKey = resourceType + '-requestSchema';
7028
+ const cached = __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_getCacheEntry).call(this, cacheKey, undefined);
7029
+ if (cached) {
7030
+ return cached.value;
7031
+ }
7032
+ const promise = new ReadablePromise((async () => {
7033
+ const query = `{
6742
7034
  StructureDefinitionList(name: "${resourceType}") {
6743
7035
  name,
6744
7036
  description,
@@ -6767,14 +7059,17 @@
6767
7059
  target
6768
7060
  }
6769
7061
  }`.replace(/\s+/g, ' ');
6770
- const response = (await this.graphql(query));
6771
- for (const structureDefinition of response.data.StructureDefinitionList) {
6772
- indexStructureDefinition(structureDefinition);
6773
- }
6774
- for (const searchParameter of response.data.SearchParameterList) {
6775
- indexSearchParameter(searchParameter);
6776
- }
6777
- return globalSchema;
7062
+ const response = (await this.graphql(query));
7063
+ for (const structureDefinition of response.data.StructureDefinitionList) {
7064
+ indexStructureDefinition(structureDefinition);
7065
+ }
7066
+ for (const searchParameter of response.data.SearchParameterList) {
7067
+ indexSearchParameter(searchParameter);
7068
+ }
7069
+ return globalSchema;
7070
+ })());
7071
+ __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_setCacheEntry).call(this, cacheKey, promise);
7072
+ return promise;
6778
7073
  }
6779
7074
  /**
6780
7075
  * Reads resource history by resource type and ID.
@@ -7075,10 +7370,15 @@
7075
7370
  throw new Error('Missing id');
7076
7371
  }
7077
7372
  this.invalidateSearches(resource.resourceType);
7078
- const result = await this.put(this.fhirUrl(resource.resourceType, resource.id), resource);
7079
- // On 304 not modified, result will be undefined
7080
- // Return the user input instead
7081
- return result ?? resource;
7373
+ let result = await this.put(this.fhirUrl(resource.resourceType, resource.id), resource);
7374
+ if (!result) {
7375
+ // On 304 not modified, result will be undefined
7376
+ // Return the user input instead
7377
+ // return result ?? resource;
7378
+ result = resource;
7379
+ }
7380
+ __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_cacheResource).call(this, result);
7381
+ return result;
7082
7382
  }
7083
7383
  /**
7084
7384
  * Updates a FHIR resource using JSONPatch operations.
@@ -7125,6 +7425,7 @@
7125
7425
  * @returns The result of the delete operation.
7126
7426
  */
7127
7427
  deleteResource(resourceType, id) {
7428
+ __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_deleteCacheEntry).call(this, this.fhirUrl(resourceType, id).toString());
7128
7429
  this.invalidateSearches(resourceType);
7129
7430
  return this.delete(this.fhirUrl(resourceType, id));
7130
7431
  }
@@ -7388,6 +7689,7 @@
7388
7689
  /**
7389
7690
  * Starts a new PKCE flow.
7390
7691
  * These PKCE values are stateful, and must survive redirects and page refreshes.
7692
+ * @category Authentication
7391
7693
  */
7392
7694
  async startPkce() {
7393
7695
  const pkceState = getRandomString();
@@ -7403,6 +7705,7 @@
7403
7705
  * Processes an OAuth authorization code.
7404
7706
  * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
7405
7707
  * @param code The authorization code received by URL parameter.
7708
+ * @category Authentication
7406
7709
  */
7407
7710
  processCode(code) {
7408
7711
  const formBody = new URLSearchParams();
@@ -7410,9 +7713,11 @@
7410
7713
  formBody.set('client_id', __classPrivateFieldGet(this, _MedplumClient_clientId, "f"));
7411
7714
  formBody.set('code', code);
7412
7715
  formBody.set('redirect_uri', getWindowOrigin());
7413
- const codeVerifier = sessionStorage.getItem('codeVerifier');
7414
- if (codeVerifier) {
7415
- formBody.set('code_verifier', codeVerifier);
7716
+ if (typeof sessionStorage !== 'undefined') {
7717
+ const codeVerifier = sessionStorage.getItem('codeVerifier');
7718
+ if (codeVerifier) {
7719
+ formBody.set('code_verifier', codeVerifier);
7720
+ }
7416
7721
  }
7417
7722
  return __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_fetchTokens).call(this, formBody);
7418
7723
  }
@@ -7464,6 +7769,14 @@
7464
7769
  if (__classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") > 0) {
7465
7770
  __classPrivateFieldGet(this, _MedplumClient_requestCache, "f").set(key, { requestTime: Date.now(), value });
7466
7771
  }
7772
+ }, _MedplumClient_cacheResource = function _MedplumClient_cacheResource(resource) {
7773
+ if (resource?.id) {
7774
+ __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_setCacheEntry).call(this, this.fhirUrl(resource.resourceType, resource.id).toString(), new ReadablePromise(Promise.resolve(resource)));
7775
+ }
7776
+ }, _MedplumClient_deleteCacheEntry = function _MedplumClient_deleteCacheEntry(key) {
7777
+ if (__classPrivateFieldGet(this, _MedplumClient_cacheTime, "f") > 0) {
7778
+ __classPrivateFieldGet(this, _MedplumClient_requestCache, "f").delete(key);
7779
+ }
7467
7780
  }, _MedplumClient_request =
7468
7781
  /**
7469
7782
  * Makes an HTTP request.
@@ -7481,7 +7794,7 @@
7481
7794
  }
7482
7795
  options.method = method;
7483
7796
  __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_addFetchOptionsDefaults).call(this, options);
7484
- const response = await __classPrivateFieldGet(this, _MedplumClient_fetch, "f").call(this, url, options);
7797
+ const response = await __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_fetchWithRetry).call(this, url, options);
7485
7798
  if (response.status === 401) {
7486
7799
  // Refresh and try again
7487
7800
  return __classPrivateFieldGet(this, _MedplumClient_instances, "m", _MedplumClient_handleUnauthenticated).call(this, method, url, options);
@@ -7490,11 +7803,30 @@
7490
7803
  // No content or change
7491
7804
  return undefined;
7492
7805
  }
7493
- const obj = await response.json();
7806
+ let obj = undefined;
7807
+ try {
7808
+ obj = await response.json();
7809
+ }
7810
+ catch (err) {
7811
+ console.error('Error parsing response', response.status, err);
7812
+ throw err;
7813
+ }
7494
7814
  if (response.status >= 400) {
7495
7815
  throw obj;
7496
7816
  }
7497
7817
  return obj;
7818
+ }, _MedplumClient_fetchWithRetry = async function _MedplumClient_fetchWithRetry(url, options) {
7819
+ const maxRetries = 3;
7820
+ const retryDelay = 200;
7821
+ let response = undefined;
7822
+ for (let retry = 0; retry < maxRetries; retry++) {
7823
+ response = (await __classPrivateFieldGet(this, _MedplumClient_fetch, "f").call(this, url, options));
7824
+ if (response.status < 500) {
7825
+ return response;
7826
+ }
7827
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
7828
+ }
7829
+ return response;
7498
7830
  }, _MedplumClient_executeAutoBatch =
7499
7831
  /**
7500
7832
  * Executes a batch of requests that were automatically batched together.
@@ -7530,7 +7862,12 @@
7530
7862
  for (let i = 0; i < entries.length; i++) {
7531
7863
  const entry = entries[i];
7532
7864
  const responseEntry = response.entry?.[i];
7533
- entry.resolve(responseEntry?.resource);
7865
+ if (responseEntry?.response?.outcome && !isOk(responseEntry.response.outcome)) {
7866
+ entry.reject(responseEntry.response.outcome);
7867
+ }
7868
+ else {
7869
+ entry.resolve(responseEntry?.resource);
7870
+ }
7534
7871
  }
7535
7872
  }, _MedplumClient_addFetchOptionsDefaults = function _MedplumClient_addFetchOptionsDefaults(options) {
7536
7873
  if (!options.headers) {
@@ -10619,6 +10956,160 @@
10619
10956
  }
10620
10957
  }
10621
10958
 
10959
+ exports.SearchParameterType = void 0;
10960
+ (function (SearchParameterType) {
10961
+ SearchParameterType["BOOLEAN"] = "BOOLEAN";
10962
+ SearchParameterType["NUMBER"] = "NUMBER";
10963
+ SearchParameterType["QUANTITY"] = "QUANTITY";
10964
+ SearchParameterType["TEXT"] = "TEXT";
10965
+ SearchParameterType["REFERENCE"] = "REFERENCE";
10966
+ SearchParameterType["DATE"] = "DATE";
10967
+ SearchParameterType["DATETIME"] = "DATETIME";
10968
+ SearchParameterType["PERIOD"] = "PERIOD";
10969
+ })(exports.SearchParameterType || (exports.SearchParameterType = {}));
10970
+ /**
10971
+ * Returns the type details of a SearchParameter.
10972
+ *
10973
+ * The SearchParameter resource has a "type" parameter, but that is missing some critical information.
10974
+ *
10975
+ * For example:
10976
+ * 1) The "date" type includes "date", "datetime", and "period".
10977
+ * 2) The "token" type includes enums and booleans.
10978
+ * 3) Arrays/multiple values are not reflected at all.
10979
+ *
10980
+ * @param resourceType The root resource type.
10981
+ * @param searchParam The search parameter.
10982
+ * @returns The search parameter type details.
10983
+ */
10984
+ function getSearchParameterDetails(resourceType, searchParam) {
10985
+ let result = globalSchema.types[resourceType]?.searchParamsDetails?.[searchParam.code];
10986
+ if (!result) {
10987
+ result = buildSearchParamterDetails(resourceType, searchParam);
10988
+ }
10989
+ return result;
10990
+ }
10991
+ function setSearchParamterDetails(resourceType, code, details) {
10992
+ const typeSchema = globalSchema.types[resourceType];
10993
+ if (!typeSchema.searchParamsDetails) {
10994
+ typeSchema.searchParamsDetails = {};
10995
+ }
10996
+ typeSchema.searchParamsDetails[code] = details;
10997
+ }
10998
+ function buildSearchParamterDetails(resourceType, searchParam) {
10999
+ const code = searchParam.code;
11000
+ const columnName = convertCodeToColumnName(code);
11001
+ const expression = getExpressionForResourceType(resourceType, searchParam.expression)?.split('.');
11002
+ if (!expression) {
11003
+ // This happens on compound types
11004
+ // In the future, explore returning multiple column definitions
11005
+ return { columnName, type: exports.SearchParameterType.TEXT };
11006
+ }
11007
+ const defaultType = getSearchParameterType(searchParam);
11008
+ let baseType = resourceType;
11009
+ let elementDefinition = undefined;
11010
+ let propertyType = undefined;
11011
+ let array = false;
11012
+ for (let i = 1; i < expression.length; i++) {
11013
+ const propertyName = expression[i];
11014
+ elementDefinition = getElementDefinition(baseType, propertyName);
11015
+ if (!elementDefinition) {
11016
+ throw new Error(`Element definition not found for ${resourceType} ${searchParam.code}`);
11017
+ }
11018
+ if (elementDefinition.max === '*') {
11019
+ array = true;
11020
+ }
11021
+ propertyType = elementDefinition.type?.[0].code;
11022
+ if (!propertyType) {
11023
+ // This happens when one of parent properties uses contentReference
11024
+ // In the future, explore following the reference
11025
+ return { columnName, type: defaultType, array };
11026
+ }
11027
+ if (i < expression.length - 1) {
11028
+ if (isBackboneElement(propertyType)) {
11029
+ baseType = buildTypeName(elementDefinition.path?.split('.'));
11030
+ }
11031
+ else {
11032
+ baseType = propertyType;
11033
+ }
11034
+ }
11035
+ }
11036
+ const type = getSearchParameterType(searchParam, propertyType);
11037
+ const result = { columnName, type, elementDefinition, array };
11038
+ setSearchParamterDetails(resourceType, code, result);
11039
+ return result;
11040
+ }
11041
+ function isBackboneElement(propertyType) {
11042
+ return propertyType === 'Element' || propertyType === 'BackboneElement';
11043
+ }
11044
+ /**
11045
+ * Converts a hyphen-delimited code to camelCase string.
11046
+ * @param code The search parameter code.
11047
+ * @returns The SQL column name.
11048
+ */
11049
+ function convertCodeToColumnName(code) {
11050
+ return code.split('-').reduce((result, word, index) => result + (index ? capitalize(word) : word), '');
11051
+ }
11052
+ function getSearchParameterType(searchParam, propertyType) {
11053
+ let type = exports.SearchParameterType.TEXT;
11054
+ switch (searchParam.type) {
11055
+ case 'date':
11056
+ if (propertyType === exports.PropertyType.dateTime || propertyType === exports.PropertyType.instant) {
11057
+ type = exports.SearchParameterType.DATETIME;
11058
+ }
11059
+ else {
11060
+ type = exports.SearchParameterType.DATE;
11061
+ }
11062
+ break;
11063
+ case 'number':
11064
+ type = exports.SearchParameterType.NUMBER;
11065
+ break;
11066
+ case 'quantity':
11067
+ type = exports.SearchParameterType.QUANTITY;
11068
+ break;
11069
+ case 'reference':
11070
+ type = exports.SearchParameterType.REFERENCE;
11071
+ break;
11072
+ case 'token':
11073
+ if (propertyType === 'boolean') {
11074
+ type = exports.SearchParameterType.BOOLEAN;
11075
+ }
11076
+ break;
11077
+ }
11078
+ return type;
11079
+ }
11080
+ function getExpressionForResourceType(resourceType, expression) {
11081
+ const expressions = expression.split(' | ');
11082
+ for (const e of expressions) {
11083
+ if (isIgnoredExpression(e)) {
11084
+ continue;
11085
+ }
11086
+ const simplified = simplifyExpression(e);
11087
+ if (simplified.startsWith(resourceType + '.')) {
11088
+ return simplified;
11089
+ }
11090
+ }
11091
+ return undefined;
11092
+ }
11093
+ function isIgnoredExpression(input) {
11094
+ return input.includes(' as Period') || input.includes(' as SampledDate');
11095
+ }
11096
+ function simplifyExpression(input) {
11097
+ let result = input.trim();
11098
+ if (result.startsWith('(') && result.endsWith(')')) {
11099
+ result = result.substring(1, result.length - 1);
11100
+ }
11101
+ if (result.includes('[0]')) {
11102
+ result = result.replaceAll('[0]', '');
11103
+ }
11104
+ const stopStrings = [' != ', ' as ', '.as(', '.exists(', '.where('];
11105
+ for (const stopString of stopStrings) {
11106
+ if (result.includes(stopString)) {
11107
+ result = result.substring(0, result.indexOf(stopString));
11108
+ }
11109
+ }
11110
+ return result;
11111
+ }
11112
+
10622
11113
  const DEFAULT_SEARCH_COUNT = 20;
10623
11114
  /**
10624
11115
  * Search operators.
@@ -10818,216 +11309,57 @@
10818
11309
  return `${filter.code}${modifier}=${prefix}${encodeURIComponent(filter.value)}`;
10819
11310
  }
10820
11311
  function formatSortRules(sortRules) {
10821
- if (!sortRules || sortRules.length === 0) {
10822
- return '';
10823
- }
10824
11312
  return '_sort=' + sortRules.map((sr) => (sr.descending ? '-' + sr.code : sr.code)).join(',');
10825
11313
  }
10826
11314
 
10827
- exports.SearchParameterType = void 0;
10828
- (function (SearchParameterType) {
10829
- SearchParameterType["BOOLEAN"] = "BOOLEAN";
10830
- SearchParameterType["NUMBER"] = "NUMBER";
10831
- SearchParameterType["QUANTITY"] = "QUANTITY";
10832
- SearchParameterType["TEXT"] = "TEXT";
10833
- SearchParameterType["REFERENCE"] = "REFERENCE";
10834
- SearchParameterType["DATE"] = "DATE";
10835
- SearchParameterType["DATETIME"] = "DATETIME";
10836
- SearchParameterType["PERIOD"] = "PERIOD";
10837
- })(exports.SearchParameterType || (exports.SearchParameterType = {}));
10838
11315
  /**
10839
- * Returns the type details of a SearchParameter.
10840
- *
10841
- * The SearchParameter resource has a "type" parameter, but that is missing some critical information.
10842
- *
10843
- * For example:
10844
- * 1) The "date" type includes "date", "datetime", and "period".
10845
- * 2) The "token" type includes enums and booleans.
10846
- * 3) Arrays/multiple values are not reflected at all.
10847
- *
10848
- * @param resourceType The root resource type.
10849
- * @param searchParam The search parameter.
10850
- * @returns The search parameter type details.
11316
+ * Determines if the resource matches the search request.
11317
+ * @param resource The resource that was created or updated.
11318
+ * @param searchRequest The subscription criteria as a search request.
11319
+ * @returns True if the resource satisfies the search request.
10851
11320
  */
10852
- function getSearchParameterDetails(resourceType, searchParam) {
10853
- let result = globalSchema.types[resourceType]?.searchParamsDetails?.[searchParam.code];
10854
- if (!result) {
10855
- result = buildSearchParamterDetails(resourceType, searchParam);
11321
+ function matchesSearchRequest(resource, searchRequest) {
11322
+ if (searchRequest.resourceType !== resource.resourceType) {
11323
+ return false;
10856
11324
  }
10857
- return result;
11325
+ if (searchRequest.filters) {
11326
+ for (const filter of searchRequest.filters) {
11327
+ if (!matchesSearchFilter(resource, searchRequest, filter)) {
11328
+ return false;
11329
+ }
11330
+ }
11331
+ }
11332
+ return true;
10858
11333
  }
10859
- function setSearchParamterDetails(resourceType, code, details) {
10860
- const typeSchema = globalSchema.types[resourceType];
10861
- if (!typeSchema.searchParamsDetails) {
10862
- typeSchema.searchParamsDetails = {};
11334
+ /**
11335
+ * Determines if the resource matches the search filter.
11336
+ * @param resource The resource that was created or updated.
11337
+ * @param filter One of the filters of a subscription criteria.
11338
+ * @returns True if the resource satisfies the search filter.
11339
+ */
11340
+ function matchesSearchFilter(resource, searchRequest, filter) {
11341
+ const searchParam = globalSchema.types[searchRequest.resourceType]?.searchParams?.[filter.code];
11342
+ switch (searchParam?.type) {
11343
+ case 'reference':
11344
+ return matchesReferenceFilter(resource, filter, searchParam);
11345
+ case 'string':
11346
+ return matchesStringFilter(resource, filter, searchParam);
11347
+ case 'token':
11348
+ return matchesTokenFilter(resource, filter, searchParam);
11349
+ case 'date':
11350
+ return matchesDateFilter(resource, filter, searchParam);
10863
11351
  }
10864
- typeSchema.searchParamsDetails[code] = details;
11352
+ // Unknown search parameter or search parameter type
11353
+ // Default fail the check
11354
+ return false;
10865
11355
  }
10866
- function buildSearchParamterDetails(resourceType, searchParam) {
10867
- if (searchParam.code === '_lastUpdated') {
10868
- return { columnName: 'lastUpdated', type: exports.SearchParameterType.DATETIME };
10869
- }
10870
- const code = searchParam.code;
10871
- const columnName = convertCodeToColumnName(code);
10872
- const expression = getExpressionForResourceType(resourceType, searchParam.expression)?.split('.');
10873
- if (!expression) {
10874
- // This happens on compound types
10875
- // In the future, explore returning multiple column definitions
10876
- return { columnName, type: exports.SearchParameterType.TEXT };
10877
- }
10878
- const defaultType = getSearchParameterType(searchParam);
10879
- let baseType = resourceType;
10880
- let elementDefinition = undefined;
10881
- let propertyType = undefined;
10882
- let array = false;
10883
- for (let i = 1; i < expression.length; i++) {
10884
- const propertyName = expression[i];
10885
- elementDefinition =
10886
- globalSchema.types[baseType]?.properties?.[propertyName] ??
10887
- globalSchema.types[baseType]?.properties?.[propertyName + '[x]'];
10888
- if (!elementDefinition) {
10889
- throw new Error(`Element definition not found for ${resourceType} ${searchParam.code}`);
10890
- }
10891
- if (elementDefinition.max === '*') {
10892
- array = true;
10893
- }
10894
- propertyType = elementDefinition.type?.[0].code;
10895
- if (!propertyType) {
10896
- // This happens when one of parent properties uses contentReference
10897
- // In the future, explore following the reference
10898
- return { columnName, type: defaultType, array };
10899
- }
10900
- if (i < expression.length - 1) {
10901
- if (propertyType === 'Element' || propertyType === 'BackboneElement') {
10902
- baseType = baseType + capitalize(propertyName);
10903
- }
10904
- else {
10905
- baseType = propertyType;
10906
- }
10907
- }
10908
- }
10909
- const type = getSearchParameterType(searchParam, propertyType);
10910
- const result = { columnName, type, elementDefinition, array };
10911
- setSearchParamterDetails(resourceType, code, result);
10912
- return result;
10913
- }
10914
- /**
10915
- * Converts a hyphen-delimited code to camelCase string.
10916
- * @param code The search parameter code.
10917
- * @returns The SQL column name.
10918
- */
10919
- function convertCodeToColumnName(code) {
10920
- return code.split('-').reduce((result, word, index) => result + (index ? capitalize(word) : word), '');
10921
- }
10922
- function getSearchParameterType(searchParam, propertyType) {
10923
- let type = exports.SearchParameterType.TEXT;
10924
- switch (searchParam.type) {
10925
- case 'date':
10926
- if (propertyType === exports.PropertyType.dateTime || propertyType === exports.PropertyType.instant) {
10927
- type = exports.SearchParameterType.DATETIME;
10928
- }
10929
- else {
10930
- type = exports.SearchParameterType.DATE;
10931
- }
10932
- break;
10933
- case 'number':
10934
- type = exports.SearchParameterType.NUMBER;
10935
- break;
10936
- case 'quantity':
10937
- type = exports.SearchParameterType.QUANTITY;
10938
- break;
10939
- case 'reference':
10940
- type = exports.SearchParameterType.REFERENCE;
10941
- break;
10942
- case 'token':
10943
- if (propertyType === 'boolean') {
10944
- type = exports.SearchParameterType.BOOLEAN;
10945
- }
10946
- break;
10947
- }
10948
- return type;
10949
- }
10950
- function getExpressionForResourceType(resourceType, expression) {
10951
- const expressions = expression.split(' | ');
10952
- for (const e of expressions) {
10953
- if (isIgnoredExpression(e)) {
10954
- continue;
10955
- }
10956
- const simplified = simplifyExpression(e);
10957
- if (simplified.startsWith(resourceType + '.')) {
10958
- return simplified;
10959
- }
10960
- }
10961
- return undefined;
10962
- }
10963
- function isIgnoredExpression(input) {
10964
- return input.includes(' as Period') || input.includes(' as SampledDate');
10965
- }
10966
- function simplifyExpression(input) {
10967
- let result = input.trim();
10968
- if (result.startsWith('(') && result.endsWith(')')) {
10969
- result = result.substring(1, result.length - 1);
10970
- }
10971
- if (result.includes('[0]')) {
10972
- result = result.replaceAll('[0]', '');
10973
- }
10974
- const stopStrings = [' != ', ' as ', '.as(', '.exists(', '.where('];
10975
- for (const stopString of stopStrings) {
10976
- if (result.includes(stopString)) {
10977
- result = result.substring(0, result.indexOf(stopString));
10978
- }
10979
- }
10980
- return result;
10981
- }
10982
-
10983
- /**
10984
- * Determines if the resource matches the search request.
10985
- * @param resource The resource that was created or updated.
10986
- * @param searchRequest The subscription criteria as a search request.
10987
- * @returns True if the resource satisfies the search request.
10988
- */
10989
- function matchesSearchRequest(resource, searchRequest) {
10990
- if (searchRequest.resourceType !== resource.resourceType) {
10991
- return false;
10992
- }
10993
- if (searchRequest.filters) {
10994
- for (const filter of searchRequest.filters) {
10995
- if (!matchesSearchFilter(resource, searchRequest, filter)) {
10996
- return false;
10997
- }
10998
- }
10999
- }
11000
- return true;
11001
- }
11002
- /**
11003
- * Determines if the resource matches the search filter.
11004
- * @param resource The resource that was created or updated.
11005
- * @param filter One of the filters of a subscription criteria.
11006
- * @returns True if the resource satisfies the search filter.
11007
- */
11008
- function matchesSearchFilter(resource, searchRequest, filter) {
11009
- const searchParam = globalSchema.types[searchRequest.resourceType]?.searchParams?.[filter.code];
11010
- switch (searchParam?.type) {
11011
- case 'reference':
11012
- return matchesReferenceFilter(resource, filter, searchParam);
11013
- case 'string':
11014
- return matchesStringFilter(resource, filter, searchParam);
11015
- case 'token':
11016
- return matchesTokenFilter(resource, filter, searchParam);
11017
- case 'date':
11018
- return matchesDateFilter(resource, filter, searchParam);
11019
- }
11020
- // Unknown search parameter or search parameter type
11021
- // Default fail the check
11022
- return false;
11023
- }
11024
- function matchesReferenceFilter(resource, filter, searchParam) {
11025
- const values = evalFhirPath(searchParam.expression, resource);
11026
- const negated = isNegated(filter.operator);
11027
- if (filter.value === '' && values.length === 0) {
11028
- // If the filter operator is "equals", then the filter matches.
11029
- // If the filter operator is "not equals", then the filter does not match.
11030
- return filter.operator === exports.Operator.EQUALS;
11356
+ function matchesReferenceFilter(resource, filter, searchParam) {
11357
+ const values = evalFhirPath(searchParam.expression, resource);
11358
+ const negated = isNegated(filter.operator);
11359
+ if (filter.value === '' && values.length === 0) {
11360
+ // If the filter operator is "equals", then the filter matches.
11361
+ // If the filter operator is "not equals", then the filter does not match.
11362
+ return filter.operator === exports.Operator.EQUALS;
11031
11363
  }
11032
11364
  // Normalize the values array into reference strings
11033
11365
  const references = values.map((value) => (typeof value === 'string' ? value : value.reference));
@@ -11129,207 +11461,216 @@
11129
11461
  return operator === exports.Operator.NOT_EQUALS || operator === exports.Operator.NOT;
11130
11462
  }
11131
11463
 
11132
- const OK_ID = 'ok';
11133
- const CREATED_ID = 'created';
11134
- const GONE_ID = 'gone';
11135
- const NOT_MODIFIED_ID = 'not-modified';
11136
- const NOT_FOUND_ID = 'not-found';
11137
- const UNAUTHORIZED_ID = 'unauthorized';
11138
- const FORBIDDEN_ID = 'forbidden';
11139
- const TOO_MANY_REQUESTS_ID = 'too-many-requests';
11140
- const allOk = {
11141
- resourceType: 'OperationOutcome',
11142
- id: OK_ID,
11143
- issue: [
11144
- {
11145
- severity: 'information',
11146
- code: 'informational',
11147
- details: {
11148
- text: 'All OK',
11149
- },
11150
- },
11151
- ],
11152
- };
11153
- const created = {
11154
- resourceType: 'OperationOutcome',
11155
- id: CREATED_ID,
11156
- issue: [
11157
- {
11158
- severity: 'information',
11159
- code: 'informational',
11160
- details: {
11161
- text: 'Created',
11162
- },
11163
- },
11164
- ],
11165
- };
11166
- const notModified = {
11167
- resourceType: 'OperationOutcome',
11168
- id: NOT_MODIFIED_ID,
11169
- issue: [
11170
- {
11171
- severity: 'information',
11172
- code: 'informational',
11173
- details: {
11174
- text: 'Not Modified',
11175
- },
11176
- },
11177
- ],
11178
- };
11179
- const notFound = {
11180
- resourceType: 'OperationOutcome',
11181
- id: NOT_FOUND_ID,
11182
- issue: [
11183
- {
11184
- severity: 'error',
11185
- code: 'not-found',
11186
- details: {
11187
- text: 'Not found',
11188
- },
11189
- },
11190
- ],
11191
- };
11192
- const unauthorized = {
11193
- resourceType: 'OperationOutcome',
11194
- id: UNAUTHORIZED_ID,
11195
- issue: [
11196
- {
11197
- severity: 'error',
11198
- code: 'login',
11199
- details: {
11200
- text: 'Unauthorized',
11201
- },
11202
- },
11203
- ],
11204
- };
11205
- const forbidden = {
11206
- resourceType: 'OperationOutcome',
11207
- id: FORBIDDEN_ID,
11208
- issue: [
11209
- {
11210
- severity: 'error',
11211
- code: 'forbidden',
11212
- details: {
11213
- text: 'Forbidden',
11214
- },
11215
- },
11216
- ],
11217
- };
11218
- const gone = {
11219
- resourceType: 'OperationOutcome',
11220
- id: GONE_ID,
11221
- issue: [
11222
- {
11223
- severity: 'error',
11224
- code: 'deleted',
11225
- details: {
11226
- text: 'Gone',
11227
- },
11228
- },
11229
- ],
11464
+ var _SearchParser_instances, _SearchParser_parseKeyValue, _SearchParser_parseSortRule, _SearchParser_parseParameter, _SearchParser_parsePrefixType, _SearchParser_parseModifierType, _SearchParser_parseReference, _SearchParser_parseQuantity, _SearchParser_parseUnknownParameter;
11465
+ /**
11466
+ * Parses a FHIR search query.
11467
+ * See: https://www.hl7.org/fhir/search.html
11468
+ */
11469
+ /**
11470
+ * For the ordered parameter types of number, date, and quantity,
11471
+ * a prefix to the parameter value may be used to control the nature
11472
+ * of the matching.
11473
+ * See: https://www.hl7.org/fhir/search.html#prefix
11474
+ */
11475
+ const prefixMap = {
11476
+ eq: exports.Operator.EQUALS,
11477
+ ne: exports.Operator.NOT_EQUALS,
11478
+ lt: exports.Operator.LESS_THAN,
11479
+ le: exports.Operator.LESS_THAN_OR_EQUALS,
11480
+ gt: exports.Operator.GREATER_THAN,
11481
+ ge: exports.Operator.GREATER_THAN_OR_EQUALS,
11482
+ sa: exports.Operator.STARTS_AFTER,
11483
+ eb: exports.Operator.ENDS_BEFORE,
11484
+ ap: exports.Operator.APPROXIMATELY,
11230
11485
  };
11231
- const tooManyRequests = {
11232
- resourceType: 'OperationOutcome',
11233
- id: TOO_MANY_REQUESTS_ID,
11234
- issue: [
11235
- {
11236
- severity: 'error',
11237
- code: 'throttled',
11238
- details: {
11239
- text: 'Too Many Requests',
11240
- },
11241
- },
11242
- ],
11486
+ /**
11487
+ * Parameter names may specify a modifier as a suffix.
11488
+ * The modifiers are separated from the parameter name by a colon.
11489
+ * See: https://www.hl7.org/fhir/search.html#modifiers
11490
+ */
11491
+ const modifierMap = {
11492
+ contains: exports.Operator.CONTAINS,
11493
+ exact: exports.Operator.EXACT,
11494
+ above: exports.Operator.ABOVE,
11495
+ below: exports.Operator.BELOW,
11496
+ text: exports.Operator.TEXT,
11497
+ not: exports.Operator.NOT,
11498
+ in: exports.Operator.IN,
11499
+ 'not-in': exports.Operator.NOT_IN,
11500
+ 'of-type': exports.Operator.OF_TYPE,
11243
11501
  };
11244
- function badRequest(details, expression) {
11245
- return {
11246
- resourceType: 'OperationOutcome',
11247
- issue: [
11248
- {
11249
- severity: 'error',
11250
- code: 'invalid',
11251
- details: {
11252
- text: details,
11253
- },
11254
- expression: expression ? [expression] : undefined,
11255
- },
11256
- ],
11257
- };
11258
- }
11259
- function isOk(outcome) {
11260
- return outcome.id === OK_ID || outcome.id === CREATED_ID || outcome.id === NOT_MODIFIED_ID;
11261
- }
11262
- function isNotFound(outcome) {
11263
- return outcome.id === NOT_FOUND_ID;
11264
- }
11265
- function isGone(outcome) {
11266
- return outcome.id === GONE_ID;
11502
+ /**
11503
+ * Parses a search URL into a search request.
11504
+ * @param resourceType The FHIR resource type.
11505
+ * @param query The collection of query string parameters.
11506
+ * @returns A parsed SearchRequest.
11507
+ */
11508
+ function parseSearchRequest(resourceType, query) {
11509
+ return new SearchParser(resourceType, query);
11267
11510
  }
11268
- function getStatus(outcome) {
11269
- if (outcome.id === OK_ID) {
11270
- return 200;
11511
+ /**
11512
+ * Parses a search URL into a search request.
11513
+ * @param url The search URL.
11514
+ * @returns A parsed SearchRequest.
11515
+ */
11516
+ function parseSearchUrl(url) {
11517
+ const resourceType = url.pathname.split('/').pop();
11518
+ return new SearchParser(resourceType, Object.fromEntries(url.searchParams.entries()));
11519
+ }
11520
+ class SearchParser {
11521
+ constructor(resourceType, query) {
11522
+ _SearchParser_instances.add(this);
11523
+ this.resourceType = resourceType;
11524
+ this.filters = [];
11525
+ this.sortRules = [];
11526
+ for (const [key, value] of Object.entries(query)) {
11527
+ if (Array.isArray(value)) {
11528
+ value.forEach((element) => __classPrivateFieldGet(this, _SearchParser_instances, "m", _SearchParser_parseKeyValue).call(this, key, element));
11529
+ }
11530
+ else {
11531
+ __classPrivateFieldGet(this, _SearchParser_instances, "m", _SearchParser_parseKeyValue).call(this, key, value ?? '');
11532
+ }
11533
+ }
11271
11534
  }
11272
- else if (outcome.id === CREATED_ID) {
11273
- return 201;
11535
+ }
11536
+ _SearchParser_instances = new WeakSet(), _SearchParser_parseKeyValue = function _SearchParser_parseKeyValue(key, value) {
11537
+ let code;
11538
+ let modifier;
11539
+ const colonIndex = key.indexOf(':');
11540
+ if (colonIndex >= 0) {
11541
+ code = key.substring(0, colonIndex);
11542
+ modifier = key.substring(colonIndex + 1);
11274
11543
  }
11275
- else if (outcome.id === NOT_MODIFIED_ID) {
11276
- return 304;
11544
+ else {
11545
+ code = key;
11546
+ modifier = '';
11277
11547
  }
11278
- else if (outcome.id === UNAUTHORIZED_ID) {
11279
- return 401;
11548
+ switch (code) {
11549
+ case '_sort':
11550
+ __classPrivateFieldGet(this, _SearchParser_instances, "m", _SearchParser_parseSortRule).call(this, value);
11551
+ break;
11552
+ case '_count':
11553
+ this.count = parseInt(value);
11554
+ break;
11555
+ case '_offset':
11556
+ this.offset = parseInt(value);
11557
+ break;
11558
+ case '_total':
11559
+ this.total = value;
11560
+ break;
11561
+ case '_summary':
11562
+ this.total = 'estimate';
11563
+ this.count = 0;
11564
+ break;
11565
+ case '_revinclude':
11566
+ this.revInclude = value;
11567
+ break;
11568
+ default: {
11569
+ const param = globalSchema.types[this.resourceType]?.searchParams?.[code];
11570
+ if (param) {
11571
+ __classPrivateFieldGet(this, _SearchParser_instances, "m", _SearchParser_parseParameter).call(this, param, modifier, value);
11572
+ }
11573
+ else {
11574
+ __classPrivateFieldGet(this, _SearchParser_instances, "m", _SearchParser_parseUnknownParameter).call(this, code, modifier, value);
11575
+ }
11576
+ }
11280
11577
  }
11281
- else if (outcome.id === FORBIDDEN_ID) {
11282
- return 403;
11578
+ }, _SearchParser_parseSortRule = function _SearchParser_parseSortRule(value) {
11579
+ for (const field of value.split(',')) {
11580
+ let code;
11581
+ let descending = false;
11582
+ if (field.startsWith('-')) {
11583
+ code = field.substring(1);
11584
+ descending = true;
11585
+ }
11586
+ else {
11587
+ code = field;
11588
+ }
11589
+ this.sortRules.push({ code, descending });
11283
11590
  }
11284
- else if (outcome.id === NOT_FOUND_ID) {
11285
- return 404;
11591
+ }, _SearchParser_parseParameter = function _SearchParser_parseParameter(searchParam, modifier, value) {
11592
+ if (modifier === 'missing') {
11593
+ this.filters.push({
11594
+ code: searchParam.code,
11595
+ operator: exports.Operator.MISSING,
11596
+ value,
11597
+ });
11598
+ return;
11286
11599
  }
11287
- else if (outcome.id === GONE_ID) {
11288
- return 410;
11600
+ switch (searchParam.type) {
11601
+ case 'number':
11602
+ case 'date':
11603
+ __classPrivateFieldGet(this, _SearchParser_instances, "m", _SearchParser_parsePrefixType).call(this, searchParam, value);
11604
+ break;
11605
+ case 'string':
11606
+ case 'token':
11607
+ case 'uri':
11608
+ __classPrivateFieldGet(this, _SearchParser_instances, "m", _SearchParser_parseModifierType).call(this, searchParam, modifier, value);
11609
+ break;
11610
+ case 'reference':
11611
+ __classPrivateFieldGet(this, _SearchParser_instances, "m", _SearchParser_parseReference).call(this, searchParam, value);
11612
+ break;
11613
+ case 'quantity':
11614
+ __classPrivateFieldGet(this, _SearchParser_instances, "m", _SearchParser_parseQuantity).call(this, searchParam, value);
11615
+ break;
11289
11616
  }
11290
- else if (outcome.id === TOO_MANY_REQUESTS_ID) {
11291
- return 429;
11617
+ }, _SearchParser_parsePrefixType = function _SearchParser_parsePrefixType(param, input) {
11618
+ const { operator, value } = parsePrefix(input);
11619
+ this.filters.push({
11620
+ code: param.code,
11621
+ operator,
11622
+ value,
11623
+ });
11624
+ }, _SearchParser_parseModifierType = function _SearchParser_parseModifierType(param, modifier, value) {
11625
+ this.filters.push({
11626
+ code: param.code,
11627
+ operator: parseModifier(modifier),
11628
+ value,
11629
+ });
11630
+ }, _SearchParser_parseReference = function _SearchParser_parseReference(param, value) {
11631
+ this.filters.push({
11632
+ code: param.code,
11633
+ operator: exports.Operator.EQUALS,
11634
+ value: value,
11635
+ });
11636
+ }, _SearchParser_parseQuantity = function _SearchParser_parseQuantity(param, input) {
11637
+ const [prefixNumber, unitSystem, unitCode] = input.split('|');
11638
+ const { operator, value } = parsePrefix(prefixNumber);
11639
+ this.filters.push({
11640
+ code: param.code,
11641
+ operator,
11642
+ value,
11643
+ unitSystem,
11644
+ unitCode,
11645
+ });
11646
+ }, _SearchParser_parseUnknownParameter = function _SearchParser_parseUnknownParameter(code, modifier, value) {
11647
+ let operator = exports.Operator.EQUALS;
11648
+ if (modifier) {
11649
+ operator = modifier;
11292
11650
  }
11293
11651
  else {
11294
- return 400;
11295
- }
11296
- }
11297
- /**
11298
- * Asserts that the operation completed successfully and that the resource is defined.
11299
- * @param outcome The operation outcome.
11300
- * @param resource The resource that may or may not have been returned.
11301
- */
11302
- function assertOk(outcome, resource) {
11303
- if (!isOk(outcome) || resource === undefined) {
11304
- throw new OperationOutcomeError(outcome);
11652
+ for (const prefix of Object.keys(prefixMap)) {
11653
+ if (value.match(new RegExp('^' + prefix + '\\d'))) {
11654
+ operator = prefix;
11655
+ value = value.substring(prefix.length);
11656
+ }
11657
+ }
11305
11658
  }
11306
- }
11307
- class OperationOutcomeError extends Error {
11308
- constructor(outcome) {
11309
- super(outcome?.issue?.[0].details?.text);
11310
- this.outcome = outcome;
11659
+ this.filters.push({
11660
+ code,
11661
+ operator,
11662
+ value,
11663
+ });
11664
+ };
11665
+ function parsePrefix(input) {
11666
+ const prefix = input.substring(0, 2);
11667
+ if (prefix in prefixMap) {
11668
+ return { operator: prefixMap[prefix], value: input.substring(2) };
11311
11669
  }
11670
+ return { operator: exports.Operator.EQUALS, value: input };
11312
11671
  }
11313
- /**
11314
- * Normalizes an error object into a displayable error string.
11315
- * @param error The error value which could be a string, Error, OperationOutcome, or other unknown type.
11316
- * @returns A display string for the error.
11317
- */
11318
- function normalizeErrorString(error) {
11319
- if (!error) {
11320
- return 'Unknown error';
11321
- }
11322
- if (typeof error === 'string') {
11323
- return error;
11324
- }
11325
- if (error instanceof Error) {
11326
- return error.message;
11327
- }
11328
- if (typeof error === 'object' && 'resourceType' in error) {
11329
- const outcome = error;
11330
- return outcome.issue?.[0]?.details?.text ?? 'Unknown error';
11331
- }
11332
- return JSON.stringify(error);
11672
+ function parseModifier(modifier) {
11673
+ return modifierMap[modifier] || exports.Operator.EQUALS;
11333
11674
  }
11334
11675
 
11335
11676
  exports.AndAtom = AndAtom;
@@ -11377,7 +11718,6 @@
11377
11718
  exports.calculateAgeString = calculateAgeString;
11378
11719
  exports.capitalize = capitalize;
11379
11720
  exports.createReference = createReference;
11380
- exports.createSchema = createSchema;
11381
11721
  exports.created = created;
11382
11722
  exports.deepClone = deepClone;
11383
11723
  exports.deepEquals = deepEquals$1;
@@ -11421,7 +11761,10 @@
11421
11761
  exports.getPropertyDisplayName = getPropertyDisplayName;
11422
11762
  exports.getQuestionnaireAnswers = getQuestionnaireAnswers;
11423
11763
  exports.getReferenceString = getReferenceString;
11764
+ exports.getResourceTypeSchema = getResourceTypeSchema;
11765
+ exports.getResourceTypes = getResourceTypes;
11424
11766
  exports.getSearchParameterDetails = getSearchParameterDetails;
11767
+ exports.getSearchParameters = getSearchParameters;
11425
11768
  exports.getStatus = getStatus;
11426
11769
  exports.getTypedPropertyValue = getTypedPropertyValue;
11427
11770
  exports.globalSchema = globalSchema;
@@ -11437,10 +11780,12 @@
11437
11780
  exports.isNotFound = isNotFound;
11438
11781
  exports.isObject = isObject$1;
11439
11782
  exports.isOk = isOk;
11783
+ exports.isOperationOutcome = isOperationOutcome;
11440
11784
  exports.isPeriod = isPeriod;
11441
11785
  exports.isProfileResource = isProfileResource;
11442
11786
  exports.isQuantity = isQuantity;
11443
11787
  exports.isQuantityEquivalent = isQuantityEquivalent;
11788
+ exports.isResourceType = isResourceType;
11444
11789
  exports.isStringArray = isStringArray;
11445
11790
  exports.isUUID = isUUID;
11446
11791
  exports.isValidDate = isValidDate;
@@ -11452,6 +11797,8 @@
11452
11797
  exports.parseFhirPath = parseFhirPath;
11453
11798
  exports.parseJWTPayload = parseJWTPayload;
11454
11799
  exports.parseSearchDefinition = parseSearchDefinition;
11800
+ exports.parseSearchRequest = parseSearchRequest;
11801
+ exports.parseSearchUrl = parseSearchUrl;
11455
11802
  exports.preciseEquals = preciseEquals;
11456
11803
  exports.preciseGreaterThan = preciseGreaterThan;
11457
11804
  exports.preciseGreaterThanOrEquals = preciseGreaterThanOrEquals;