@medplum/core 2.0.24 → 2.0.26

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 (90) hide show
  1. package/dist/cjs/index.cjs +28 -14114
  2. package/dist/cjs/index.cjs.map +7 -1
  3. package/dist/esm/index.mjs +31 -29
  4. package/dist/esm/index.mjs.map +7 -1
  5. package/dist/types/client.d.ts +42 -10
  6. package/dist/types/fhirpath/atoms.d.ts +25 -11
  7. package/dist/types/fhirpath/parse.d.ts +33 -33
  8. package/dist/types/index.d.ts +1 -1
  9. package/dist/types/jwt.d.ts +6 -0
  10. package/dist/types/schema.d.ts +1 -0
  11. package/dist/types/search/details.d.ts +3 -1
  12. package/dist/types/types.d.ts +2 -2
  13. package/dist/types/typeschema/validation.d.ts +1 -1
  14. package/dist/types/utils.d.ts +15 -0
  15. package/package.json +5 -2
  16. package/dist/cjs/index.min.cjs +0 -1
  17. package/dist/esm/access.mjs +0 -142
  18. package/dist/esm/access.mjs.map +0 -1
  19. package/dist/esm/base-schema.json.mjs +0 -4408
  20. package/dist/esm/base-schema.json.mjs.map +0 -1
  21. package/dist/esm/base64.mjs +0 -33
  22. package/dist/esm/base64.mjs.map +0 -1
  23. package/dist/esm/bundle.mjs +0 -36
  24. package/dist/esm/bundle.mjs.map +0 -1
  25. package/dist/esm/cache.mjs +0 -64
  26. package/dist/esm/cache.mjs.map +0 -1
  27. package/dist/esm/client.mjs +0 -2168
  28. package/dist/esm/client.mjs.map +0 -1
  29. package/dist/esm/crypto.mjs +0 -22
  30. package/dist/esm/crypto.mjs.map +0 -1
  31. package/dist/esm/eventtarget.mjs +0 -36
  32. package/dist/esm/eventtarget.mjs.map +0 -1
  33. package/dist/esm/fhirlexer/parse.mjs +0 -122
  34. package/dist/esm/fhirlexer/parse.mjs.map +0 -1
  35. package/dist/esm/fhirlexer/tokenize.mjs +0 -231
  36. package/dist/esm/fhirlexer/tokenize.mjs.map +0 -1
  37. package/dist/esm/fhirmapper/parse.mjs +0 -329
  38. package/dist/esm/fhirmapper/parse.mjs.map +0 -1
  39. package/dist/esm/fhirmapper/tokenize.mjs +0 -13
  40. package/dist/esm/fhirmapper/tokenize.mjs.map +0 -1
  41. package/dist/esm/fhirpath/atoms.mjs +0 -347
  42. package/dist/esm/fhirpath/atoms.mjs.map +0 -1
  43. package/dist/esm/fhirpath/date.mjs +0 -24
  44. package/dist/esm/fhirpath/date.mjs.map +0 -1
  45. package/dist/esm/fhirpath/functions.mjs +0 -1626
  46. package/dist/esm/fhirpath/functions.mjs.map +0 -1
  47. package/dist/esm/fhirpath/parse.mjs +0 -145
  48. package/dist/esm/fhirpath/parse.mjs.map +0 -1
  49. package/dist/esm/fhirpath/tokenize.mjs +0 -10
  50. package/dist/esm/fhirpath/tokenize.mjs.map +0 -1
  51. package/dist/esm/fhirpath/utils.mjs +0 -377
  52. package/dist/esm/fhirpath/utils.mjs.map +0 -1
  53. package/dist/esm/filter/parse.mjs +0 -101
  54. package/dist/esm/filter/parse.mjs.map +0 -1
  55. package/dist/esm/filter/tokenize.mjs +0 -16
  56. package/dist/esm/filter/tokenize.mjs.map +0 -1
  57. package/dist/esm/filter/types.mjs +0 -34
  58. package/dist/esm/filter/types.mjs.map +0 -1
  59. package/dist/esm/format.mjs +0 -390
  60. package/dist/esm/format.mjs.map +0 -1
  61. package/dist/esm/hl7.mjs +0 -242
  62. package/dist/esm/hl7.mjs.map +0 -1
  63. package/dist/esm/index.min.mjs +0 -1
  64. package/dist/esm/jwt.mjs +0 -30
  65. package/dist/esm/jwt.mjs.map +0 -1
  66. package/dist/esm/node_modules/tslib/package.json +0 -1
  67. package/dist/esm/outcomes.mjs +0 -295
  68. package/dist/esm/outcomes.mjs.map +0 -1
  69. package/dist/esm/readablepromise.mjs +0 -82
  70. package/dist/esm/readablepromise.mjs.map +0 -1
  71. package/dist/esm/schema.mjs +0 -417
  72. package/dist/esm/schema.mjs.map +0 -1
  73. package/dist/esm/search/details.mjs +0 -162
  74. package/dist/esm/search/details.mjs.map +0 -1
  75. package/dist/esm/search/match.mjs +0 -166
  76. package/dist/esm/search/match.mjs.map +0 -1
  77. package/dist/esm/search/search.mjs +0 -378
  78. package/dist/esm/search/search.mjs.map +0 -1
  79. package/dist/esm/sftp.mjs +0 -24
  80. package/dist/esm/sftp.mjs.map +0 -1
  81. package/dist/esm/storage.mjs +0 -95
  82. package/dist/esm/storage.mjs.map +0 -1
  83. package/dist/esm/types.mjs +0 -370
  84. package/dist/esm/types.mjs.map +0 -1
  85. package/dist/esm/typeschema/types.mjs +0 -278
  86. package/dist/esm/typeschema/types.mjs.map +0 -1
  87. package/dist/esm/typeschema/validation.mjs +0 -262
  88. package/dist/esm/typeschema/validation.mjs.map +0 -1
  89. package/dist/esm/utils.mjs +0 -632
  90. package/dist/esm/utils.mjs.map +0 -1
@@ -1,2168 +0,0 @@
1
- import { encodeBase64 } from './base64.mjs';
2
- import { LRUCache } from './cache.mjs';
3
- import { getRandomString, encryptSHA256 } from './crypto.mjs';
4
- import { EventTarget } from './eventtarget.mjs';
5
- import { parseJWTPayload } from './jwt.mjs';
6
- import { isOperationOutcome, OperationOutcomeError, notFound, normalizeOperationOutcome, isOk, badRequest } from './outcomes.mjs';
7
- import { ReadablePromise } from './readablepromise.mjs';
8
- import { ClientStorage } from './storage.mjs';
9
- import { globalSchema, indexStructureDefinition, indexSearchParameter } from './types.mjs';
10
- import { createReference, arrayBufferToBase64 } from './utils.mjs';
11
-
12
- // PKCE auth based on:
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.24-8a2dc16" ;
15
- const DEFAULT_BASE_URL = 'https://api.medplum.com/';
16
- const DEFAULT_RESOURCE_CACHE_SIZE = 1000;
17
- const DEFAULT_CACHE_TIME = 60000; // 60 seconds
18
- const JSON_CONTENT_TYPE = 'application/json';
19
- const FHIR_CONTENT_TYPE = 'application/fhir+json';
20
- const PATCH_CONTENT_TYPE = 'application/json-patch+json';
21
- const system = { resourceType: 'Device', id: 'system', deviceName: [{ name: 'System' }] };
22
- /**
23
- * OAuth 2.0 Grant Type Identifiers
24
- * Standard identifiers defined here: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#name-grant-types
25
- * Token exchange extension defined here: https://datatracker.ietf.org/doc/html/rfc8693
26
- */
27
- var OAuthGrantType;
28
- (function (OAuthGrantType) {
29
- OAuthGrantType["ClientCredentials"] = "client_credentials";
30
- OAuthGrantType["AuthorizationCode"] = "authorization_code";
31
- OAuthGrantType["RefreshToken"] = "refresh_token";
32
- OAuthGrantType["TokenExchange"] = "urn:ietf:params:oauth:grant-type:token-exchange";
33
- })(OAuthGrantType || (OAuthGrantType = {}));
34
- /**
35
- * OAuth 2.0 Token Type Identifiers
36
- * See: https://datatracker.ietf.org/doc/html/rfc8693#name-token-type-identifiers
37
- */
38
- var OAuthTokenType;
39
- (function (OAuthTokenType) {
40
- /** Indicates that the token is an OAuth 2.0 access token issued by the given authorization server. */
41
- OAuthTokenType["AccessToken"] = "urn:ietf:params:oauth:token-type:access_token";
42
- /** Indicates that the token is an OAuth 2.0 refresh token issued by the given authorization server. */
43
- OAuthTokenType["RefreshToken"] = "urn:ietf:params:oauth:token-type:refresh_token";
44
- /** Indicates that the token is an ID Token as defined in Section 2 of [OpenID.Core]. */
45
- OAuthTokenType["IdToken"] = "urn:ietf:params:oauth:token-type:id_token";
46
- /** Indicates that the token is a base64url-encoded SAML 1.1 [OASIS.saml-core-1.1] assertion. */
47
- OAuthTokenType["Saml1Token"] = "urn:ietf:params:oauth:token-type:saml1";
48
- /** Indicates that the token is a base64url-encoded SAML 2.0 [OASIS.saml-core-2.0-os] assertion. */
49
- OAuthTokenType["Saml2Token"] = "urn:ietf:params:oauth:token-type:saml2";
50
- })(OAuthTokenType || (OAuthTokenType = {}));
51
- /**
52
- * The MedplumClient class provides a client for the Medplum FHIR server.
53
- *
54
- * The client can be used in the browser, in a Node.js application, or in a Medplum Bot.
55
- *
56
- * The client provides helpful methods for common operations such as:
57
- * 1) Authenticating
58
- * 2) Creating resources
59
- * 2) Reading resources
60
- * 3) Updating resources
61
- * 5) Deleting resources
62
- * 6) Searching
63
- * 7) Making GraphQL queries
64
- *
65
- * Here is a quick example of how to use the client:
66
- *
67
- * ```typescript
68
- * import { MedplumClient } from '@medplum/core';
69
- * const medplum = new MedplumClient();
70
- * ```
71
- *
72
- * Create a `Patient`:
73
- *
74
- * ```typescript
75
- * const patient = await medplum.createResource({
76
- * resourceType: 'Patient',
77
- * name: [{
78
- * given: ['Alice'],
79
- * family: 'Smith'
80
- * }]
81
- * });
82
- * ```
83
- *
84
- * Read a `Patient` by ID:
85
- *
86
- * ```typescript
87
- * const patient = await medplum.readResource('Patient', '123');
88
- * console.log(patient.name[0].given[0]);
89
- * ```
90
- *
91
- * Search for a `Patient` by name:
92
- *
93
- * ```typescript
94
- * const bundle = await medplum.search('Patient', 'name=Alice');
95
- * console.log(bundle.total);
96
- * ```
97
- *
98
- * <head>
99
- * <meta name="algolia:pageRank" content="100" />
100
- * </head>
101
- */
102
- class MedplumClient extends EventTarget {
103
- constructor(options) {
104
- super();
105
- if (options?.baseUrl) {
106
- if (!options.baseUrl.startsWith('http')) {
107
- throw new Error('Base URL must start with http or https');
108
- }
109
- }
110
- this.fetch = options?.fetch ?? getDefaultFetch();
111
- this.storage = options?.storage || new ClientStorage();
112
- this.createPdfImpl = options?.createPdf;
113
- this.baseUrl = ensureTrailingSlash(options?.baseUrl) || DEFAULT_BASE_URL;
114
- this.fhirBaseUrl = this.baseUrl + (ensureTrailingSlash(options?.fhirUrlPath) || 'fhir/R4/');
115
- this.clientId = options?.clientId || '';
116
- this.authorizeUrl = options?.authorizeUrl || this.baseUrl + 'oauth2/authorize';
117
- this.tokenUrl = options?.tokenUrl || this.baseUrl + 'oauth2/token';
118
- this.logoutUrl = options?.logoutUrl || this.baseUrl + 'oauth2/logout';
119
- this.onUnauthenticated = options?.onUnauthenticated;
120
- this.cacheTime = options?.cacheTime ?? DEFAULT_CACHE_TIME;
121
- if (this.cacheTime > 0) {
122
- this.requestCache = new LRUCache(options?.resourceCacheSize ?? DEFAULT_RESOURCE_CACHE_SIZE);
123
- }
124
- else {
125
- this.requestCache = undefined;
126
- }
127
- if (options?.autoBatchTime) {
128
- this.autoBatchTime = options.autoBatchTime ?? 0;
129
- this.autoBatchQueue = [];
130
- }
131
- else {
132
- this.autoBatchTime = 0;
133
- this.autoBatchQueue = undefined;
134
- }
135
- const activeLogin = this.getActiveLogin();
136
- if (activeLogin) {
137
- this.accessToken = activeLogin.accessToken;
138
- this.refreshToken = activeLogin.refreshToken;
139
- this.refreshProfile().catch(console.log);
140
- }
141
- this.setupStorageListener();
142
- }
143
- /**
144
- * Returns the current base URL for all API requests.
145
- * By default, this is set to `https://api.medplum.com/`.
146
- * This can be overridden by setting the `baseUrl` option when creating the client.
147
- * @category HTTP
148
- * @returns The current base URL for all API requests.
149
- */
150
- getBaseUrl() {
151
- return this.baseUrl;
152
- }
153
- /**
154
- * Returns the current authorize URL.
155
- * By default, this is set to `https://api.medplum.com/oauth2/authorize`.
156
- * This can be overridden by setting the `authorizeUrl` option when creating the client.
157
- * @category HTTP
158
- * @returns The current authorize URL.
159
- */
160
- getAuthorizeUrl() {
161
- return this.authorizeUrl;
162
- }
163
- /**
164
- * Clears all auth state including local storage and session storage.
165
- * @category Authentication
166
- */
167
- clear() {
168
- this.storage.clear();
169
- this.clearActiveLogin();
170
- }
171
- /**
172
- * Clears the active login from local storage.
173
- * Does not clear all local storage (such as other logins).
174
- * @category Authentication
175
- */
176
- clearActiveLogin() {
177
- if (this.basicAuth) {
178
- return;
179
- }
180
- this.storage.setString('activeLogin', undefined);
181
- this.requestCache?.clear();
182
- this.accessToken = undefined;
183
- this.refreshToken = undefined;
184
- this.sessionDetails = undefined;
185
- this.dispatchEvent({ type: 'change' });
186
- }
187
- /**
188
- * Invalidates any cached values or cached requests for the given URL.
189
- * @category Caching
190
- * @param url The URL to invalidate.
191
- */
192
- invalidateUrl(url) {
193
- url = url.toString();
194
- this.requestCache?.delete(url);
195
- }
196
- /**
197
- * Invalidates all cached values and flushes the cache.
198
- * @category Caching
199
- */
200
- invalidateAll() {
201
- this.requestCache?.clear();
202
- }
203
- /**
204
- * Invalidates all cached search results or cached requests for the given resourceType.
205
- * @category Caching
206
- * @param resourceType The resource type to invalidate.
207
- */
208
- invalidateSearches(resourceType) {
209
- const url = this.fhirBaseUrl + resourceType;
210
- if (this.requestCache) {
211
- for (const key of this.requestCache.keys()) {
212
- if (key.endsWith(url) || key.includes(url + '?')) {
213
- this.requestCache.delete(key);
214
- }
215
- }
216
- }
217
- }
218
- /**
219
- * Makes an HTTP GET request to the specified URL.
220
- *
221
- * This is a lower level method for custom requests.
222
- * For common operations, we recommend using higher level methods
223
- * such as `readResource()`, `search()`, etc.
224
- * @category HTTP
225
- * @param url The target URL.
226
- * @param options Optional fetch options.
227
- * @returns Promise to the response content.
228
- */
229
- get(url, options = {}) {
230
- url = url.toString();
231
- const cached = this.getCacheEntry(url, options);
232
- if (cached) {
233
- return cached.value;
234
- }
235
- let promise;
236
- if (url.startsWith(this.fhirBaseUrl) && this.autoBatchQueue) {
237
- promise = new Promise((resolve, reject) => {
238
- this.autoBatchQueue.push({
239
- method: 'GET',
240
- url: url.replace(this.fhirBaseUrl, ''),
241
- options,
242
- resolve,
243
- reject,
244
- });
245
- if (!this.autoBatchTimerId) {
246
- this.autoBatchTimerId = setTimeout(() => this.executeAutoBatch(), this.autoBatchTime);
247
- }
248
- });
249
- }
250
- else {
251
- promise = this.request('GET', url, options);
252
- }
253
- const readablePromise = new ReadablePromise(promise);
254
- this.setCacheEntry(url, readablePromise);
255
- return readablePromise;
256
- }
257
- /**
258
- * Makes an HTTP POST request to the specified URL.
259
- *
260
- * This is a lower level method for custom requests.
261
- * For common operations, we recommend using higher level methods
262
- * such as `createResource()`.
263
- * @category HTTP
264
- * @param url The target URL.
265
- * @param body The content body. Strings and `File` objects are passed directly. Other objects are converted to JSON.
266
- * @param contentType The content type to be included in the "Content-Type" header.
267
- * @param options Optional fetch options.
268
- * @returns Promise to the response content.
269
- */
270
- post(url, body, contentType, options = {}) {
271
- url = url.toString();
272
- if (body) {
273
- this.setRequestBody(options, body);
274
- }
275
- if (contentType) {
276
- this.setRequestContentType(options, contentType);
277
- }
278
- this.invalidateUrl(url);
279
- return this.request('POST', url, options);
280
- }
281
- /**
282
- * Makes an HTTP PUT request to the specified URL.
283
- *
284
- * This is a lower level method for custom requests.
285
- * For common operations, we recommend using higher level methods
286
- * such as `updateResource()`.
287
- * @category HTTP
288
- * @param url The target URL.
289
- * @param body The content body. Strings and `File` objects are passed directly. Other objects are converted to JSON.
290
- * @param contentType The content type to be included in the "Content-Type" header.
291
- * @param options Optional fetch options.
292
- * @returns Promise to the response content.
293
- */
294
- put(url, body, contentType, options = {}) {
295
- url = url.toString();
296
- if (body) {
297
- this.setRequestBody(options, body);
298
- }
299
- if (contentType) {
300
- this.setRequestContentType(options, contentType);
301
- }
302
- this.invalidateUrl(url);
303
- return this.request('PUT', url, options);
304
- }
305
- /**
306
- * Makes an HTTP PATCH request to the specified URL.
307
- *
308
- * This is a lower level method for custom requests.
309
- * For common operations, we recommend using higher level methods
310
- * such as `patchResource()`.
311
- * @category HTTP
312
- * @param url The target URL.
313
- * @param operations Array of JSONPatch operations.
314
- * @param options Optional fetch options.
315
- * @returns Promise to the response content.
316
- */
317
- patch(url, operations, options = {}) {
318
- url = url.toString();
319
- this.setRequestBody(options, operations);
320
- this.setRequestContentType(options, PATCH_CONTENT_TYPE);
321
- this.invalidateUrl(url);
322
- return this.request('PATCH', url, options);
323
- }
324
- /**
325
- * Makes an HTTP DELETE request to the specified URL.
326
- *
327
- *
328
- * This is a lower level method for custom requests.
329
- * For common operations, we recommend using higher level methods
330
- * such as `deleteResource()`.
331
- * @category HTTP
332
- * @param url The target URL.
333
- * @param options Optional fetch options.
334
- * @returns Promise to the response content.
335
- */
336
- delete(url, options) {
337
- url = url.toString();
338
- this.invalidateUrl(url);
339
- return this.request('DELETE', url, options);
340
- }
341
- /**
342
- * Initiates a new user flow.
343
- *
344
- * This method is part of the two different user registration flows:
345
- * 1) New Practitioner and new Project
346
- * 2) New Patient registration
347
- * @category Authentication
348
- * @param newUserRequest Register request including email and password.
349
- * @param options Optional fetch options.
350
- * @returns Promise to the authentication response.
351
- */
352
- async startNewUser(newUserRequest, options) {
353
- const { codeChallengeMethod, codeChallenge } = await this.startPkce();
354
- return this.post('auth/newuser', {
355
- ...newUserRequest,
356
- codeChallengeMethod,
357
- codeChallenge,
358
- }, undefined, options);
359
- }
360
- /**
361
- * Initiates a new project flow.
362
- *
363
- * This requires a partial login from `startNewUser` or `startNewGoogleUser`.
364
- * @param newProjectRequest Register request including email and password.
365
- * @param options Optional fetch options.
366
- * @returns Promise to the authentication response.
367
- */
368
- async startNewProject(newProjectRequest, options) {
369
- return this.post('auth/newproject', newProjectRequest, undefined, options);
370
- }
371
- /**
372
- * Initiates a new patient flow.
373
- *
374
- * This requires a partial login from `startNewUser` or `startNewGoogleUser`.
375
- * @param newPatientRequest Register request including email and password.
376
- * @param options Optional fetch options.
377
- * @returns Promise to the authentication response.
378
- */
379
- async startNewPatient(newPatientRequest, options) {
380
- return this.post('auth/newpatient', newPatientRequest, undefined, options);
381
- }
382
- /**
383
- * Initiates a user login flow.
384
- * @category Authentication
385
- * @param loginRequest Login request including email and password.
386
- * @param options Optional fetch options.
387
- * @returns Promise to the authentication response.
388
- */
389
- async startLogin(loginRequest, options) {
390
- return this.post('auth/login', {
391
- ...(await this.ensureCodeChallenge(loginRequest)),
392
- clientId: loginRequest.clientId ?? this.clientId,
393
- scope: loginRequest.scope,
394
- }, undefined, options);
395
- }
396
- /**
397
- * Tries to sign in with Google authentication.
398
- * The response parameter is the result of a Google authentication.
399
- * See: https://developers.google.com/identity/gsi/web/guides/handle-credential-responses-js-functions
400
- * @category Authentication
401
- * @param loginRequest Login request including Google credential response.
402
- * @param options Optional fetch options.
403
- * @returns Promise to the authentication response.
404
- */
405
- async startGoogleLogin(loginRequest, options) {
406
- return this.post('auth/google', {
407
- ...(await this.ensureCodeChallenge(loginRequest)),
408
- clientId: loginRequest.clientId ?? this.clientId,
409
- scope: loginRequest.scope,
410
- }, undefined, options);
411
- }
412
- /**
413
- * Returns the PKCE code challenge and method.
414
- * If the login request already includes a code challenge, it is returned.
415
- * Otherwise, a new PKCE code challenge is generated.
416
- * @category Authentication
417
- * @param loginRequest The original login request.
418
- * @returns The PKCE code challenge and method.
419
- */
420
- async ensureCodeChallenge(loginRequest) {
421
- if (loginRequest.codeChallenge) {
422
- return loginRequest;
423
- }
424
- return { ...loginRequest, ...(await this.startPkce()) };
425
- }
426
- /**
427
- * Signs out locally.
428
- * Does not invalidate tokens with the server.
429
- * @category Authentication
430
- */
431
- async signOut() {
432
- await this.post(this.logoutUrl, {});
433
- this.clear();
434
- }
435
- /**
436
- * Tries to sign in the user.
437
- * Returns true if the user is signed in.
438
- * This may result in navigating away to the sign in page.
439
- * @category Authentication
440
- * @param loginParams Optional login parameters.
441
- * @returns The user profile resource if available.
442
- */
443
- async signInWithRedirect(loginParams) {
444
- const urlParams = new URLSearchParams(window.location.search);
445
- const code = urlParams.get('code');
446
- if (!code) {
447
- await this.requestAuthorization(loginParams);
448
- return undefined;
449
- }
450
- else {
451
- return this.processCode(code);
452
- }
453
- }
454
- /**
455
- * Tries to sign out the user.
456
- * See: https://docs.aws.amazon.com/cognito/latest/developerguide/logout-endpoint.html
457
- * @category Authentication
458
- */
459
- signOutWithRedirect() {
460
- window.location.assign(this.logoutUrl);
461
- }
462
- /**
463
- * Initiates sign in with an external identity provider.
464
- * @param authorizeUrl The external authorization URL.
465
- * @param clientId The external client ID.
466
- * @param redirectUri The external identity provider redirect URI.
467
- * @param baseLogin The Medplum login request.
468
- * @category Authentication
469
- */
470
- async signInWithExternalAuth(authorizeUrl, clientId, redirectUri, baseLogin) {
471
- const loginRequest = await this.ensureCodeChallenge(baseLogin);
472
- window.location.assign(this.getExternalAuthRedirectUri(authorizeUrl, clientId, redirectUri, loginRequest));
473
- }
474
- /**
475
- * Exchange an external access token for a Medplum access token.
476
- * @param token The access token that was generated by the external identity provider.
477
- * @param clientId The ID of the `ClientApplication` in your Medplum project that will be making the exchange request.
478
- * @returns The user profile resource.
479
- * @category Authentication
480
- */
481
- async exchangeExternalAccessToken(token, clientId) {
482
- clientId = clientId || this.clientId;
483
- if (!clientId) {
484
- throw new Error('MedplumClient is missing clientId');
485
- }
486
- const formBody = new URLSearchParams();
487
- formBody.set('grant_type', OAuthGrantType.TokenExchange);
488
- formBody.set('subject_token_type', OAuthTokenType.AccessToken);
489
- formBody.set('client_id', clientId);
490
- formBody.set('subject_token', token);
491
- return this.fetchTokens(formBody);
492
- }
493
- /**
494
- * Builds the external identity provider redirect URI.
495
- * @param authorizeUrl The external authorization URL.
496
- * @param clientId The external client ID.
497
- * @param redirectUri The external identity provider redirect URI.
498
- * @param loginRequest The Medplum login request.
499
- * @returns The external identity provider redirect URI.
500
- * @category Authentication
501
- */
502
- getExternalAuthRedirectUri(authorizeUrl, clientId, redirectUri, loginRequest) {
503
- const url = new URL(authorizeUrl);
504
- url.searchParams.set('response_type', 'code');
505
- url.searchParams.set('client_id', clientId);
506
- url.searchParams.set('redirect_uri', redirectUri);
507
- url.searchParams.set('scope', 'openid profile email');
508
- url.searchParams.set('state', JSON.stringify(loginRequest));
509
- return url.toString();
510
- }
511
- /**
512
- * Builds a FHIR URL from a collection of URL path components.
513
- * For example, `buildUrl('/Patient', '123')` returns `fhir/R4/Patient/123`.
514
- * @category HTTP
515
- * @param path The path component of the URL.
516
- * @returns The well-formed FHIR URL.
517
- */
518
- fhirUrl(...path) {
519
- return new URL(this.fhirBaseUrl + path.join('/'));
520
- }
521
- /**
522
- * Builds a FHIR search URL from a search query or structured query object.
523
- * @category HTTP
524
- * @category Search
525
- * @param resourceType The FHIR resource type.
526
- * @param query The FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
527
- * @returns The well-formed FHIR URL.
528
- */
529
- fhirSearchUrl(resourceType, query) {
530
- const url = this.fhirUrl(resourceType);
531
- if (query) {
532
- url.search = new URLSearchParams(query).toString();
533
- }
534
- return url;
535
- }
536
- /**
537
- * Sends a FHIR search request.
538
- *
539
- * Example using a FHIR search string:
540
- *
541
- * ```typescript
542
- * const bundle = await client.search('Patient', 'name=Alice');
543
- * console.log(bundle);
544
- * ```
545
- *
546
- * The return value is a FHIR bundle:
547
- *
548
- * ```json
549
- * {
550
- * "resourceType": "Bundle",
551
- * "type": "searchset",
552
- * "entry": [
553
- * {
554
- * "resource": {
555
- * "resourceType": "Patient",
556
- * "name": [
557
- * {
558
- * "given": [
559
- * "George"
560
- * ],
561
- * "family": "Washington"
562
- * }
563
- * ],
564
- * }
565
- * }
566
- * ]
567
- * }
568
- * ```
569
- *
570
- * To query the count of a search, use the summary feature like so:
571
- *
572
- * ```typescript
573
- * const patients = medplum.search('Patient', '_summary=count');
574
- * ```
575
- *
576
- * See FHIR search for full details: https://www.hl7.org/fhir/search.html
577
- * @category Search
578
- * @param resourceType The FHIR resource type.
579
- * @param query Optional FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
580
- * @param options Optional fetch options.
581
- * @returns Promise to the search result bundle.
582
- */
583
- search(resourceType, query, options) {
584
- const url = this.fhirSearchUrl(resourceType, query);
585
- const cacheKey = url.toString() + '-search';
586
- const cached = this.getCacheEntry(cacheKey, options);
587
- if (cached) {
588
- return cached.value;
589
- }
590
- const promise = new ReadablePromise((async () => {
591
- const bundle = await this.get(url, options);
592
- if (bundle.entry) {
593
- for (const entry of bundle.entry) {
594
- this.cacheResource(entry.resource);
595
- }
596
- }
597
- return bundle;
598
- })());
599
- this.setCacheEntry(cacheKey, promise);
600
- return promise;
601
- }
602
- /**
603
- * Sends a FHIR search request for a single resource.
604
- *
605
- * This is a convenience method for `search()` that returns the first resource rather than a `Bundle`.
606
- *
607
- * Example using a FHIR search string:
608
- *
609
- * ```typescript
610
- * const patient = await client.searchOne('Patient', 'identifier=123');
611
- * console.log(patient);
612
- * ```
613
- *
614
- * The return value is the resource, if available; otherwise, undefined.
615
- *
616
- * See FHIR search for full details: https://www.hl7.org/fhir/search.html
617
- * @category Search
618
- * @param resourceType The FHIR resource type.
619
- * @param query Optional FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
620
- * @param options Optional fetch options.
621
- * @returns Promise to the first search result.
622
- */
623
- searchOne(resourceType, query, options) {
624
- const url = this.fhirSearchUrl(resourceType, query);
625
- url.searchParams.set('_count', '1');
626
- url.searchParams.sort();
627
- const cacheKey = url.toString() + '-searchOne';
628
- const cached = this.getCacheEntry(cacheKey, options);
629
- if (cached) {
630
- return cached.value;
631
- }
632
- const promise = new ReadablePromise(this.search(resourceType, url.searchParams, options).then((b) => b.entry?.[0]?.resource));
633
- this.setCacheEntry(cacheKey, promise);
634
- return promise;
635
- }
636
- /**
637
- * Sends a FHIR search request for an array of resources.
638
- *
639
- * This is a convenience method for `search()` that returns the resources as an array rather than a `Bundle`.
640
- *
641
- * Example using a FHIR search string:
642
- *
643
- * ```typescript
644
- * const patients = await client.searchResources('Patient', 'name=Alice');
645
- * console.log(patients);
646
- * ```
647
- *
648
- * The return value is an array of resources.
649
- *
650
- * See FHIR search for full details: https://www.hl7.org/fhir/search.html
651
- * @category Search
652
- * @param resourceType The FHIR resource type.
653
- * @param query Optional FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
654
- * @param options Optional fetch options.
655
- * @returns Promise to the array of search results.
656
- */
657
- searchResources(resourceType, query, options) {
658
- const url = this.fhirSearchUrl(resourceType, query);
659
- const cacheKey = url.toString() + '-searchResources';
660
- const cached = this.getCacheEntry(cacheKey, options);
661
- if (cached) {
662
- return cached.value;
663
- }
664
- const promise = new ReadablePromise(this.search(resourceType, query, options).then((b) => b.entry?.map((e) => e.resource) ?? []));
665
- this.setCacheEntry(cacheKey, promise);
666
- return promise;
667
- }
668
- /**
669
- * Creates an
670
- * [async generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator)
671
- * over a series of FHIR search requests for paginated search results. Each iteration of the generator yields
672
- * the array of resources on each page.
673
- *
674
- *
675
- * ```typescript
676
- * for await (const page of medplum.searchResourcePages('Patient', { _count: 10 })) {
677
- * for (const patient of page) {
678
- * console.log(`Processing Patient resource with ID: ${patient.id}`);
679
- * }
680
- * }
681
- * ```
682
- * @category Search
683
- * @param resourceType The FHIR resource type.
684
- * @param query Optional FHIR search query or structured query object. Can be any valid input to the URLSearchParams() constructor.
685
- * @param options Optional fetch options.
686
- * @yields An async generator, where each result is an array of resources for each page.
687
- */
688
- async *searchResourcePages(resourceType, query, options) {
689
- let url = this.fhirSearchUrl(resourceType, query);
690
- while (url) {
691
- const searchParams = new URL(url).searchParams;
692
- const bundle = await this.search(resourceType, searchParams, options);
693
- const nextLink = bundle.link?.find((link) => link.relation === 'next');
694
- if (!bundle.entry?.length && !nextLink) {
695
- break;
696
- }
697
- yield bundle.entry?.map((e) => e.resource) ?? [];
698
- url = nextLink?.url ? new URL(nextLink.url) : undefined;
699
- }
700
- }
701
- /**
702
- * Searches a ValueSet resource using the "expand" operation.
703
- * See: https://www.hl7.org/fhir/operation-valueset-expand.html
704
- * @category Search
705
- * @param system The ValueSet system url.
706
- * @param filter The search string.
707
- * @param options Optional fetch options.
708
- * @returns Promise to expanded ValueSet.
709
- */
710
- searchValueSet(system, filter, options) {
711
- const url = this.fhirUrl('ValueSet', '$expand');
712
- url.searchParams.set('url', system);
713
- url.searchParams.set('filter', filter);
714
- return this.get(url.toString(), options);
715
- }
716
- /**
717
- * Returns a cached resource if it is available.
718
- * @category Caching
719
- * @param resourceType The FHIR resource type.
720
- * @param id The FHIR resource ID.
721
- * @returns The resource if it is available in the cache; undefined otherwise.
722
- */
723
- getCached(resourceType, id) {
724
- const cached = this.requestCache?.get(this.fhirUrl(resourceType, id).toString())?.value;
725
- return cached?.isOk() ? cached.read() : undefined;
726
- }
727
- /**
728
- * Returns a cached resource if it is available.
729
- * @category Caching
730
- * @param reference The FHIR reference.
731
- * @returns The resource if it is available in the cache; undefined otherwise.
732
- */
733
- getCachedReference(reference) {
734
- const refString = reference.reference;
735
- if (!refString) {
736
- return undefined;
737
- }
738
- if (refString === 'system') {
739
- return system;
740
- }
741
- const [resourceType, id] = refString.split('/');
742
- if (!resourceType || !id) {
743
- return undefined;
744
- }
745
- return this.getCached(resourceType, id);
746
- }
747
- /**
748
- * Reads a resource by resource type and ID.
749
- *
750
- * Example:
751
- *
752
- * ```typescript
753
- * const patient = await medplum.readResource('Patient', '123');
754
- * console.log(patient);
755
- * ```
756
- *
757
- * See the FHIR "read" operation for full details: https://www.hl7.org/fhir/http.html#read
758
- * @category Read
759
- * @param resourceType The FHIR resource type.
760
- * @param id The resource ID.
761
- * @param options Optional fetch options.
762
- * @returns The resource if available; undefined otherwise.
763
- */
764
- readResource(resourceType, id, options) {
765
- return this.get(this.fhirUrl(resourceType, id), options);
766
- }
767
- /**
768
- * Reads a resource by `Reference`.
769
- *
770
- * This is a convenience method for `readResource()` that accepts a `Reference` object.
771
- *
772
- * Example:
773
- *
774
- * ```typescript
775
- * const serviceRequest = await medplum.readResource('ServiceRequest', '123');
776
- * const patient = await medplum.readReference(serviceRequest.subject);
777
- * console.log(patient);
778
- * ```
779
- *
780
- * See the FHIR "read" operation for full details: https://www.hl7.org/fhir/http.html#read
781
- * @category Read
782
- * @param reference The FHIR reference object.
783
- * @param options Optional fetch options.
784
- * @returns The resource if available; undefined otherwise.
785
- */
786
- readReference(reference, options) {
787
- const refString = reference.reference;
788
- if (!refString) {
789
- return new ReadablePromise(Promise.reject(new Error('Missing reference')));
790
- }
791
- if (refString === 'system') {
792
- return new ReadablePromise(Promise.resolve(system));
793
- }
794
- const [resourceType, id] = refString.split('/');
795
- if (!resourceType || !id) {
796
- return new ReadablePromise(Promise.reject(new Error('Invalid reference')));
797
- }
798
- return this.readResource(resourceType, id, options);
799
- }
800
- /**
801
- * Returns a cached schema for a resource type.
802
- * If the schema is not cached, returns undefined.
803
- * It is assumed that a client will call requestSchema before using this method.
804
- * @category Schema
805
- * @returns The schema if immediately available, undefined otherwise.
806
- * @deprecated Use globalSchema instead.
807
- */
808
- getSchema() {
809
- return globalSchema;
810
- }
811
- /**
812
- * Requests the schema for a resource type.
813
- * If the schema is already cached, the promise is resolved immediately.
814
- * @category Schema
815
- * @param resourceType The FHIR resource type.
816
- * @returns Promise to a schema with the requested resource type.
817
- */
818
- requestSchema(resourceType) {
819
- if (resourceType in globalSchema.types) {
820
- return Promise.resolve(globalSchema);
821
- }
822
- const cacheKey = resourceType + '-requestSchema';
823
- const cached = this.getCacheEntry(cacheKey, undefined);
824
- if (cached) {
825
- return cached.value;
826
- }
827
- const promise = new ReadablePromise((async () => {
828
- const query = `{
829
- StructureDefinitionList(name: "${resourceType}") {
830
- name,
831
- description,
832
- snapshot {
833
- element {
834
- id,
835
- path,
836
- min,
837
- max,
838
- type {
839
- code,
840
- targetProfile
841
- },
842
- binding {
843
- valueSet
844
- },
845
- definition
846
- }
847
- }
848
- }
849
- SearchParameterList(base: "${resourceType}", _count: 100) {
850
- base,
851
- code,
852
- type,
853
- expression,
854
- target
855
- }
856
- }`.replace(/\s+/g, ' ');
857
- const response = (await this.graphql(query));
858
- for (const structureDefinition of response.data.StructureDefinitionList) {
859
- indexStructureDefinition(structureDefinition);
860
- }
861
- for (const searchParameter of response.data.SearchParameterList) {
862
- indexSearchParameter(searchParameter);
863
- }
864
- return globalSchema;
865
- })());
866
- this.setCacheEntry(cacheKey, promise);
867
- return promise;
868
- }
869
- /**
870
- * Reads resource history by resource type and ID.
871
- *
872
- * The return value is a bundle of all versions of the resource.
873
- *
874
- * Example:
875
- *
876
- * ```typescript
877
- * const history = await medplum.readHistory('Patient', '123');
878
- * console.log(history);
879
- * ```
880
- *
881
- * See the FHIR "history" operation for full details: https://www.hl7.org/fhir/http.html#history
882
- * @category Read
883
- * @param resourceType The FHIR resource type.
884
- * @param id The resource ID.
885
- * @param options Optional fetch options.
886
- * @returns Promise to the resource history.
887
- */
888
- readHistory(resourceType, id, options) {
889
- return this.get(this.fhirUrl(resourceType, id, '_history'), options);
890
- }
891
- /**
892
- * Reads a specific version of a resource by resource type, ID, and version ID.
893
- *
894
- * Example:
895
- *
896
- * ```typescript
897
- * const version = await medplum.readVersion('Patient', '123', '456');
898
- * console.log(version);
899
- * ```
900
- *
901
- * See the FHIR "vread" operation for full details: https://www.hl7.org/fhir/http.html#vread
902
- * @category Read
903
- * @param resourceType The FHIR resource type.
904
- * @param id The resource ID.
905
- * @param vid The version ID.
906
- * @param options Optional fetch options.
907
- * @returns The resource if available; undefined otherwise.
908
- */
909
- readVersion(resourceType, id, vid, options) {
910
- return this.get(this.fhirUrl(resourceType, id, '_history', vid), options);
911
- }
912
- /**
913
- * Executes the Patient "everything" operation for a patient.
914
- *
915
- * Example:
916
- *
917
- * ```typescript
918
- * const bundle = await medplum.readPatientEverything('123');
919
- * console.log(bundle);
920
- * ```
921
- *
922
- * See the FHIR "patient-everything" operation for full details: https://hl7.org/fhir/operation-patient-everything.html
923
- * @category Read
924
- * @param id The Patient Id
925
- * @param options Optional fetch options.
926
- * @returns A Bundle of all Resources related to the Patient
927
- */
928
- readPatientEverything(id, options) {
929
- return this.get(this.fhirUrl('Patient', id, '$everything'), options);
930
- }
931
- /**
932
- * Creates a new FHIR resource.
933
- *
934
- * The return value is the newly created resource, including the ID and meta.
935
- *
936
- * Example:
937
- *
938
- * ```typescript
939
- * const result = await medplum.createResource({
940
- * resourceType: 'Patient',
941
- * name: [{
942
- * family: 'Smith',
943
- * given: ['John']
944
- * }]
945
- * });
946
- * console.log(result.id);
947
- * ```
948
- *
949
- * See the FHIR "create" operation for full details: https://www.hl7.org/fhir/http.html#create
950
- * @category Create
951
- * @param resource The FHIR resource to create.
952
- * @param options Optional fetch options.
953
- * @returns The result of the create operation.
954
- */
955
- createResource(resource, options) {
956
- if (!resource.resourceType) {
957
- throw new Error('Missing resourceType');
958
- }
959
- this.invalidateSearches(resource.resourceType);
960
- return this.post(this.fhirUrl(resource.resourceType), resource, undefined, options);
961
- }
962
- /**
963
- * Conditionally create a new FHIR resource only if some equivalent resource does not already exist on the server.
964
- *
965
- * The return value is the existing resource or the newly created resource, including the ID and meta.
966
- *
967
- * Example:
968
- *
969
- * ```typescript
970
- * const result = await medplum.createResourceIfNoneExist(
971
- * {
972
- * resourceType: 'Patient',
973
- * identifier: [{
974
- * system: 'http://example.com/mrn',
975
- * value: '123'
976
- * }]
977
- * name: [{
978
- * family: 'Smith',
979
- * given: ['John']
980
- * }]
981
- * },
982
- * 'identifier=123'
983
- * );
984
- * console.log(result.id);
985
- * ```
986
- *
987
- * This method is syntactic sugar for:
988
- *
989
- * ```typescript
990
- * return searchOne(resourceType, query) ?? createResource(resource);
991
- * ```
992
- *
993
- * The query parameter only contains the search parameters (what would be in the URL following the "?").
994
- *
995
- * See the FHIR "conditional create" operation for full details: https://www.hl7.org/fhir/http.html#ccreate
996
- * @category Create
997
- * @param resource The FHIR resource to create.
998
- * @param query The search query for an equivalent resource (should not include resource type or "?").
999
- * @param options Optional fetch options.
1000
- * @returns The result of the create operation.
1001
- */
1002
- async createResourceIfNoneExist(resource, query, options) {
1003
- return ((await this.searchOne(resource.resourceType, query, options)) ??
1004
- this.createResource(resource, options));
1005
- }
1006
- /**
1007
- * Creates a FHIR `Binary` resource with the provided data content.
1008
- *
1009
- * The return value is the newly created resource, including the ID and meta.
1010
- *
1011
- * The `data` parameter can be a string or a `File` object.
1012
- *
1013
- * A `File` object often comes from a `<input type="file">` element.
1014
- *
1015
- * Example:
1016
- *
1017
- * ```typescript
1018
- * const result = await medplum.createBinary(myFile, 'test.jpg', 'image/jpeg');
1019
- * console.log(result.id);
1020
- * ```
1021
- *
1022
- * See the FHIR "create" operation for full details: https://www.hl7.org/fhir/http.html#create
1023
- * @category Create
1024
- * @param data The binary data to upload.
1025
- * @param filename Optional filename for the binary.
1026
- * @param contentType Content type for the binary.
1027
- * @param onProgress Optional callback for progress events.
1028
- * @returns The result of the create operation.
1029
- */
1030
- createBinary(data, filename, contentType, onProgress) {
1031
- const url = this.fhirUrl('Binary');
1032
- if (filename) {
1033
- url.searchParams.set('_filename', filename);
1034
- }
1035
- if (onProgress) {
1036
- return this.uploadwithProgress(url, data, contentType, onProgress);
1037
- }
1038
- else {
1039
- return this.post(url, data, contentType);
1040
- }
1041
- }
1042
- uploadwithProgress(url, data, contentType, onProgress) {
1043
- return new Promise((resolve, reject) => {
1044
- const xhr = new XMLHttpRequest();
1045
- xhr.responseType = 'json';
1046
- xhr.onabort = () => reject(new Error('Request aborted'));
1047
- xhr.onerror = () => reject(new Error('Request error'));
1048
- if (onProgress) {
1049
- xhr.upload.onprogress = (e) => onProgress(e);
1050
- xhr.upload.onload = (e) => onProgress(e);
1051
- }
1052
- xhr.onload = () => {
1053
- if (xhr.status >= 200 && xhr.status < 300) {
1054
- resolve(xhr.response);
1055
- }
1056
- else {
1057
- reject(new Error(xhr.statusText));
1058
- }
1059
- };
1060
- xhr.open('POST', url);
1061
- xhr.withCredentials = true;
1062
- xhr.setRequestHeader('Authorization', 'Bearer ' + this.accessToken);
1063
- xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, max-age=0');
1064
- xhr.setRequestHeader('Content-Type', contentType);
1065
- xhr.setRequestHeader('X-Medplum', 'extended');
1066
- xhr.send(data);
1067
- });
1068
- }
1069
- /**
1070
- * Creates a PDF as a FHIR `Binary` resource based on pdfmake document definition.
1071
- *
1072
- * The return value is the newly created resource, including the ID and meta.
1073
- *
1074
- * The `docDefinition` parameter is a pdfmake document definition.
1075
- *
1076
- * Example:
1077
- *
1078
- * ```typescript
1079
- * const result = await medplum.createPdf({
1080
- * content: ['Hello world']
1081
- * });
1082
- * console.log(result.id);
1083
- * ```
1084
- *
1085
- * See the pdfmake document definition for full details: https://pdfmake.github.io/docs/0.1/document-definition-object/
1086
- * @category Media
1087
- * @param docDefinition The PDF document definition.
1088
- * @param filename Optional filename for the PDF binary resource.
1089
- * @param tableLayouts Optional pdfmake custom table layout.
1090
- * @param fonts Optional pdfmake custom font dictionary.
1091
- * @returns The result of the create operation.
1092
- */
1093
- async createPdf(docDefinition, filename, tableLayouts, fonts) {
1094
- if (!this.createPdfImpl) {
1095
- throw new Error('PDF creation not enabled');
1096
- }
1097
- const blob = await this.createPdfImpl(docDefinition, tableLayouts, fonts);
1098
- return this.createBinary(blob, filename, 'application/pdf');
1099
- }
1100
- /**
1101
- * Creates a FHIR `Communication` resource with the provided data content.
1102
- *
1103
- * This is a convenience method to handle commmon cases where a `Communication` resource is created with a `payload`.
1104
- * @category Create
1105
- * @param resource The FHIR resource to comment on.
1106
- * @param text The text of the comment.
1107
- * @param options Optional fetch options.
1108
- * @returns The result of the create operation.
1109
- */
1110
- createComment(resource, text, options) {
1111
- const profile = this.getProfile();
1112
- let encounter = undefined;
1113
- let subject = undefined;
1114
- if (resource.resourceType === 'Encounter') {
1115
- encounter = createReference(resource);
1116
- subject = resource.subject;
1117
- }
1118
- if (resource.resourceType === 'ServiceRequest') {
1119
- encounter = resource.encounter;
1120
- subject = resource.subject;
1121
- }
1122
- if (resource.resourceType === 'Patient') {
1123
- subject = createReference(resource);
1124
- }
1125
- return this.createResource({
1126
- resourceType: 'Communication',
1127
- basedOn: [createReference(resource)],
1128
- encounter,
1129
- subject,
1130
- sender: profile ? createReference(profile) : undefined,
1131
- sent: new Date().toISOString(),
1132
- payload: [{ contentString: text }],
1133
- }, options);
1134
- }
1135
- /**
1136
- * Updates a FHIR resource.
1137
- *
1138
- * The return value is the updated resource, including the ID and meta.
1139
- *
1140
- * Example:
1141
- *
1142
- * ```typescript
1143
- * const result = await medplum.updateResource({
1144
- * resourceType: 'Patient',
1145
- * id: '123',
1146
- * name: [{
1147
- * family: 'Smith',
1148
- * given: ['John']
1149
- * }]
1150
- * });
1151
- * console.log(result.meta.versionId);
1152
- * ```
1153
- *
1154
- * See the FHIR "update" operation for full details: https://www.hl7.org/fhir/http.html#update
1155
- * @category Write
1156
- * @param resource The FHIR resource to update.
1157
- * @param options Optional fetch options.
1158
- * @returns The result of the update operation.
1159
- */
1160
- async updateResource(resource, options) {
1161
- if (!resource.resourceType) {
1162
- throw new Error('Missing resourceType');
1163
- }
1164
- if (!resource.id) {
1165
- throw new Error('Missing id');
1166
- }
1167
- this.invalidateSearches(resource.resourceType);
1168
- let result = await this.put(this.fhirUrl(resource.resourceType, resource.id), resource, undefined, options);
1169
- if (!result) {
1170
- // On 304 not modified, result will be undefined
1171
- // Return the user input instead
1172
- // return result ?? resource;
1173
- result = resource;
1174
- }
1175
- this.cacheResource(result);
1176
- return result;
1177
- }
1178
- /**
1179
- * Updates a FHIR resource using JSONPatch operations.
1180
- *
1181
- * The return value is the updated resource, including the ID and meta.
1182
- *
1183
- * Example:
1184
- *
1185
- * ```typescript
1186
- * const result = await medplum.patchResource('Patient', '123', [
1187
- * {op: 'replace', path: '/name/0/family', value: 'Smith'},
1188
- * ]);
1189
- * console.log(result.meta.versionId);
1190
- * ```
1191
- *
1192
- * See the FHIR "update" operation for full details: https://www.hl7.org/fhir/http.html#patch
1193
- *
1194
- * See the JSONPatch specification for full details: https://tools.ietf.org/html/rfc6902
1195
- * @category Write
1196
- * @param resourceType The FHIR resource type.
1197
- * @param id The resource ID.
1198
- * @param operations The JSONPatch operations.
1199
- * @param options Optional fetch options.
1200
- * @returns The result of the patch operations.
1201
- */
1202
- patchResource(resourceType, id, operations, options) {
1203
- this.invalidateSearches(resourceType);
1204
- return this.patch(this.fhirUrl(resourceType, id), operations, options);
1205
- }
1206
- /**
1207
- * Deletes a FHIR resource by resource type and ID.
1208
- *
1209
- * Example:
1210
- *
1211
- * ```typescript
1212
- * await medplum.deleteResource('Patient', '123');
1213
- * ```
1214
- *
1215
- * See the FHIR "delete" operation for full details: https://www.hl7.org/fhir/http.html#delete
1216
- * @category Delete
1217
- * @param resourceType The FHIR resource type.
1218
- * @param id The resource ID.
1219
- * @param options Optional fetch options.
1220
- * @returns The result of the delete operation.
1221
- */
1222
- deleteResource(resourceType, id, options) {
1223
- this.deleteCacheEntry(this.fhirUrl(resourceType, id).toString());
1224
- this.invalidateSearches(resourceType);
1225
- return this.delete(this.fhirUrl(resourceType, id), options);
1226
- }
1227
- /**
1228
- * Executes the validate operation with the provided resource.
1229
- *
1230
- * Example:
1231
- *
1232
- * ```typescript
1233
- * const result = await medplum.validateResource({
1234
- * resourceType: 'Patient',
1235
- * name: [{ given: ['Alice'], family: 'Smith' }],
1236
- * });
1237
- * ```
1238
- *
1239
- * See the FHIR "$validate" operation for full details: https://www.hl7.org/fhir/resource-operation-validate.html
1240
- * @param resource The FHIR resource.
1241
- * @param options Optional fetch options.
1242
- * @returns The validate operation outcome.
1243
- */
1244
- validateResource(resource, options) {
1245
- return this.post(this.fhirUrl(resource.resourceType, '$validate'), resource, undefined, options);
1246
- }
1247
- /**
1248
- * Executes a bot by ID or Identifier.
1249
- * @param idOrIdentifier The Bot ID or Identifier.
1250
- * @param body The content body. Strings and `File` objects are passed directly. Other objects are converted to JSON.
1251
- * @param contentType The content type to be included in the "Content-Type" header.
1252
- * @param options Optional fetch options.
1253
- * @returns The Bot return value.
1254
- */
1255
- executeBot(idOrIdentifier, body, contentType, options) {
1256
- let url;
1257
- if (typeof idOrIdentifier === 'string') {
1258
- const id = idOrIdentifier;
1259
- url = this.fhirUrl('Bot', id, '$execute');
1260
- }
1261
- else {
1262
- const identifier = idOrIdentifier;
1263
- url = this.fhirUrl('Bot', '$execute') + `?identifier=${identifier.system}|${identifier.value}`;
1264
- }
1265
- return this.post(url, body, contentType, options);
1266
- }
1267
- /**
1268
- * Executes a batch or transaction of FHIR operations.
1269
- *
1270
- * Example:
1271
- *
1272
- * ```typescript
1273
- * await medplum.executeBatch({
1274
- * "resourceType": "Bundle",
1275
- * "type": "transaction",
1276
- * "entry": [
1277
- * {
1278
- * "fullUrl": "urn:uuid:61ebe359-bfdc-4613-8bf2-c5e300945f0a",
1279
- * "resource": {
1280
- * "resourceType": "Patient",
1281
- * "name": [{ "use": "official", "given": ["Alice"], "family": "Smith" }],
1282
- * "gender": "female",
1283
- * "birthDate": "1974-12-25"
1284
- * },
1285
- * "request": {
1286
- * "method": "POST",
1287
- * "url": "Patient"
1288
- * }
1289
- * },
1290
- * {
1291
- * "fullUrl": "urn:uuid:88f151c0-a954-468a-88bd-5ae15c08e059",
1292
- * "resource": {
1293
- * "resourceType": "Patient",
1294
- * "identifier": [{ "system": "http:/example.org/fhir/ids", "value": "234234" }],
1295
- * "name": [{ "use": "official", "given": ["Bob"], "family": "Jones" }],
1296
- * "gender": "male",
1297
- * "birthDate": "1974-12-25"
1298
- * },
1299
- * "request": {
1300
- * "method": "POST",
1301
- * "url": "Patient",
1302
- * "ifNoneExist": "identifier=http:/example.org/fhir/ids|234234"
1303
- * }
1304
- * }
1305
- * ]
1306
- * });
1307
- * ```
1308
- *
1309
- * See The FHIR "batch/transaction" section for full details: https://hl7.org/fhir/http.html#transaction
1310
- * @category Batch
1311
- * @param bundle The FHIR batch/transaction bundle.
1312
- * @param options Optional fetch options.
1313
- * @returns The FHIR batch/transaction response bundle.
1314
- */
1315
- executeBatch(bundle, options) {
1316
- return this.post(this.fhirBaseUrl.slice(0, -1), bundle, undefined, options);
1317
- }
1318
- /**
1319
- * Sends an email using the Medplum Email API.
1320
- *
1321
- * Builds the email using nodemailer MailComposer.
1322
- *
1323
- * Examples:
1324
- *
1325
- * Send a simple text email:
1326
- *
1327
- * ```typescript
1328
- * await medplum.sendEmail({
1329
- * to: 'alice@example.com',
1330
- * cc: 'bob@example.com',
1331
- * subject: 'Hello',
1332
- * text: 'Hello Alice',
1333
- * });
1334
- * ```
1335
- *
1336
- * Send an email with a `Binary` attachment:
1337
- *
1338
- * ```typescript
1339
- * await medplum.sendEmail({
1340
- * to: 'alice@example.com',
1341
- * subject: 'Email with attachment',
1342
- * text: 'See the attached report',
1343
- * attachments: [{
1344
- * filename: 'report.pdf',
1345
- * path: "Binary/" + binary.id
1346
- * }]
1347
- * });
1348
- * ```
1349
- *
1350
- * See options here: https://nodemailer.com/extras/mailcomposer/
1351
- * @category Media
1352
- * @param email The MailComposer options.
1353
- * @param options Optional fetch options.
1354
- * @returns Promise to the operation outcome.
1355
- */
1356
- sendEmail(email, options) {
1357
- return this.post('email/v1/send', email, 'application/json', options);
1358
- }
1359
- /**
1360
- * Executes a GraphQL query.
1361
- *
1362
- * Example:
1363
- *
1364
- * ```typescript
1365
- * const result = await medplum.graphql(`{
1366
- * Patient(id: "123") {
1367
- * resourceType
1368
- * id
1369
- * name {
1370
- * given
1371
- * family
1372
- * }
1373
- * }
1374
- * }`);
1375
- * ```
1376
- *
1377
- * Advanced queries such as named operations and variable substitution are supported:
1378
- *
1379
- * ```typescript
1380
- * const result = await medplum.graphql(
1381
- * `query GetPatientById($patientId: ID!) {
1382
- * Patient(id: $patientId) {
1383
- * resourceType
1384
- * id
1385
- * name {
1386
- * given
1387
- * family
1388
- * }
1389
- * }
1390
- * }`,
1391
- * 'GetPatientById',
1392
- * { patientId: '123' }
1393
- * );
1394
- * ```
1395
- *
1396
- * See the GraphQL documentation for more details: https://graphql.org/learn/
1397
- *
1398
- * See the FHIR GraphQL documentation for FHIR specific details: https://www.hl7.org/fhir/graphql.html
1399
- * @category Read
1400
- * @param query The GraphQL query.
1401
- * @param operationName Optional GraphQL operation name.
1402
- * @param variables Optional GraphQL variables.
1403
- * @param options Optional fetch options.
1404
- * @returns The GraphQL result.
1405
- */
1406
- graphql(query, operationName, variables, options) {
1407
- return this.post(this.fhirUrl('$graphql'), { query, operationName, variables }, JSON_CONTENT_TYPE, options);
1408
- }
1409
- /**
1410
- *
1411
- * Executes the $graph operation on this resource to fetch a Bundle of resources linked to the target resource
1412
- * according to a graph definition
1413
- * @category Read
1414
- * @param resourceType The FHIR resource type.
1415
- * @param id The resource ID.
1416
- * @param graphName `name` parameter of the GraphDefinition
1417
- * @param options Optional fetch options.
1418
- * @returns A Bundle
1419
- */
1420
- readResourceGraph(resourceType, id, graphName, options) {
1421
- return this.get(`${this.fhirUrl(resourceType, id)}/$graph?graph=${graphName}`, options);
1422
- }
1423
- /**
1424
- * @category Authentication
1425
- * @returns The Login State
1426
- */
1427
- getActiveLogin() {
1428
- return this.storage.getObject('activeLogin');
1429
- }
1430
- /**
1431
- * Sets the active login.
1432
- * @param login The new active login state.
1433
- * @category Authentication
1434
- */
1435
- async setActiveLogin(login) {
1436
- this.clearActiveLogin();
1437
- this.accessToken = login.accessToken;
1438
- if (this.basicAuth) {
1439
- return;
1440
- }
1441
- this.refreshToken = login.refreshToken;
1442
- this.storage.setObject('activeLogin', login);
1443
- this.addLogin(login);
1444
- this.refreshPromise = undefined;
1445
- await this.refreshProfile();
1446
- }
1447
- /**
1448
- * Returns the current access token.
1449
- * @returns The current access token.
1450
- * @category Authentication
1451
- */
1452
- getAccessToken() {
1453
- return this.accessToken;
1454
- }
1455
- /**
1456
- * Sets the current access token.
1457
- * @param accessToken The new access token.
1458
- * @category Authentication
1459
- */
1460
- setAccessToken(accessToken) {
1461
- this.accessToken = accessToken;
1462
- this.refreshToken = undefined;
1463
- this.sessionDetails = undefined;
1464
- }
1465
- /**
1466
- * Returns the list of available logins.
1467
- * @returns The list of available logins.
1468
- * @category Authentication
1469
- */
1470
- getLogins() {
1471
- return this.storage.getObject('logins') ?? [];
1472
- }
1473
- addLogin(newLogin) {
1474
- const logins = this.getLogins().filter((login) => login.profile?.reference !== newLogin.profile?.reference);
1475
- logins.push(newLogin);
1476
- this.storage.setObject('logins', logins);
1477
- }
1478
- async refreshProfile() {
1479
- this.profilePromise = new Promise((resolve, reject) => {
1480
- if (this.basicAuth) {
1481
- return;
1482
- }
1483
- this.get('auth/me')
1484
- .then((result) => {
1485
- this.profilePromise = undefined;
1486
- this.sessionDetails = result;
1487
- this.dispatchEvent({ type: 'change' });
1488
- resolve(result.profile);
1489
- })
1490
- .catch(reject);
1491
- });
1492
- return this.profilePromise;
1493
- }
1494
- /**
1495
- * Returns true if the client is waiting for authentication.
1496
- * @returns True if the client is waiting for authentication.
1497
- * @category Authentication
1498
- */
1499
- isLoading() {
1500
- return !!this.profilePromise;
1501
- }
1502
- /**
1503
- * Returns true if the current user is authenticated as a super admin.
1504
- * @returns True if the current user is authenticated as a super admin.
1505
- * @category Authentication
1506
- */
1507
- isSuperAdmin() {
1508
- return !!this.sessionDetails?.project.superAdmin;
1509
- }
1510
- /**
1511
- * Returns true if the current user is authenticated as a project admin.
1512
- * @returns True if the current user is authenticated as a project admin.
1513
- * @category Authentication
1514
- */
1515
- isProjectAdmin() {
1516
- return !!this.sessionDetails?.membership.admin;
1517
- }
1518
- /**
1519
- * Returns the current project if available.
1520
- * @returns The current project if available.
1521
- * @category User Profile
1522
- */
1523
- getProject() {
1524
- return this.sessionDetails?.project;
1525
- }
1526
- /**
1527
- * Returns the current project membership if available.
1528
- * @returns The current project membership if available.
1529
- * @category User Profile
1530
- */
1531
- getProjectMembership() {
1532
- return this.sessionDetails?.membership;
1533
- }
1534
- /**
1535
- * Returns the current user profile resource if available.
1536
- * This method does not wait for loading promises.
1537
- * @returns The current user profile resource if available.
1538
- * @category User Profile
1539
- */
1540
- getProfile() {
1541
- return this.sessionDetails?.profile;
1542
- }
1543
- /**
1544
- * Returns the current user profile resource if available.
1545
- * This method waits for loading promises.
1546
- * @returns The current user profile resource if available.
1547
- * @category User Profile
1548
- */
1549
- async getProfileAsync() {
1550
- if (this.profilePromise) {
1551
- await this.profilePromise;
1552
- }
1553
- return this.getProfile();
1554
- }
1555
- /**
1556
- * Returns the current user configuration if available.
1557
- * @returns The current user configuration if available.
1558
- * @category User Profile
1559
- */
1560
- getUserConfiguration() {
1561
- return this.sessionDetails?.config;
1562
- }
1563
- /**
1564
- * Returns the current user access policy if available.
1565
- * @returns The current user access policy if available.
1566
- * @category User Profile
1567
- */
1568
- getAccessPolicy() {
1569
- return this.sessionDetails?.accessPolicy;
1570
- }
1571
- /**
1572
- * Downloads the URL as a blob.
1573
- * @category Read
1574
- * @param url The URL to request.
1575
- * @param options Optional fetch request init options.
1576
- * @returns Promise to the response body as a blob.
1577
- */
1578
- async download(url, options = {}) {
1579
- if (this.refreshPromise) {
1580
- await this.refreshPromise;
1581
- }
1582
- this.addFetchOptionsDefaults(options);
1583
- const response = await this.fetch(url.toString(), options);
1584
- return response.blob();
1585
- }
1586
- /**
1587
- * Upload media to the server and create a Media instance for the uploaded content.
1588
- * @param contents The contents of the media file, as a string, Uint8Array, File, or Blob.
1589
- * @param contentType The media type of the content.
1590
- * @param filename The name of the file to be uploaded, or undefined if not applicable.
1591
- * @param additionalFields Additional fields for Media.
1592
- * @param options Optional fetch options.
1593
- * @returns Promise that resolves to the created Media
1594
- */
1595
- async uploadMedia(contents, contentType, filename, additionalFields, options) {
1596
- const binary = await this.createBinary(contents, filename, contentType);
1597
- return this.createResource({
1598
- ...additionalFields,
1599
- resourceType: 'Media',
1600
- content: {
1601
- contentType: contentType,
1602
- url: 'Binary/' + binary.id,
1603
- title: filename,
1604
- },
1605
- }, options);
1606
- }
1607
- /**
1608
- * 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
1609
- * @param exportLevel Optional export level. Defaults to system level export. 'Group/:id' - Group of Patients, 'Patient' - All Patients.
1610
- * @param resourceTypes A string of comma-delimited FHIR resource types.
1611
- * @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).
1612
- * @param options Optional fetch options.
1613
- * @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
1614
- */
1615
- async bulkExport(
1616
- //eslint-disable-next-line default-param-last
1617
- exportLevel = '', resourceTypes, since, options) {
1618
- const fhirPath = exportLevel ? `${exportLevel}/` : exportLevel;
1619
- const url = this.fhirUrl(`${fhirPath}$export`);
1620
- if (resourceTypes) {
1621
- url.searchParams.set('_type', resourceTypes);
1622
- }
1623
- if (since) {
1624
- url.searchParams.set('_since', since);
1625
- }
1626
- return this.startAsyncRequest(url.toString(), options);
1627
- }
1628
- /**
1629
- * Starts an async request following the FHIR "Asynchronous Request Pattern".
1630
- * See: https://hl7.org/fhir/r4/async.html
1631
- * @param url The URL to request.
1632
- * @param options Optional fetch options.
1633
- * @returns The response body.
1634
- */
1635
- async startAsyncRequest(url, options = {}) {
1636
- this.addFetchOptionsDefaults(options);
1637
- const headers = options.headers;
1638
- headers['Prefer'] = 'respond-async';
1639
- const response = await this.fetchWithRetry(url, options);
1640
- if (response.status === 202) {
1641
- // Accepted content location can come from multiple sources
1642
- // The authoritative source is the "Content-Location" HTTP header.
1643
- const contentLocation = response.headers.get('content-location');
1644
- if (contentLocation) {
1645
- return this.pollStatus(contentLocation);
1646
- }
1647
- // However, "Content-Location" may not be available due to CORS limitations.
1648
- // In this case, we use the OperationOutcome.diagnostics field.
1649
- const body = await response.json();
1650
- if (isOperationOutcome(body) && body.issue?.[0]?.diagnostics) {
1651
- return this.pollStatus(body.issue[0].diagnostics);
1652
- }
1653
- }
1654
- return this.parseResponse(response, 'POST', url);
1655
- }
1656
- //
1657
- // Private helpers
1658
- //
1659
- /**
1660
- * Returns the cache entry if available and not expired.
1661
- * @param key The cache key to retrieve.
1662
- * @param options Optional fetch options for cache settings.
1663
- * @returns The cached entry if found.
1664
- */
1665
- getCacheEntry(key, options) {
1666
- if (!this.requestCache || options?.cache === 'no-cache' || options?.cache === 'reload') {
1667
- return undefined;
1668
- }
1669
- const entry = this.requestCache.get(key);
1670
- if (!entry || entry.requestTime + this.cacheTime < Date.now()) {
1671
- return undefined;
1672
- }
1673
- return entry;
1674
- }
1675
- /**
1676
- * Adds a readable promise to the cache.
1677
- * @param key The cache key to store.
1678
- * @param value The readable promise to store.
1679
- */
1680
- setCacheEntry(key, value) {
1681
- if (this.requestCache) {
1682
- this.requestCache.set(key, { requestTime: Date.now(), value });
1683
- }
1684
- }
1685
- /**
1686
- * Adds a concrete value as the cache entry for the given resource.
1687
- * This is used in cases where the resource is loaded indirectly.
1688
- * For example, when a resource is loaded as part of a Bundle.
1689
- * @param resource The resource to cache.
1690
- */
1691
- cacheResource(resource) {
1692
- if (resource?.id) {
1693
- this.setCacheEntry(this.fhirUrl(resource.resourceType, resource.id).toString(), new ReadablePromise(Promise.resolve(resource)));
1694
- }
1695
- }
1696
- /**
1697
- * Deletes a cache entry.
1698
- * @param key The cache key to delete.
1699
- */
1700
- deleteCacheEntry(key) {
1701
- if (this.requestCache) {
1702
- this.requestCache.delete(key);
1703
- }
1704
- }
1705
- /**
1706
- * Makes an HTTP request.
1707
- * @param method The HTTP method (GET, POST, etc).
1708
- * @param url The target URL.
1709
- * @param options Optional fetch request init options.
1710
- * @returns The JSON content body if available.
1711
- */
1712
- async request(method, url, options = {}) {
1713
- if (this.refreshPromise) {
1714
- await this.refreshPromise;
1715
- }
1716
- options.method = method;
1717
- this.addFetchOptionsDefaults(options);
1718
- const response = await this.fetchWithRetry(url, options);
1719
- return this.parseResponse(response, method, url, options);
1720
- }
1721
- async parseResponse(response, method, url, options = {}) {
1722
- if (response.status === 401) {
1723
- // Refresh and try again
1724
- return this.handleUnauthenticated(method, url, options);
1725
- }
1726
- if (response.status === 204 || response.status === 304) {
1727
- // No content or change
1728
- return undefined;
1729
- }
1730
- if (response.status === 404) {
1731
- const contentType = response.headers.get('content-type');
1732
- if (!contentType?.includes('application/fhir+json')) {
1733
- throw new OperationOutcomeError(notFound);
1734
- }
1735
- }
1736
- let obj = undefined;
1737
- try {
1738
- obj = await response.json();
1739
- }
1740
- catch (err) {
1741
- console.error('Error parsing response', response.status, err);
1742
- throw err;
1743
- }
1744
- if (response.status >= 400) {
1745
- throw new OperationOutcomeError(normalizeOperationOutcome(obj));
1746
- }
1747
- return obj;
1748
- }
1749
- async fetchWithRetry(url, options) {
1750
- if (!url.startsWith('http')) {
1751
- url = this.baseUrl + url;
1752
- }
1753
- const maxRetries = 3;
1754
- const retryDelay = 200;
1755
- let response = undefined;
1756
- for (let retry = 0; retry < maxRetries; retry++) {
1757
- try {
1758
- response = (await this.fetch(url, options));
1759
- if (response.status < 500) {
1760
- return response;
1761
- }
1762
- }
1763
- catch (err) {
1764
- this.retryCatch(retry, maxRetries, err);
1765
- }
1766
- await new Promise((resolve) => {
1767
- setTimeout(resolve, retryDelay);
1768
- });
1769
- }
1770
- return response;
1771
- }
1772
- async pollStatus(statusUrl) {
1773
- let checkStatus = true;
1774
- let resultResponse;
1775
- const retryDelay = 2000;
1776
- while (checkStatus) {
1777
- const fetchOptions = {};
1778
- this.addFetchOptionsDefaults(fetchOptions);
1779
- const statusResponse = await this.fetchWithRetry(statusUrl, fetchOptions);
1780
- if (statusResponse.status !== 202) {
1781
- checkStatus = false;
1782
- resultResponse = statusResponse;
1783
- }
1784
- await new Promise((resolve) => {
1785
- setTimeout(resolve, retryDelay);
1786
- });
1787
- }
1788
- return this.parseResponse(resultResponse, 'POST', statusUrl);
1789
- }
1790
- /**
1791
- * Executes a batch of requests that were automatically batched together.
1792
- */
1793
- async executeAutoBatch() {
1794
- // Get the current queue
1795
- const entries = [...this.autoBatchQueue];
1796
- // Clear the queue
1797
- this.autoBatchQueue.length = 0;
1798
- // Clear the timer
1799
- this.autoBatchTimerId = undefined;
1800
- // If there is only one request in the batch, just execute it
1801
- if (entries.length === 1) {
1802
- const entry = entries[0];
1803
- try {
1804
- entry.resolve(await this.request(entry.method, this.fhirBaseUrl + entry.url, entry.options));
1805
- }
1806
- catch (err) {
1807
- entry.reject(new OperationOutcomeError(normalizeOperationOutcome(err)));
1808
- }
1809
- return;
1810
- }
1811
- // Build the batch request
1812
- const batch = {
1813
- resourceType: 'Bundle',
1814
- type: 'batch',
1815
- entry: entries.map((e) => ({
1816
- request: {
1817
- method: e.method,
1818
- url: e.url,
1819
- },
1820
- resource: e.options.body ? JSON.parse(e.options.body) : undefined,
1821
- })),
1822
- };
1823
- // Execute the batch request
1824
- const response = (await this.post(this.fhirBaseUrl.slice(0, -1), batch));
1825
- // Process the response
1826
- for (let i = 0; i < entries.length; i++) {
1827
- const entry = entries[i];
1828
- const responseEntry = response.entry?.[i];
1829
- if (responseEntry?.response?.outcome && !isOk(responseEntry.response.outcome)) {
1830
- entry.reject(new OperationOutcomeError(responseEntry.response.outcome));
1831
- }
1832
- else {
1833
- entry.resolve(responseEntry?.resource);
1834
- }
1835
- }
1836
- }
1837
- /**
1838
- * Adds default options to the fetch options.
1839
- * @param options The options to add defaults to.
1840
- */
1841
- addFetchOptionsDefaults(options) {
1842
- let headers = options.headers;
1843
- if (!headers) {
1844
- headers = {};
1845
- options.headers = headers;
1846
- }
1847
- headers['Accept'] = FHIR_CONTENT_TYPE;
1848
- headers['X-Medplum'] = 'extended';
1849
- if (options.body && !headers['Content-Type']) {
1850
- headers['Content-Type'] = FHIR_CONTENT_TYPE;
1851
- }
1852
- if (this.accessToken) {
1853
- headers['Authorization'] = 'Bearer ' + this.accessToken;
1854
- }
1855
- else if (this.basicAuth) {
1856
- headers['Authorization'] = 'Basic ' + this.basicAuth;
1857
- }
1858
- if (!options.cache) {
1859
- options.cache = 'no-cache';
1860
- }
1861
- if (!options.credentials) {
1862
- options.credentials = 'include';
1863
- }
1864
- }
1865
- /**
1866
- * Sets the "Content-Type" header on fetch options.
1867
- * @param options The fetch options.
1868
- * @param contentType The new content type to set.
1869
- */
1870
- setRequestContentType(options, contentType) {
1871
- if (!options.headers) {
1872
- options.headers = {};
1873
- }
1874
- const headers = options.headers;
1875
- headers['Content-Type'] = contentType;
1876
- }
1877
- /**
1878
- * Sets the body on fetch options.
1879
- * @param options The fetch options.
1880
- * @param data The new content body.
1881
- */
1882
- setRequestBody(options, data) {
1883
- if (typeof data === 'string' ||
1884
- (typeof Blob !== 'undefined' && data instanceof Blob) ||
1885
- (typeof File !== 'undefined' && data instanceof File) ||
1886
- (typeof Uint8Array !== 'undefined' && data instanceof Uint8Array)) {
1887
- options.body = data;
1888
- }
1889
- else if (data) {
1890
- options.body = JSON.stringify(data);
1891
- }
1892
- }
1893
- /**
1894
- * Handles an unauthenticated response from the server.
1895
- * First, tries to refresh the access token and retry the request.
1896
- * Otherwise, calls unauthenticated callbacks and rejects.
1897
- * @param method The HTTP method of the original request.
1898
- * @param url The URL of the original request.
1899
- * @param options Optional fetch request init options.
1900
- * @returns The result of the retry.
1901
- */
1902
- handleUnauthenticated(method, url, options) {
1903
- if (this.refresh()) {
1904
- return this.request(method, url, options);
1905
- }
1906
- this.clearActiveLogin();
1907
- if (this.onUnauthenticated) {
1908
- this.onUnauthenticated();
1909
- }
1910
- return Promise.reject(new Error('Unauthenticated'));
1911
- }
1912
- /**
1913
- * Starts a new PKCE flow.
1914
- * These PKCE values are stateful, and must survive redirects and page refreshes.
1915
- * @category Authentication
1916
- * @returns The PKCE code challenge details.
1917
- */
1918
- async startPkce() {
1919
- const pkceState = getRandomString();
1920
- sessionStorage.setItem('pkceState', pkceState);
1921
- const codeVerifier = getRandomString();
1922
- sessionStorage.setItem('codeVerifier', codeVerifier);
1923
- const arrayHash = await encryptSHA256(codeVerifier);
1924
- const codeChallenge = arrayBufferToBase64(arrayHash).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
1925
- sessionStorage.setItem('codeChallenge', codeChallenge);
1926
- return { codeChallengeMethod: 'S256', codeChallenge };
1927
- }
1928
- /**
1929
- * Redirects the user to the login screen for authorization.
1930
- * Clears all auth state including local storage and session storage.
1931
- * @param loginParams The authorization login parameters.
1932
- * @see https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
1933
- */
1934
- async requestAuthorization(loginParams) {
1935
- const loginRequest = await this.ensureCodeChallenge(loginParams || {});
1936
- const url = new URL(this.authorizeUrl);
1937
- url.searchParams.set('response_type', 'code');
1938
- url.searchParams.set('state', sessionStorage.getItem('pkceState'));
1939
- url.searchParams.set('client_id', loginRequest.clientId || this.clientId);
1940
- url.searchParams.set('redirect_uri', loginRequest.redirectUri || getWindowOrigin());
1941
- url.searchParams.set('code_challenge_method', loginRequest.codeChallengeMethod);
1942
- url.searchParams.set('code_challenge', loginRequest.codeChallenge);
1943
- url.searchParams.set('scope', loginRequest.scope || 'openid profile');
1944
- window.location.assign(url.toString());
1945
- }
1946
- /**
1947
- * Processes an OAuth authorization code.
1948
- * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
1949
- * @param code The authorization code received by URL parameter.
1950
- * @param loginParams Optional login parameters.
1951
- * @returns The user profile resource.
1952
- * @category Authentication
1953
- */
1954
- processCode(code, loginParams) {
1955
- const formBody = new URLSearchParams();
1956
- formBody.set('grant_type', OAuthGrantType.AuthorizationCode);
1957
- formBody.set('code', code);
1958
- formBody.set('client_id', loginParams?.clientId || this.clientId);
1959
- formBody.set('redirect_uri', loginParams?.redirectUri || getWindowOrigin());
1960
- if (typeof sessionStorage !== 'undefined') {
1961
- const codeVerifier = sessionStorage.getItem('codeVerifier');
1962
- if (codeVerifier) {
1963
- formBody.set('code_verifier', codeVerifier);
1964
- }
1965
- }
1966
- return this.fetchTokens(formBody);
1967
- }
1968
- /**
1969
- * Tries to refresh the auth tokens.
1970
- * @returns The refresh promise if available; otherwise undefined.
1971
- * @see https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
1972
- */
1973
- refresh() {
1974
- if (this.refreshPromise) {
1975
- return this.refreshPromise;
1976
- }
1977
- if (this.refreshToken) {
1978
- const formBody = new URLSearchParams();
1979
- formBody.set('grant_type', OAuthGrantType.RefreshToken);
1980
- formBody.set('client_id', this.clientId);
1981
- formBody.set('refresh_token', this.refreshToken);
1982
- this.refreshPromise = this.fetchTokens(formBody);
1983
- return this.refreshPromise;
1984
- }
1985
- if (this.clientId && this.clientSecret) {
1986
- this.refreshPromise = this.startClientLogin(this.clientId, this.clientSecret);
1987
- return this.refreshPromise;
1988
- }
1989
- return undefined;
1990
- }
1991
- /**
1992
- * Starts a new OAuth2 client credentials flow.
1993
- *
1994
- * ```typescript
1995
- * await medplum.startClientLogin(process.env.MEDPLUM_CLIENT_ID, process.env.MEDPLUM_CLIENT_SECRET)
1996
- * // Example Search
1997
- * await medplum.searchResources('Patient')
1998
- * ```
1999
- *
2000
- *
2001
- * See: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
2002
- * @category Authentication
2003
- * @param clientId The client ID.
2004
- * @param clientSecret The client secret.
2005
- * @returns Promise that resolves to the client profile.
2006
- */
2007
- async startClientLogin(clientId, clientSecret) {
2008
- this.clientId = clientId;
2009
- this.clientSecret = clientSecret;
2010
- const formBody = new URLSearchParams();
2011
- formBody.set('grant_type', OAuthGrantType.ClientCredentials);
2012
- formBody.set('client_id', clientId);
2013
- formBody.set('client_secret', clientSecret);
2014
- return this.fetchTokens(formBody);
2015
- }
2016
- /**
2017
- * Sets the client ID and secret for basic auth.
2018
- *
2019
- * ```typescript
2020
- * medplum.setBasicAuth(process.env.MEDPLUM_CLIENT_ID, process.env.MEDPLUM_CLIENT_SECRET)
2021
- * // Example Search
2022
- * await medplum.searchResources('Patient')
2023
- * ```
2024
- * @category Authentication
2025
- * @param clientId The client ID.
2026
- * @param clientSecret The client secret.
2027
- */
2028
- setBasicAuth(clientId, clientSecret) {
2029
- this.clientId = clientId;
2030
- this.clientSecret = clientSecret;
2031
- this.basicAuth = encodeBase64(clientId + ':' + clientSecret);
2032
- }
2033
- /**
2034
- * Invite a user to a project.
2035
- * @param projectId The project ID.
2036
- * @param body The InviteBody.
2037
- * @returns Promise that returns a project membership or an operation outcome.
2038
- */
2039
- async invite(projectId, body) {
2040
- return this.post('admin/projects/' + projectId + '/invite', body);
2041
- }
2042
- /**
2043
- * Makes a POST request to the tokens endpoint.
2044
- * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
2045
- * @param formBody Token parameters in URL encoded format.
2046
- * @returns The user profile resource.
2047
- */
2048
- async fetchTokens(formBody) {
2049
- const options = {
2050
- method: 'POST',
2051
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2052
- body: formBody,
2053
- credentials: 'include',
2054
- };
2055
- const headers = options.headers;
2056
- if (this.basicAuth) {
2057
- headers['Authorization'] = `Basic ${this.basicAuth}`;
2058
- }
2059
- const response = await this.fetch(this.tokenUrl, options);
2060
- if (!response.ok) {
2061
- this.clearActiveLogin();
2062
- try {
2063
- const error = await response.json();
2064
- throw new OperationOutcomeError(badRequest(error.error_description));
2065
- }
2066
- catch (err) {
2067
- throw new OperationOutcomeError(badRequest('Failed to fetch tokens'), err);
2068
- }
2069
- }
2070
- const tokens = await response.json();
2071
- await this.verifyTokens(tokens);
2072
- return this.getProfile();
2073
- }
2074
- /**
2075
- * Verifies the tokens received from the auth server.
2076
- * Validates the JWT against the JWKS.
2077
- * See: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
2078
- * @param tokens The token response.
2079
- * @returns Promise to complete.
2080
- */
2081
- async verifyTokens(tokens) {
2082
- const token = tokens.access_token;
2083
- // Verify token has not expired
2084
- const tokenPayload = parseJWTPayload(token);
2085
- if (Date.now() >= tokenPayload.exp * 1000) {
2086
- this.clearActiveLogin();
2087
- throw new Error('Token expired');
2088
- }
2089
- // Verify app_client_id
2090
- // external tokenPayload
2091
- if (tokenPayload.cid) {
2092
- if (tokenPayload.cid !== this.clientId) {
2093
- this.clearActiveLogin();
2094
- throw new Error('Token was not issued for this audience');
2095
- }
2096
- }
2097
- else if (this.clientId && tokenPayload.client_id !== this.clientId) {
2098
- this.clearActiveLogin();
2099
- throw new Error('Token was not issued for this audience');
2100
- }
2101
- return this.setActiveLogin({
2102
- accessToken: token,
2103
- refreshToken: tokens.refresh_token,
2104
- project: tokens.project,
2105
- profile: tokens.profile,
2106
- });
2107
- }
2108
- /**
2109
- * Sets up a listener for window storage events.
2110
- * This synchronizes state across browser windows and browser tabs.
2111
- */
2112
- setupStorageListener() {
2113
- try {
2114
- window.addEventListener('storage', (e) => {
2115
- if (e.key === null || e.key === 'activeLogin') {
2116
- // Storage events fire when different tabs make changes.
2117
- // On storage clear (key === null) or activeLogin change (key === 'activeLogin')
2118
- // Refresh the page to ensure the active login is up to date.
2119
- window.location.reload();
2120
- }
2121
- });
2122
- }
2123
- catch (err) {
2124
- // Silently ignore if this environment does not support storage events
2125
- }
2126
- }
2127
- retryCatch(retryNumber, maxRetries, err) {
2128
- // This is for the 1st retry to avoid multiple notifications
2129
- if (err.message === 'Failed to fetch' && retryNumber === 1) {
2130
- this.dispatchEvent({ type: 'offline' });
2131
- }
2132
- if (retryNumber >= maxRetries - 1) {
2133
- throw err;
2134
- }
2135
- }
2136
- }
2137
- /**
2138
- * Returns the default fetch method.
2139
- * The default fetch is currently only available in browser environments.
2140
- * If you want to use SSR such as Next.js, you should pass a custom fetch function.
2141
- * @returns The default fetch function for the current environment.
2142
- */
2143
- function getDefaultFetch() {
2144
- if (!globalThis.fetch) {
2145
- throw new Error('Fetch not available in this environment');
2146
- }
2147
- return globalThis.fetch.bind(globalThis);
2148
- }
2149
- /**
2150
- * Returns the base URL for the current page.
2151
- * @returns The window origin string.
2152
- * @category HTTP
2153
- */
2154
- function getWindowOrigin() {
2155
- if (typeof window === 'undefined') {
2156
- return '';
2157
- }
2158
- return window.location.protocol + '//' + window.location.host + '/';
2159
- }
2160
- function ensureTrailingSlash(url) {
2161
- if (!url) {
2162
- return url;
2163
- }
2164
- return url.endsWith('/') ? url : url + '/';
2165
- }
2166
-
2167
- export { MEDPLUM_VERSION, MedplumClient, OAuthGrantType, OAuthTokenType };
2168
- //# sourceMappingURL=client.mjs.map