@ovok/core 0.4.26 → 0.4.27

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.
@@ -0,0 +1,5 @@
1
+ import { IClientStorage } from "@medplum/core";
2
+ export declare const LOGINS_STORAGE_KEY = "logins";
3
+ export declare const EXPIRED_ACCOUNTS_STORAGE_KEY = "ovok.core.expired-accounts";
4
+ export declare const setStoredAccountExpired: (storage: IClientStorage, accountId: string, expired: boolean) => void;
5
+ export declare const removeStoredAccount: (storage: IClientStorage, accountId: string) => void;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.removeStoredAccount = exports.setStoredAccountExpired = exports.EXPIRED_ACCOUNTS_STORAGE_KEY = exports.LOGINS_STORAGE_KEY = void 0;
4
+ exports.LOGINS_STORAGE_KEY = "logins";
5
+ exports.EXPIRED_ACCOUNTS_STORAGE_KEY = "ovok.core.expired-accounts";
6
+ const setStoredAccountExpired = (storage, accountId, expired) => {
7
+ var _a;
8
+ const expiredAccountIds = new Set((_a = storage.getObject(exports.EXPIRED_ACCOUNTS_STORAGE_KEY)) !== null && _a !== void 0 ? _a : []);
9
+ if (expired) {
10
+ expiredAccountIds.add(accountId);
11
+ }
12
+ else {
13
+ expiredAccountIds.delete(accountId);
14
+ }
15
+ if (expiredAccountIds.size > 0) {
16
+ storage.setObject(exports.EXPIRED_ACCOUNTS_STORAGE_KEY, Array.from(expiredAccountIds));
17
+ }
18
+ else {
19
+ storage.setString(exports.EXPIRED_ACCOUNTS_STORAGE_KEY, undefined);
20
+ }
21
+ };
22
+ exports.setStoredAccountExpired = setStoredAccountExpired;
23
+ const removeStoredAccount = (storage, accountId) => {
24
+ var _a;
25
+ const logins = (_a = storage.getObject(exports.LOGINS_STORAGE_KEY)) !== null && _a !== void 0 ? _a : [];
26
+ storage.setObject(exports.LOGINS_STORAGE_KEY, logins.filter((login) => { var _a; return ((_a = login.profile) === null || _a === void 0 ? void 0 : _a.reference) !== accountId; }));
27
+ (0, exports.setStoredAccountExpired)(storage, accountId, false);
28
+ };
29
+ exports.removeStoredAccount = removeStoredAccount;
@@ -1,3 +1,3 @@
1
1
  import { OvokClient } from "../../ovok-client";
2
- /** Revokes the current server session and always clears the local login. */
2
+ /** Revokes the current server session and always removes its local account. */
3
3
  export declare function logout(this: OvokClient): Promise<void>;
@@ -1,12 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.logout = logout;
4
- /** Revokes the current server session and always clears the local login. */
4
+ const account_storage_1 = require("../account-storage");
5
+ /** Revokes the current server session and always removes its local account. */
5
6
  async function logout() {
7
+ var _a, _b;
8
+ const accountId = (_b = (_a = this.getActiveLogin()) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
6
9
  try {
7
10
  await this.delete("auth/session/current");
8
11
  }
9
12
  finally {
10
13
  this.clearActiveLogin();
14
+ if (accountId && this.clientStorage) {
15
+ (0, account_storage_1.removeStoredAccount)(this.clientStorage, accountId);
16
+ }
11
17
  }
12
18
  }
@@ -52,7 +52,8 @@ export declare class OvokClient extends MedplumClient {
52
52
  switchAccount(accountId: string): Promise<void>;
53
53
  /**
54
54
  * Ends the selected account's current server session and removes its saved login.
55
- * The account is retained locally if the server cannot confirm the logout.
55
+ * Accounts with an expired server session are removed locally; network and service
56
+ * failures retain the account so it can be removed after connectivity returns.
56
57
  */
57
58
  removeAccount(accountId: string): Promise<void>;
58
59
  /** Removes every saved account and expiry marker from client storage. */
@@ -79,6 +80,8 @@ export declare class OvokClient extends MedplumClient {
79
80
  private getLogin;
80
81
  private getExpiredAccountIds;
81
82
  private setAccountExpired;
83
+ private removeSavedAccount;
84
+ private syncSavedAccountProfile;
82
85
  private restoreActiveLogin;
83
86
  private executeConditionalObservationCreates;
84
87
  private executeObservationBundle;
@@ -38,6 +38,7 @@ exports.OvokClient = exports.MAX_OBSERVATION_BUNDLE_BYTES = exports.MAX_CONDITIO
38
38
  const core_1 = require("@medplum/core");
39
39
  const capability_1 = require("../conformance/capability");
40
40
  const aiFhirMethods = __importStar(require("./ai-fhir/methods"));
41
+ const account_storage_1 = require("./auth/account-storage");
41
42
  const authMethods = __importStar(require("./auth/methods"));
42
43
  const botMethods = __importStar(require("./bot/methods"));
43
44
  const changeHistoryMethods = __importStar(require("./change-history/methods"));
@@ -63,20 +64,51 @@ const slimMethods = __importStar(require("./slim/methods"));
63
64
  exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
64
65
  var observation_bundle_utils_2 = require("./observation/observation-bundle-utils");
65
66
  Object.defineProperty(exports, "MAX_OBSERVATION_BUNDLE_BYTES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_OBSERVATION_BUNDLE_BYTES; } });
66
- const LOGINS_STORAGE_KEY = "logins";
67
- const EXPIRED_ACCOUNTS_STORAGE_KEY = "ovok.core.expired-accounts";
67
+ const isNetworkError = (error) => {
68
+ if (!(error instanceof Error)) {
69
+ return false;
70
+ }
71
+ if (error.name === "NetworkError" ||
72
+ error.name === "UnknownHostException" ||
73
+ /network request failed|failed to fetch|load failed|unknownhostexception|econn|enotfound/i.test(error.message)) {
74
+ return true;
75
+ }
76
+ return isNetworkError(error.cause);
77
+ };
68
78
  class OvokClient extends core_1.MedplumClient {
69
79
  /**
70
80
  * @param config - The configuration for the client.
71
81
  * @param config.socialLoginClientId - The social login client id to use for the client.
72
82
  */
73
83
  constructor(config) {
74
- var _a, _b, _c;
84
+ var _a, _b, _c, _d, _e, _f;
75
85
  const storage = (_a = config.storage) !== null && _a !== void 0 ? _a : new core_1.ClientStorage();
76
- super({ ...config, storage });
86
+ const fetchImplementation = (_b = config.fetch) !== null && _b !== void 0 ? _b : globalThis.fetch.bind(globalThis);
87
+ const baseUrl = (_c = config.baseUrl) !== null && _c !== void 0 ? _c : "https://api.medplum.com/";
88
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
89
+ const tokenUrl = new URL((_d = config.tokenUrl) !== null && _d !== void 0 ? _d : "oauth2/token", normalizedBaseUrl).href;
90
+ super({
91
+ ...config,
92
+ storage,
93
+ fetch: async (url, options) => {
94
+ var _a, _b;
95
+ const response = await fetchImplementation(url, options);
96
+ const requestUrl = new URL(url, normalizedBaseUrl);
97
+ const refreshRefused = requestUrl.href === tokenUrl && [400, 401, 403].includes(response.status);
98
+ const sessionEnded = requestUrl.pathname.endsWith("/auth/session/current") && response.status === 401;
99
+ if (refreshRefused || sessionEnded) {
100
+ const accountId = (_b = (_a = storage.getObject("activeLogin")) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
101
+ if (accountId) {
102
+ (0, account_storage_1.setStoredAccountExpired)(storage, accountId, true);
103
+ }
104
+ }
105
+ return response;
106
+ },
107
+ });
77
108
  this.socialLoginClientId = "";
78
109
  this.hasExplicitStorageAdapter = config.storage !== undefined;
79
- this.socialLoginClientId = (_b = config.socialLoginClientId) !== null && _b !== void 0 ? _b : "";
110
+ this.socialLoginClientId = (_e = config.socialLoginClientId) !== null && _e !== void 0 ? _e : "";
111
+ this.addEventListener("profileRefreshed", () => this.syncSavedAccountProfile());
80
112
  this.bindMethods(authMethods);
81
113
  this.bindMethods(observationMethods);
82
114
  this.bindMethods(questionnaireResponseMethods);
@@ -101,7 +133,7 @@ class OvokClient extends core_1.MedplumClient {
101
133
  return (0, errors_1.isSignalsError)(mapped) ? mapped : (0, ovok_error_1.mapOvokApiError)(mapped);
102
134
  });
103
135
  this.clientStorage = storage;
104
- this.offlineQueueOptions = (_c = config.offlineQueue) !== null && _c !== void 0 ? _c : {};
136
+ this.offlineQueueOptions = (_f = config.offlineQueue) !== null && _f !== void 0 ? _f : {};
105
137
  }
106
138
  /** Lists saved accounts without exposing their access or refresh tokens. */
107
139
  getSavedAccounts() {
@@ -124,6 +156,7 @@ class OvokClient extends core_1.MedplumClient {
124
156
  }
125
157
  /** Activates a previously saved account without asking for its password again. */
126
158
  async switchAccount(accountId) {
159
+ var _a, _b;
127
160
  const login = this.getLogin(accountId);
128
161
  if (!login) {
129
162
  throw new Error(`No saved account found for ${accountId}.`);
@@ -131,34 +164,68 @@ class OvokClient extends core_1.MedplumClient {
131
164
  if (this.getExpiredAccountIds().includes(accountId)) {
132
165
  throw new Error("This account's session has expired. Sign in again.");
133
166
  }
134
- await this.setActiveLogin(login);
167
+ const previousLogin = this.getActiveLogin();
168
+ try {
169
+ await this.setActiveLogin(login);
170
+ }
171
+ catch (error) {
172
+ const activeLogin = this.getActiveLogin();
173
+ if (((_a = activeLogin === null || activeLogin === void 0 ? void 0 : activeLogin.profile) === null || _a === void 0 ? void 0 : _a.reference) === accountId && isNetworkError(error)) {
174
+ return;
175
+ }
176
+ if (!activeLogin &&
177
+ previousLogin &&
178
+ ((_b = previousLogin.profile) === null || _b === void 0 ? void 0 : _b.reference) !== accountId) {
179
+ await this.restoreActiveLogin(previousLogin);
180
+ }
181
+ throw error;
182
+ }
135
183
  }
136
184
  /**
137
185
  * Ends the selected account's current server session and removes its saved login.
138
- * The account is retained locally if the server cannot confirm the logout.
186
+ * Accounts with an expired server session are removed locally; network and service
187
+ * failures retain the account so it can be removed after connectivity returns.
139
188
  */
140
189
  async removeAccount(accountId) {
141
- var _a, _b;
190
+ var _a;
142
191
  const login = this.getLogin(accountId);
143
192
  if (!login) {
144
193
  throw new Error(`No saved account found for ${accountId}.`);
145
194
  }
146
195
  const previousLogin = this.getActiveLogin();
196
+ const previousAccountId = (_a = previousLogin === null || previousLogin === void 0 ? void 0 : previousLogin.profile) === null || _a === void 0 ? void 0 : _a.reference;
147
197
  try {
148
198
  await this.setActiveLogin(login);
149
199
  await this.delete("auth/session/current");
150
200
  }
151
201
  catch (error) {
152
- if (previousLogin && previousLogin.profile.reference !== accountId) {
202
+ if (this.getExpiredAccountIds().includes(accountId)) {
203
+ this.removeSavedAccount(accountId);
204
+ if (previousLogin && previousAccountId && previousAccountId !== accountId) {
205
+ await this.restoreActiveLogin(previousLogin);
206
+ }
207
+ else {
208
+ this.clearActiveLogin();
209
+ }
210
+ return;
211
+ }
212
+ if (previousLogin && previousAccountId !== accountId) {
153
213
  await this.restoreActiveLogin(previousLogin);
154
214
  }
155
215
  throw error;
156
216
  }
157
217
  this.clearActiveLogin();
158
- const remainingLogins = this.getLogins().filter((savedLogin) => savedLogin.profile.reference !== accountId);
159
- (_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.setObject(LOGINS_STORAGE_KEY, remainingLogins);
160
- this.setAccountExpired(accountId, false);
161
- const nextLogin = (_b = ((previousLogin === null || previousLogin === void 0 ? void 0 : previousLogin.profile.reference) !== accountId ? previousLogin : undefined)) !== null && _b !== void 0 ? _b : remainingLogins[0];
218
+ this.removeSavedAccount(accountId);
219
+ const availableAccountIds = new Set(this.getSavedAccounts().filter((account) => !account.expired).map((account) => account.id));
220
+ let nextLogin;
221
+ if (previousAccountId && previousAccountId !== accountId) {
222
+ if (availableAccountIds.has(previousAccountId)) {
223
+ nextLogin = previousLogin;
224
+ }
225
+ }
226
+ else if (previousAccountId === accountId) {
227
+ nextLogin = this.getLogins().find((savedLogin) => { var _a, _b; return availableAccountIds.has((_b = (_a = savedLogin.profile) === null || _a === void 0 ? void 0 : _a.reference) !== null && _b !== void 0 ? _b : ""); });
228
+ }
162
229
  if (nextLogin) {
163
230
  await this.restoreActiveLogin(nextLogin);
164
231
  }
@@ -268,10 +335,10 @@ class OvokClient extends core_1.MedplumClient {
268
335
  const queuedMeasurements = storage === null || storage === void 0 ? void 0 : storage.getObject(queueKey);
269
336
  super.clear();
270
337
  if (logins.length > 0) {
271
- storage === null || storage === void 0 ? void 0 : storage.setObject(LOGINS_STORAGE_KEY, logins);
338
+ storage === null || storage === void 0 ? void 0 : storage.setObject(account_storage_1.LOGINS_STORAGE_KEY, logins);
272
339
  }
273
340
  if (expiredAccountIds.size > 0) {
274
- storage === null || storage === void 0 ? void 0 : storage.setObject(EXPIRED_ACCOUNTS_STORAGE_KEY, Array.from(expiredAccountIds));
341
+ storage === null || storage === void 0 ? void 0 : storage.setObject(account_storage_1.EXPIRED_ACCOUNTS_STORAGE_KEY, Array.from(expiredAccountIds));
275
342
  }
276
343
  if (Array.isArray(queuedMeasurements)) {
277
344
  storage === null || storage === void 0 ? void 0 : storage.setObject(queueKey, queuedMeasurements);
@@ -282,23 +349,32 @@ class OvokClient extends core_1.MedplumClient {
282
349
  }
283
350
  getExpiredAccountIds() {
284
351
  var _a, _b;
285
- return ((_b = (_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.getObject(EXPIRED_ACCOUNTS_STORAGE_KEY)) !== null && _b !== void 0 ? _b : []);
352
+ return ((_b = (_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.getObject(account_storage_1.EXPIRED_ACCOUNTS_STORAGE_KEY)) !== null && _b !== void 0 ? _b : []);
286
353
  }
287
354
  setAccountExpired(accountId, expired) {
288
- var _a, _b;
289
- const expiredAccountIds = new Set(this.getExpiredAccountIds());
290
- if (expired) {
291
- expiredAccountIds.add(accountId);
355
+ if (this.clientStorage) {
356
+ (0, account_storage_1.setStoredAccountExpired)(this.clientStorage, accountId, expired);
292
357
  }
293
- else {
294
- expiredAccountIds.delete(accountId);
295
- }
296
- if (expiredAccountIds.size > 0) {
297
- (_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.setObject(EXPIRED_ACCOUNTS_STORAGE_KEY, Array.from(expiredAccountIds));
358
+ }
359
+ removeSavedAccount(accountId) {
360
+ if (this.clientStorage) {
361
+ (0, account_storage_1.removeStoredAccount)(this.clientStorage, accountId);
298
362
  }
299
- else {
300
- (_b = this.clientStorage) === null || _b === void 0 ? void 0 : _b.setString(EXPIRED_ACCOUNTS_STORAGE_KEY, undefined);
363
+ }
364
+ syncSavedAccountProfile() {
365
+ var _a, _b, _c;
366
+ const login = this.getActiveLogin();
367
+ const profile = this.getProfile();
368
+ const accountId = (_a = login === null || login === void 0 ? void 0 : login.profile) === null || _a === void 0 ? void 0 : _a.reference;
369
+ if (!login || !profile || !accountId) {
370
+ return;
301
371
  }
372
+ const updatedLogin = {
373
+ ...login,
374
+ profile: { ...login.profile, display: (0, core_1.getDisplayString)(profile) },
375
+ };
376
+ (_b = this.clientStorage) === null || _b === void 0 ? void 0 : _b.setObject("activeLogin", updatedLogin);
377
+ (_c = this.clientStorage) === null || _c === void 0 ? void 0 : _c.setObject(account_storage_1.LOGINS_STORAGE_KEY, this.getLogins().map((savedLogin) => { var _a; return ((_a = savedLogin.profile) === null || _a === void 0 ? void 0 : _a.reference) === accountId ? updatedLogin : savedLogin; }));
302
378
  }
303
379
  async restoreActiveLogin(login) {
304
380
  try {
@@ -48,7 +48,7 @@ exports.OVOK_CORE_REQUIREMENTS = {
48
48
  description: "The FHIR surface a server must provide for the @ovok/core client library's own methods to function.",
49
49
  software: {
50
50
  name: "@ovok/core",
51
- version: "0.4.26",
51
+ version: "0.4.27",
52
52
  },
53
53
  fhirVersion: "4.0.1",
54
54
  format: ["json"],
@@ -45,22 +45,18 @@ const useObservations = ({ types, period, patientId, id, groupBy, timeToleranceM
45
45
  // A broad search is intentional: one measurement can expand into several
46
46
  // observations, and the inverse mapper performs the type-specific grouping.
47
47
  const codes = React.useMemo(() => (0, observationsToMeasurements_1.observationCodesForTypes)(types), [types]);
48
- const query = React.useMemo(() => ({
49
- ...(codes ? { code: codes } : {}),
50
- ...(patientId ? { subject: `Patient/${patientId}` } : {}),
51
- ...(id ? { _id: id } : {}),
52
- ...((period === null || period === void 0 ? void 0 : period.start) || (period === null || period === void 0 ? void 0 : period.end)
53
- ? {
54
- date: [
55
- ...(period.start ? [`ge${period.start.toISOString()}`] : []),
56
- ...(period.end ? [`le${period.end.toISOString()}`] : []),
57
- ],
58
- }
59
- : {}),
60
- ...(count !== undefined ? { _count: count } : {}),
61
- ...(sort ? { _sort: sort } : {}),
62
- ...(elements ? { _elements: Array.isArray(elements) ? elements.join(",") : elements } : {}),
63
- }), [codes, count, elements, id, patientId, period === null || period === void 0 ? void 0 : period.end, period === null || period === void 0 ? void 0 : period.start, sort]);
48
+ const query = React.useMemo(() => [
49
+ ...(codes ? [["code", codes]] : []),
50
+ ...(patientId ? [["subject", `Patient/${patientId}`]] : []),
51
+ ...(id ? [["_id", id]] : []),
52
+ ...((period === null || period === void 0 ? void 0 : period.start) ? [["date", `ge${period.start.toISOString()}`]] : []),
53
+ ...((period === null || period === void 0 ? void 0 : period.end) ? [["date", `le${period.end.toISOString()}`]] : []),
54
+ ...(count !== undefined ? [["_count", String(count)]] : []),
55
+ ...(sort ? [["_sort", sort]] : []),
56
+ ...(elements
57
+ ? [["_elements", typeof elements === "string" ? elements : elements.join(",")]]
58
+ : []),
59
+ ], [codes, count, elements, id, patientId, period === null || period === void 0 ? void 0 : period.end, period === null || period === void 0 ? void 0 : period.start, sort]);
64
60
  const client = (0, index_1.useClient)();
65
61
  const load = React.useCallback(() => client.searchResources("Observation", query), [client, query]);
66
62
  const { data: observations = [], reload: refetch, loading, error } = (0, api_hooks_1.useOvokRequest)(load);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovok/core",
3
- "version": "0.4.26",
3
+ "version": "0.4.27",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",