@ovok/core 0.4.36 → 0.4.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/auth/account-storage.d.ts +1 -0
- package/dist/client/auth/account-storage.js +2 -1
- package/dist/{utils → client}/auth/exchange-code.d.ts +1 -1
- package/dist/{utils → client}/auth/generate-code-challenge-and-verifier.js +5 -17
- package/dist/{utils → client}/auth/handle-tenant-code.d.ts +1 -1
- package/dist/{utils → client}/auth/handle-tenant-code.js +1 -1
- package/dist/client/auth/methods/appleLogin.js +2 -6
- package/dist/client/auth/methods/changePassword.js +2 -1
- package/dist/client/auth/methods/getSessions.js +2 -1
- package/dist/client/auth/methods/googleLogin.js +2 -6
- package/dist/client/auth/methods/login.js +26 -44
- package/dist/client/auth/methods/logout.js +2 -1
- package/dist/client/auth/methods/requestDeleteUser.js +5 -4
- package/dist/client/auth/methods/revokeSessions.js +2 -1
- package/dist/client/auth/methods/socialLogin.d.ts +7 -0
- package/dist/client/auth/methods/socialLogin.js +11 -0
- package/dist/client/auth/session-fetch.d.ts +35 -0
- package/dist/client/auth/session-fetch.js +206 -0
- package/dist/client/auth/session-http.d.ts +11 -0
- package/dist/client/auth/session-http.js +169 -0
- package/dist/client/auth/sha256.d.ts +2 -0
- package/dist/client/auth/sha256.js +65 -0
- package/dist/{utils → client}/auth/start-login.d.ts +2 -2
- package/dist/client/change-history/methods.js +2 -1
- package/dist/client/cms/methods.js +16 -28
- package/dist/client/consent/methods.js +3 -2
- package/dist/client/email/methods.js +2 -1
- package/dist/client/errors/ovok-error-utils.d.ts +6 -0
- package/dist/client/errors/ovok-error-utils.js +10 -1
- package/dist/client/errors/ovok-error.js +6 -13
- package/dist/client/errors/rate-limit-error.js +2 -6
- package/dist/client/experience/methods.js +28 -37
- package/dist/client/invites/methods.js +8 -12
- package/dist/client/notifications/methods.js +5 -5
- package/dist/client/observation/fhir-systems.d.ts +4 -0
- package/dist/client/observation/fhir-systems.js +7 -0
- package/dist/client/observation/methods/generateObservationBodiesByMeasurement.js +23 -29
- package/dist/client/observation/methods/getLatestObservationsByCodes.js +6 -9
- package/dist/client/observation/methods/observationsToMeasurements.js +44 -91
- package/dist/client/observation/services/base-observation-service.js +4 -3
- package/dist/client/observation/services/blood-pressure.js +1 -4
- package/dist/client/observation/services/{baby-sleep.d.ts → component-observation-services.d.ts} +18 -13
- package/dist/client/observation/services/component-observation-services.js +69 -0
- package/dist/client/observation/services/creator.d.ts +1 -2
- package/dist/client/observation/services/creator.js +15 -33
- package/dist/client/observation/services/ecg-observation-fragments.js +5 -6
- package/dist/client/observation/services/ecg.js +3 -2
- package/dist/client/observation/services/lipid-panel.js +8 -12
- package/dist/client/observation/services/measurement-time.d.ts +4 -0
- package/dist/client/observation/services/measurement-time.js +6 -0
- package/dist/client/observation/services/single-value-services.d.ts +36 -0
- package/dist/client/observation/services/single-value-services.js +100 -0
- package/dist/client/observation/services/uric-acid-service.js +6 -7
- package/dist/client/observation/types/generate-measurement/HealthConnectTypes.d.ts +2 -458
- package/dist/client/observation/types/generate-measurement/HealthConnectTypes.js +0 -6
- package/dist/client/offline/offline-measurement-queue.js +31 -28
- package/dist/client/ovok-client.d.ts +7 -15
- package/dist/client/ovok-client.js +54 -398
- package/dist/client/patient/methods.js +27 -36
- package/dist/client/patient-sharing/methods.js +4 -4
- package/dist/client/platform/methods.js +21 -30
- package/dist/client/platform/types.d.ts +3 -6
- package/dist/client/query-string.d.ts +3 -0
- package/dist/client/query-string.js +15 -0
- package/dist/client/questionnaire-response/methods/createQuestionnaireResponseWithObservations.js +2 -1
- package/dist/client/questionnaire-response/methods/extractQuestionnaireObservations.js +1 -2
- package/dist/client/questionnaire-response/methods/populateQuestionnaire.js +1 -2
- package/dist/client/questionnaire-response/methods/submitQuestionnaireResponse.js +2 -1
- package/dist/client/request-options.d.ts +2 -0
- package/dist/client/request-options.js +6 -0
- package/dist/client/signals/errors.js +2 -6
- package/dist/client/signals/methods.js +22 -22
- package/dist/client/slim/live-read.d.ts +4 -0
- package/dist/client/slim/live-read.js +87 -0
- package/dist/client/slim/methods.d.ts +3 -0
- package/dist/client/slim/methods.js +73 -166
- package/dist/client/storage.js +3 -2
- package/dist/conformance/capability-requirements.js +1 -1
- package/dist/conformance/capability.js +8 -7
- package/dist/hooks/account-hooks.js +6 -68
- package/dist/hooks/api-hooks.d.ts +10 -6
- package/dist/hooks/api-hooks.js +29 -88
- package/dist/hooks/cms-hooks.js +8 -13
- package/dist/hooks/email-hooks.js +1 -40
- package/dist/hooks/experience-hooks.js +4 -60
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.js +4 -4
- package/dist/hooks/invites-hooks.js +0 -1
- package/dist/hooks/notification-hooks.d.ts +4 -1
- package/dist/hooks/notification-hooks.js +29 -86
- package/dist/hooks/notification-invalidation.d.ts +6 -0
- package/dist/hooks/notification-invalidation.js +52 -0
- package/dist/hooks/notification-note-hooks.js +13 -28
- package/dist/hooks/observation-hooks.js +2 -4
- package/dist/hooks/patient-hooks.js +13 -113
- package/dist/hooks/patient-sharing-hooks.js +2 -7
- package/dist/hooks/platform-hooks.js +3 -51
- package/dist/hooks/poll-hooks.js +10 -8
- package/dist/hooks/signals-alert-hooks.js +4 -6
- package/dist/hooks/signals-hooks.js +2 -45
- package/package.json +1 -1
- package/dist/client/observation/services/baby-sleep.js +0 -69
- package/dist/client/observation/services/blood-glucose.d.ts +0 -12
- package/dist/client/observation/services/blood-glucose.js +0 -36
- package/dist/client/observation/services/body-temperature.d.ts +0 -13
- package/dist/client/observation/services/body-temperature.js +0 -36
- package/dist/client/observation/services/body-weight.d.ts +0 -13
- package/dist/client/observation/services/body-weight.js +0 -36
- package/dist/client/observation/services/heart-rate-variability.d.ts +0 -13
- package/dist/client/observation/services/heart-rate-variability.js +0 -38
- package/dist/client/observation/services/heart-rate.d.ts +0 -13
- package/dist/client/observation/services/heart-rate.js +0 -39
- package/dist/client/observation/services/respiratory-rate.d.ts +0 -13
- package/dist/client/observation/services/respiratory-rate.js +0 -38
- package/dist/client/observation/services/respiratory-therapy.d.ts +0 -28
- package/dist/client/observation/services/respiratory-therapy.js +0 -69
- package/dist/client/observation/services/resting-heart-rate.d.ts +0 -13
- package/dist/client/observation/services/resting-heart-rate.js +0 -38
- package/dist/client/observation/services/vo2-max.d.ts +0 -13
- package/dist/client/observation/services/vo2-max.js +0 -38
- package/dist/client/observation/services/walking-heart-rate-average.d.ts +0 -12
- package/dist/client/observation/services/walking-heart-rate-average.js +0 -38
- package/dist/utils/auth/sha256.d.ts +0 -8
- package/dist/utils/auth/sha256.js +0 -300
- /package/dist/{utils → client}/auth/exchange-code.js +0 -0
- /package/dist/{utils → client}/auth/generate-code-challenge-and-verifier.d.ts +0 -0
- /package/dist/{utils → client}/auth/start-login.js +0 -0
|
@@ -41,6 +41,8 @@ const aiFhirMethods = __importStar(require("./ai-fhir/methods"));
|
|
|
41
41
|
const account_storage_1 = require("./auth/account-storage");
|
|
42
42
|
const errors_1 = require("./auth/errors");
|
|
43
43
|
const authMethods = __importStar(require("./auth/methods"));
|
|
44
|
+
const session_fetch_1 = require("./auth/session-fetch");
|
|
45
|
+
const session_http_1 = require("./auth/session-http");
|
|
44
46
|
const botMethods = __importStar(require("./bot/methods"));
|
|
45
47
|
const changeHistoryMethods = __importStar(require("./change-history/methods"));
|
|
46
48
|
const aiMethods = __importStar(require("./chat/ai/methods"));
|
|
@@ -48,7 +50,6 @@ const cmsMethods = __importStar(require("./cms/methods"));
|
|
|
48
50
|
const consentMethods = __importStar(require("./consent/methods"));
|
|
49
51
|
const emailMethods = __importStar(require("./email"));
|
|
50
52
|
const ovok_error_1 = require("./errors/ovok-error");
|
|
51
|
-
const ovok_error_utils_1 = require("./errors/ovok-error-utils");
|
|
52
53
|
const rate_limit_error_1 = require("./errors/rate-limit-error");
|
|
53
54
|
const experienceMethods = __importStar(require("./experience/methods"));
|
|
54
55
|
const inviteMethods = __importStar(require("./invites/methods"));
|
|
@@ -60,144 +61,13 @@ const patientMethods = __importStar(require("./patient/methods"));
|
|
|
60
61
|
const patientSharingMethods = __importStar(require("./patient-sharing/methods"));
|
|
61
62
|
const platformMethods = __importStar(require("./platform/methods"));
|
|
62
63
|
const questionnaireResponseMethods = __importStar(require("./questionnaire-response/methods"));
|
|
64
|
+
const request_options_1 = require("./request-options");
|
|
63
65
|
const errors_2 = require("./signals/errors");
|
|
64
66
|
const signalsMethods = __importStar(require("./signals/methods"));
|
|
65
67
|
const slimMethods = __importStar(require("./slim/methods"));
|
|
66
|
-
exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
67
68
|
var observation_bundle_utils_2 = require("./observation/observation-bundle-utils");
|
|
69
|
+
Object.defineProperty(exports, "MAX_CONDITIONAL_OBSERVATION_ENTRIES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_CONDITIONAL_OBSERVATION_ENTRIES; } });
|
|
68
70
|
Object.defineProperty(exports, "MAX_OBSERVATION_BUNDLE_BYTES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_OBSERVATION_BUNDLE_BYTES; } });
|
|
69
|
-
const retryAfterMs = (response) => {
|
|
70
|
-
var _a, _b;
|
|
71
|
-
const value = (_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, "Retry-After");
|
|
72
|
-
if (!value) {
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
75
|
-
const seconds = Number(value);
|
|
76
|
-
if (Number.isFinite(seconds)) {
|
|
77
|
-
return Math.max(0, seconds * 1000);
|
|
78
|
-
}
|
|
79
|
-
const date = Date.parse(value);
|
|
80
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
|
|
81
|
-
};
|
|
82
|
-
const timeoutError = () => {
|
|
83
|
-
const error = new Error("The request timed out.");
|
|
84
|
-
error.name = "TimeoutError";
|
|
85
|
-
return error;
|
|
86
|
-
};
|
|
87
|
-
const withHttpErrorMetadata = async (response) => {
|
|
88
|
-
var _a, _b;
|
|
89
|
-
if (response.ok || typeof response.clone !== "function") {
|
|
90
|
-
return response;
|
|
91
|
-
}
|
|
92
|
-
let bodyText = "";
|
|
93
|
-
let body;
|
|
94
|
-
try {
|
|
95
|
-
bodyText = await response.clone().text();
|
|
96
|
-
body = JSON.parse(bodyText);
|
|
97
|
-
}
|
|
98
|
-
catch (_c) {
|
|
99
|
-
body = undefined;
|
|
100
|
-
}
|
|
101
|
-
const outcome = body && typeof body === "object" && body.resourceType === "OperationOutcome"
|
|
102
|
-
? body
|
|
103
|
-
: undefined;
|
|
104
|
-
const envelope = body && typeof body === "object" ? body : undefined;
|
|
105
|
-
const nestedError = (envelope === null || envelope === void 0 ? void 0 : envelope.error) && typeof envelope.error === "object"
|
|
106
|
-
? envelope.error
|
|
107
|
-
: undefined;
|
|
108
|
-
const safeResponse = envelope
|
|
109
|
-
? Object.fromEntries(Object.entries(envelope).filter(([key]) => ["code", "error", "message", "requestId", "retryAfterSec", "status", "statusCode", "errors", "missing"].includes(key)))
|
|
110
|
-
: { message: typeof body === "string" ? body : response.statusText };
|
|
111
|
-
const status = response.status;
|
|
112
|
-
const retryAfter = (_a = retryAfterMs(response)) !== null && _a !== void 0 ? _a : (typeof (envelope === null || envelope === void 0 ? void 0 : envelope.retryAfterSec) === "number" ? envelope.retryAfterSec * 1000 : undefined);
|
|
113
|
-
const requestId = (_b = response.headers.get("x-request-id")) !== null && _b !== void 0 ? _b : (typeof (envelope === null || envelope === void 0 ? void 0 : envelope.requestId) === "string" ? envelope.requestId : undefined);
|
|
114
|
-
const message = typeof (envelope === null || envelope === void 0 ? void 0 : envelope.message) === "string"
|
|
115
|
-
? envelope.message
|
|
116
|
-
: typeof (nestedError === null || nestedError === void 0 ? void 0 : nestedError.message) === "string"
|
|
117
|
-
? nestedError.message
|
|
118
|
-
: response.statusText || `HTTP ${status}`;
|
|
119
|
-
const extensions = [];
|
|
120
|
-
const addMetadata = (url, value, kind) => {
|
|
121
|
-
if (value !== undefined) {
|
|
122
|
-
extensions.push({ url, [`value${kind}`]: value });
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
addMetadata("status", status, "Integer");
|
|
126
|
-
addMetadata("requestId", requestId, "String");
|
|
127
|
-
addMetadata("retryAfterMs", retryAfter, "Integer");
|
|
128
|
-
if (!outcome) {
|
|
129
|
-
extensions.push({ url: "response", valueString: JSON.stringify(safeResponse) });
|
|
130
|
-
}
|
|
131
|
-
const enrichedOutcome = outcome !== null && outcome !== void 0 ? outcome : {
|
|
132
|
-
resourceType: "OperationOutcome",
|
|
133
|
-
id: "ovok-http-error",
|
|
134
|
-
issue: [{
|
|
135
|
-
severity: "error",
|
|
136
|
-
code: status === 429 ? "throttled" : "exception",
|
|
137
|
-
details: { text: bodyText || message },
|
|
138
|
-
}],
|
|
139
|
-
};
|
|
140
|
-
const existingExtensions = Array.isArray(enrichedOutcome.extension)
|
|
141
|
-
? enrichedOutcome.extension
|
|
142
|
-
: [];
|
|
143
|
-
enrichedOutcome.extension = [
|
|
144
|
-
...existingExtensions,
|
|
145
|
-
{ url: ovok_error_utils_1.OVOK_HTTP_ERROR_EXTENSION_URL, extension: extensions },
|
|
146
|
-
];
|
|
147
|
-
const headers = new Headers(response.headers);
|
|
148
|
-
headers.delete("content-length");
|
|
149
|
-
headers.set("content-type", "application/fhir+json");
|
|
150
|
-
return new Response(JSON.stringify(enrichedOutcome), {
|
|
151
|
-
status,
|
|
152
|
-
statusText: response.statusText,
|
|
153
|
-
headers,
|
|
154
|
-
});
|
|
155
|
-
};
|
|
156
|
-
const waitForRetry = (milliseconds, signal) => new Promise((resolve, reject) => {
|
|
157
|
-
var _a;
|
|
158
|
-
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
159
|
-
const error = new Error("Token refresh aborted");
|
|
160
|
-
error.name = "AbortError";
|
|
161
|
-
reject((_a = signal.reason) !== null && _a !== void 0 ? _a : error);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const timer = setTimeout(() => {
|
|
165
|
-
signal === null || signal === void 0 ? void 0 : signal.removeEventListener("abort", abort);
|
|
166
|
-
resolve();
|
|
167
|
-
}, milliseconds);
|
|
168
|
-
const abort = () => {
|
|
169
|
-
var _a;
|
|
170
|
-
clearTimeout(timer);
|
|
171
|
-
const error = new Error("Token refresh aborted");
|
|
172
|
-
error.name = "AbortError";
|
|
173
|
-
reject((_a = signal === null || signal === void 0 ? void 0 : signal.reason) !== null && _a !== void 0 ? _a : error);
|
|
174
|
-
};
|
|
175
|
-
signal === null || signal === void 0 ? void 0 : signal.addEventListener("abort", abort, { once: true });
|
|
176
|
-
});
|
|
177
|
-
const isTemporaryRefreshFailure = (status) => status === 429 || (status >= 500 && status <= 599);
|
|
178
|
-
const refreshUnavailableError = (response) => {
|
|
179
|
-
const error = new Error("Token refresh is temporarily unavailable");
|
|
180
|
-
error.name = "NetworkError";
|
|
181
|
-
Object.assign(error, { cause: response });
|
|
182
|
-
return error;
|
|
183
|
-
};
|
|
184
|
-
const refreshResponseError = (response) => {
|
|
185
|
-
const error = new Error(`Token refresh returned HTTP ${response.status}`);
|
|
186
|
-
error.name = "TokenRefreshError";
|
|
187
|
-
Object.assign(error, { status: response.status, cause: response });
|
|
188
|
-
return error;
|
|
189
|
-
};
|
|
190
|
-
const isNetworkError = (error) => {
|
|
191
|
-
if (!(error instanceof Error)) {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
if (error.name === "NetworkError" ||
|
|
195
|
-
error.name === "UnknownHostException" ||
|
|
196
|
-
/network request failed|failed to fetch|load failed|unknownhostexception|econn|enotfound/i.test(error.message)) {
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
return isNetworkError(error.cause);
|
|
200
|
-
};
|
|
201
71
|
const isRetrySafePost = (pathname) => pathname === "/ai/fhir/search";
|
|
202
72
|
class OvokClient extends core_1.MedplumClient {
|
|
203
73
|
/**
|
|
@@ -214,236 +84,19 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
214
84
|
const baseUrl = (_c = config.baseUrl) !== null && _c !== void 0 ? _c : "https://api.medplum.com/";
|
|
215
85
|
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
216
86
|
const tokenUrl = new URL((_d = config.tokenUrl) !== null && _d !== void 0 ? _d : "oauth2/token", normalizedBaseUrl).href;
|
|
217
|
-
|
|
218
|
-
let clearActiveLogin = () => undefined;
|
|
219
|
-
let terminalRefreshToken;
|
|
220
|
-
let terminalRefreshError;
|
|
221
|
-
const refreshState = { unavailable: false };
|
|
222
|
-
const isTokenRefresh = (url, options) => new URL(url, normalizedBaseUrl).href === tokenUrl &&
|
|
223
|
-
(options === null || options === void 0 ? void 0 : options.method) === "POST" &&
|
|
224
|
-
typeof options.body === "string" &&
|
|
225
|
-
new URLSearchParams(options.body).get("grant_type") === "refresh_token";
|
|
226
|
-
const markActiveAccountExpired = () => {
|
|
227
|
-
var _a, _b;
|
|
228
|
-
const accountId = (_b = (_a = storage.getObject("activeLogin")) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
|
|
229
|
-
if (accountId) {
|
|
230
|
-
(0, account_storage_1.setStoredAccountExpired)(storage, accountId, true);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
const fetchOnce = async (url, options) => {
|
|
234
|
-
var _a, _b;
|
|
235
|
-
const requestTimeoutMs = config.requestTimeoutMs;
|
|
236
|
-
let response;
|
|
237
|
-
if (requestTimeoutMs === undefined) {
|
|
238
|
-
response = await fetchImplementation(url, options);
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
const controller = new AbortController();
|
|
242
|
-
const callerSignal = options === null || options === void 0 ? void 0 : options.signal;
|
|
243
|
-
let timedOut = false;
|
|
244
|
-
const abortFromCaller = () => controller.abort(callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.reason);
|
|
245
|
-
const timeout = setTimeout(() => {
|
|
246
|
-
timedOut = true;
|
|
247
|
-
controller.abort(timeoutError());
|
|
248
|
-
}, requestTimeoutMs);
|
|
249
|
-
if (callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.aborted) {
|
|
250
|
-
abortFromCaller();
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.addEventListener("abort", abortFromCaller, { once: true });
|
|
254
|
-
}
|
|
255
|
-
try {
|
|
256
|
-
response = await fetchImplementation(url, { ...options, signal: controller.signal });
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
if (timedOut) {
|
|
260
|
-
throw timeoutError();
|
|
261
|
-
}
|
|
262
|
-
throw error;
|
|
263
|
-
}
|
|
264
|
-
finally {
|
|
265
|
-
clearTimeout(timeout);
|
|
266
|
-
callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.removeEventListener("abort", abortFromCaller);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const requestUrl = new URL(url, normalizedBaseUrl);
|
|
270
|
-
if (isTokenRefresh(url, options)) {
|
|
271
|
-
if (response.status === 400) {
|
|
272
|
-
let invalidGrant = false;
|
|
273
|
-
try {
|
|
274
|
-
invalidGrant = (await response.clone().json()).error === "invalid_grant";
|
|
275
|
-
}
|
|
276
|
-
catch (_c) {
|
|
277
|
-
// Do not sign out unless the response confirms that the refresh token was revoked.
|
|
278
|
-
}
|
|
279
|
-
if (!invalidGrant) {
|
|
280
|
-
throw refreshResponseError(response);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
if ([400, 401, 403].includes(response.status)) {
|
|
284
|
-
unauthenticatedReason = {
|
|
285
|
-
type: "refresh-rejected",
|
|
286
|
-
status: response.status,
|
|
287
|
-
};
|
|
288
|
-
markActiveAccountExpired();
|
|
289
|
-
}
|
|
290
|
-
else if (response.ok) {
|
|
291
|
-
const accountId = (_b = (_a = storage.getObject("activeLogin")) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
|
|
292
|
-
if (accountId) {
|
|
293
|
-
(0, account_storage_1.setStoredAccountExpired)(storage, accountId, false);
|
|
294
|
-
}
|
|
295
|
-
unauthenticatedReason = undefined;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
else if (requestUrl.href === tokenUrl && !response.ok) {
|
|
299
|
-
unauthenticatedReason = {
|
|
300
|
-
type: "authentication-rejected",
|
|
301
|
-
status: response.status,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
else if (response.status === 401) {
|
|
305
|
-
unauthenticatedReason = { type: "request-unauthorized", status: 401 };
|
|
306
|
-
}
|
|
307
|
-
return isTokenRefresh(url, options)
|
|
308
|
-
? response
|
|
309
|
-
: withHttpErrorMetadata(response);
|
|
310
|
-
};
|
|
311
|
-
const fetchWithRefreshRetry = async (url, options) => {
|
|
312
|
-
var _a, _b, _c, _d;
|
|
313
|
-
const requestUrl = new URL(url, normalizedBaseUrl);
|
|
314
|
-
if (!isTokenRefresh(url, options)) {
|
|
315
|
-
const response = await fetchOnce(url, options);
|
|
316
|
-
const sessionEnded = requestUrl.pathname.endsWith("/auth/session/current") && response.status === 401;
|
|
317
|
-
if (sessionEnded) {
|
|
318
|
-
markActiveAccountExpired();
|
|
319
|
-
}
|
|
320
|
-
return response;
|
|
321
|
-
}
|
|
322
|
-
const login = storage.getObject("activeLogin");
|
|
323
|
-
if (terminalRefreshError) {
|
|
324
|
-
if ((login === null || login === void 0 ? void 0 : login.accessToken) && login.accessToken !== terminalRefreshToken) {
|
|
325
|
-
terminalRefreshToken = undefined;
|
|
326
|
-
terminalRefreshError = undefined;
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
throw terminalRefreshError;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
const expiresAt = (login === null || login === void 0 ? void 0 : login.accessToken) ? (0, core_1.tryGetJwtExpiration)(login.accessToken) : undefined;
|
|
333
|
-
const signal = options === null || options === void 0 ? void 0 : options.signal;
|
|
334
|
-
if (expiresAt !== undefined && options) {
|
|
335
|
-
const tokenRequestOptions = options;
|
|
336
|
-
tokenRequestOptions.maxRetries = 0;
|
|
337
|
-
tokenRequestOptions.maxRetryTime = 0;
|
|
338
|
-
}
|
|
339
|
-
if (expiresAt === undefined) {
|
|
340
|
-
const response = await fetchOnce(url, options);
|
|
341
|
-
if (isTemporaryRefreshFailure(response.status)) {
|
|
342
|
-
void ((_a = response.body) === null || _a === void 0 ? void 0 : _a.cancel().catch(() => undefined));
|
|
343
|
-
if (config.keepSessionOnRefreshUnavailable) {
|
|
344
|
-
refreshState.unavailable = true;
|
|
345
|
-
}
|
|
346
|
-
throw refreshUnavailableError(response);
|
|
347
|
-
}
|
|
348
|
-
return response;
|
|
349
|
-
}
|
|
350
|
-
let attempt = 0;
|
|
351
|
-
let firstAttempt = true;
|
|
352
|
-
let lastResponse;
|
|
353
|
-
let lastNetworkError;
|
|
354
|
-
while (firstAttempt || Date.now() < expiresAt) {
|
|
355
|
-
firstAttempt = false;
|
|
356
|
-
try {
|
|
357
|
-
lastResponse = await fetchOnce(url, options);
|
|
358
|
-
}
|
|
359
|
-
catch (error) {
|
|
360
|
-
if ((signal === null || signal === void 0 ? void 0 : signal.aborted) || !isNetworkError(error)) {
|
|
361
|
-
throw error;
|
|
362
|
-
}
|
|
363
|
-
if (config.keepSessionOnRefreshUnavailable) {
|
|
364
|
-
refreshState.unavailable = true;
|
|
365
|
-
lastNetworkError = error;
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
if (Date.now() >= expiresAt) {
|
|
369
|
-
lastNetworkError = error;
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
lastNetworkError = error;
|
|
373
|
-
const remainingMs = expiresAt - Date.now();
|
|
374
|
-
const backoffMs = Math.min(500 * (2 ** Math.min(attempt, 6)), 30000);
|
|
375
|
-
await waitForRetry(Math.min(backoffMs, remainingMs), signal);
|
|
376
|
-
attempt += 1;
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
if (!isTemporaryRefreshFailure(lastResponse.status)) {
|
|
380
|
-
return lastResponse;
|
|
381
|
-
}
|
|
382
|
-
if (config.keepSessionOnRefreshUnavailable) {
|
|
383
|
-
refreshState.unavailable = true;
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
const remainingMs = expiresAt - Date.now();
|
|
387
|
-
if (remainingMs <= 0) {
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
const backoffMs = Math.min(500 * (2 ** Math.min(attempt, 6)), 30000);
|
|
391
|
-
const waitMs = Math.min(Math.max((_b = retryAfterMs(lastResponse)) !== null && _b !== void 0 ? _b : 0, backoffMs), remainingMs);
|
|
392
|
-
if (waitMs >= remainingMs) {
|
|
393
|
-
await waitForRetry(remainingMs, signal);
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
void ((_c = lastResponse.body) === null || _c === void 0 ? void 0 : _c.cancel().catch(() => undefined));
|
|
397
|
-
await waitForRetry(waitMs, signal);
|
|
398
|
-
attempt += 1;
|
|
399
|
-
}
|
|
400
|
-
const status = lastResponse === null || lastResponse === void 0 ? void 0 : lastResponse.status;
|
|
401
|
-
const reason = {
|
|
402
|
-
type: "refresh-unavailable",
|
|
403
|
-
...(status === undefined ? {} : { status }),
|
|
404
|
-
};
|
|
405
|
-
if (config.keepSessionOnRefreshUnavailable) {
|
|
406
|
-
refreshState.unavailable = true;
|
|
407
|
-
if (lastResponse) {
|
|
408
|
-
return lastResponse;
|
|
409
|
-
}
|
|
410
|
-
throw lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : refreshUnavailableError(undefined);
|
|
411
|
-
}
|
|
412
|
-
unauthenticatedReason = reason;
|
|
413
|
-
markActiveAccountExpired();
|
|
414
|
-
if (lastResponse) {
|
|
415
|
-
return lastResponse;
|
|
416
|
-
}
|
|
417
|
-
terminalRefreshToken = login === null || login === void 0 ? void 0 : login.accessToken;
|
|
418
|
-
terminalRefreshError = lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : refreshUnavailableError(undefined);
|
|
419
|
-
clearActiveLogin();
|
|
420
|
-
(_d = config.onUnauthenticated) === null || _d === void 0 ? void 0 : _d.call(config, reason);
|
|
421
|
-
unauthenticatedReason = undefined;
|
|
422
|
-
throw terminalRefreshError;
|
|
423
|
-
};
|
|
87
|
+
const session = (0, session_fetch_1.createSessionFetch)({ config, storage, fetchImplementation, normalizedBaseUrl, tokenUrl });
|
|
424
88
|
super({
|
|
425
89
|
...config,
|
|
426
90
|
storage,
|
|
427
|
-
onUnauthenticated:
|
|
428
|
-
|
|
429
|
-
if (refreshState.unavailable) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const reason = unauthenticatedReason !== null && unauthenticatedReason !== void 0 ? unauthenticatedReason : {
|
|
433
|
-
type: "request-unauthorized",
|
|
434
|
-
status: 401,
|
|
435
|
-
};
|
|
436
|
-
unauthenticatedReason = undefined;
|
|
437
|
-
(_a = config.onUnauthenticated) === null || _a === void 0 ? void 0 : _a.call(config, reason);
|
|
438
|
-
},
|
|
439
|
-
fetch: fetchWithRefreshRetry,
|
|
91
|
+
onUnauthenticated: session.onUnauthenticated,
|
|
92
|
+
fetch: session.fetch,
|
|
440
93
|
});
|
|
441
94
|
this.socialLoginClientId = "";
|
|
442
|
-
|
|
95
|
+
session.setClearActiveLogin(this.clearActiveLogin.bind(this));
|
|
443
96
|
this.hasExplicitStorageAdapter = config.storage !== undefined;
|
|
444
97
|
this.clientOptions = config;
|
|
445
98
|
this.keepSessionOnRefreshUnavailable = config.keepSessionOnRefreshUnavailable === true;
|
|
446
|
-
this.refreshState = refreshState;
|
|
99
|
+
this.refreshState = session.refreshState;
|
|
447
100
|
this.socialLoginClientId = (_e = config.socialLoginClientId) !== null && _e !== void 0 ? _e : "";
|
|
448
101
|
this.addEventListener("profileRefreshed", () => this.syncSavedAccountProfile());
|
|
449
102
|
this.bindMethods(authMethods);
|
|
@@ -494,7 +147,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
494
147
|
!(activeAccount === null || activeAccount === void 0 ? void 0 : activeAccount.expired) &&
|
|
495
148
|
expiresAt !== undefined &&
|
|
496
149
|
expiresAt > Date.now()) {
|
|
497
|
-
(_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.setObject(
|
|
150
|
+
(_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.setObject(account_storage_1.ACTIVE_LOGIN_STORAGE_KEY, activeLogin);
|
|
498
151
|
this.setAccessToken(accessToken, activeLogin.refreshToken);
|
|
499
152
|
return;
|
|
500
153
|
}
|
|
@@ -546,7 +199,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
546
199
|
}
|
|
547
200
|
catch (error) {
|
|
548
201
|
const activeLogin = this.getActiveLogin();
|
|
549
|
-
if (((_a = activeLogin === null || activeLogin === void 0 ? void 0 : activeLogin.profile) === null || _a === void 0 ? void 0 : _a.reference) === accountId && isNetworkError(error)) {
|
|
202
|
+
if (((_a = activeLogin === null || activeLogin === void 0 ? void 0 : activeLogin.profile) === null || _a === void 0 ? void 0 : _a.reference) === accountId && (0, session_http_1.isNetworkError)(error)) {
|
|
550
203
|
return;
|
|
551
204
|
}
|
|
552
205
|
if (!activeLogin &&
|
|
@@ -573,7 +226,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
573
226
|
const wasActive = previousAccountId === accountId;
|
|
574
227
|
const sessionClient = this.createAccountSessionClient(login);
|
|
575
228
|
try {
|
|
576
|
-
await sessionClient.delete("auth/session/current", ...(
|
|
229
|
+
await sessionClient.delete("auth/session/current", ...(0, request_options_1.optionalArg)(requestOptions));
|
|
577
230
|
}
|
|
578
231
|
catch (error) {
|
|
579
232
|
if (!sessionClient.getSavedAccounts().some((account) => account.id === accountId && account.expired)) {
|
|
@@ -601,14 +254,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
601
254
|
}
|
|
602
255
|
/** Removes every saved account and expiry marker from client storage. */
|
|
603
256
|
clearAllAccounts() {
|
|
604
|
-
|
|
605
|
-
const storage = this.clientStorage;
|
|
606
|
-
const queueKey = (_a = this.offlineQueueOptions.storageKey) !== null && _a !== void 0 ? _a : offline_1.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY;
|
|
607
|
-
const queuedMeasurements = storage === null || storage === void 0 ? void 0 : storage.getObject(queueKey);
|
|
608
|
-
super.clear();
|
|
609
|
-
if (Array.isArray(queuedMeasurements)) {
|
|
610
|
-
storage === null || storage === void 0 ? void 0 : storage.setObject(queueKey, queuedMeasurements);
|
|
611
|
-
}
|
|
257
|
+
this.clearKeepingQueue();
|
|
612
258
|
}
|
|
613
259
|
/** Reauthentication clears the expired marker for the account that signed in. */
|
|
614
260
|
async setActiveLogin(login) {
|
|
@@ -622,22 +268,22 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
622
268
|
/** Saves a measurement immediately, or durably queues it when the opt-in queue is enabled. */
|
|
623
269
|
async saveMeasurement(params, requestOptions) {
|
|
624
270
|
const observations = await this.generateObservationBodiesByMeasurement(params);
|
|
625
|
-
return this.saveObservations(observations, ...(
|
|
271
|
+
return this.saveObservations(observations, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
626
272
|
}
|
|
627
273
|
/** Reads Ovok's composed FHIR and public-route capability statement. */
|
|
628
274
|
async getCapabilityStatement(version = "R4", requestOptions) {
|
|
629
|
-
return this.get(`/fhir/${version}/metadata`, ...(
|
|
275
|
+
return this.get(`/fhir/${version}/metadata`, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
630
276
|
}
|
|
631
277
|
/** Returns the non-FHIR public routes published by the backend statement. */
|
|
632
278
|
async getPublicApiEndpoints(version = "R4", requestOptions) {
|
|
633
|
-
return (0, capability_1.getOvokPublicApiEndpoints)(await this.getCapabilityStatement(version, ...(
|
|
279
|
+
return (0, capability_1.getOvokPublicApiEndpoints)(await this.getCapabilityStatement(version, ...(0, request_options_1.optionalArg)(requestOptions)));
|
|
634
280
|
}
|
|
635
281
|
/** Saves observations with conditional creates so a retry cannot create a duplicate. */
|
|
636
282
|
async saveObservations(observations, requestOptions) {
|
|
637
283
|
const linkedObservations = (0, observation_bundle_utils_1.linkObservationMembers)(observations);
|
|
638
284
|
const queue = this.getOfflineMeasurementQueue();
|
|
639
285
|
if (queue) {
|
|
640
|
-
const { id, flush } = await queue.enqueueAndFlush(linkedObservations, ...(
|
|
286
|
+
const { id, flush } = await queue.enqueueAndFlush(linkedObservations, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
641
287
|
const response = flush.responses[id];
|
|
642
288
|
return response
|
|
643
289
|
? { status: "saved", response }
|
|
@@ -659,7 +305,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
659
305
|
failures: {},
|
|
660
306
|
};
|
|
661
307
|
}
|
|
662
|
-
return queue.flush(options, ...(
|
|
308
|
+
return queue.flush(options, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
663
309
|
}
|
|
664
310
|
getOfflineMeasurementQueue() {
|
|
665
311
|
var _a;
|
|
@@ -669,31 +315,33 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
669
315
|
if (!this.clientStorage || !this.hasExplicitStorageAdapter) {
|
|
670
316
|
throw new Error("Offline measurement queue requires a durable client storage adapter.");
|
|
671
317
|
}
|
|
672
|
-
(_a = this.offlineMeasurementQueue) !== null && _a !== void 0 ? _a : (this.offlineMeasurementQueue = new offline_1.OfflineMeasurementQueue(this.clientStorage, (bundle, requestOptions) => this.executeObservationBundle(bundle, ...(
|
|
318
|
+
(_a = this.offlineMeasurementQueue) !== null && _a !== void 0 ? _a : (this.offlineMeasurementQueue = new offline_1.OfflineMeasurementQueue(this.clientStorage, (bundle, requestOptions) => this.executeObservationBundle(bundle, ...(0, request_options_1.optionalArg)(requestOptions)), {
|
|
673
319
|
...this.offlineQueueOptions,
|
|
674
|
-
getOwnerKey: () =>
|
|
675
|
-
var _a, _b, _c;
|
|
676
|
-
const login = this.getActiveLogin();
|
|
677
|
-
const profileReference = (_a = login === null || login === void 0 ? void 0 : login.profile) === null || _a === void 0 ? void 0 : _a.reference;
|
|
678
|
-
if (!profileReference) {
|
|
679
|
-
return undefined;
|
|
680
|
-
}
|
|
681
|
-
return [
|
|
682
|
-
this.getBaseUrl(),
|
|
683
|
-
(_c = (_b = login === null || login === void 0 ? void 0 : login.project) === null || _b === void 0 ? void 0 : _b.reference) !== null && _c !== void 0 ? _c : "",
|
|
684
|
-
profileReference,
|
|
685
|
-
].join("|");
|
|
686
|
-
},
|
|
320
|
+
getOwnerKey: () => this.offlineOwnerKey(),
|
|
687
321
|
}));
|
|
688
322
|
return this.offlineMeasurementQueue;
|
|
689
323
|
}
|
|
324
|
+
/** Queued measurements belong to one account on one server and project. */
|
|
325
|
+
offlineOwnerKey() {
|
|
326
|
+
var _a, _b, _c;
|
|
327
|
+
const login = this.getActiveLogin();
|
|
328
|
+
const profileReference = (_a = login === null || login === void 0 ? void 0 : login.profile) === null || _a === void 0 ? void 0 : _a.reference;
|
|
329
|
+
if (!profileReference) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
return [
|
|
333
|
+
this.getBaseUrl(),
|
|
334
|
+
(_c = (_b = login === null || login === void 0 ? void 0 : login.project) === null || _b === void 0 ? void 0 : _b.reference) !== null && _c !== void 0 ? _c : "",
|
|
335
|
+
profileReference,
|
|
336
|
+
].join("|");
|
|
337
|
+
}
|
|
690
338
|
/**
|
|
691
339
|
* Medplum clears its entire client storage after an unrecoverable 401. Keep
|
|
692
340
|
* saved accounts (marking only the active one expired) and queued measurements.
|
|
693
341
|
* Use clearAllAccounts() when the caller intends to remove all saved accounts.
|
|
694
342
|
*/
|
|
695
343
|
clear() {
|
|
696
|
-
var _a, _b
|
|
344
|
+
var _a, _b;
|
|
697
345
|
const storage = this.clientStorage;
|
|
698
346
|
const activeAccountId = (_b = (_a = this.getActiveLogin()) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.reference;
|
|
699
347
|
const logins = this.getLogins().map((login) => {
|
|
@@ -706,15 +354,23 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
706
354
|
if (activeAccountId) {
|
|
707
355
|
expiredAccountIds.add(activeAccountId);
|
|
708
356
|
}
|
|
709
|
-
|
|
357
|
+
this.clearKeepingQueue(() => {
|
|
358
|
+
if (logins.length > 0) {
|
|
359
|
+
storage === null || storage === void 0 ? void 0 : storage.setObject(account_storage_1.LOGINS_STORAGE_KEY, logins);
|
|
360
|
+
}
|
|
361
|
+
if (expiredAccountIds.size > 0) {
|
|
362
|
+
storage === null || storage === void 0 ? void 0 : storage.setObject(account_storage_1.EXPIRED_ACCOUNTS_STORAGE_KEY, Array.from(expiredAccountIds));
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/** Runs Medplum's storage clear but keeps queued offline measurements; `restore` runs in between. */
|
|
367
|
+
clearKeepingQueue(restore) {
|
|
368
|
+
var _a;
|
|
369
|
+
const storage = this.clientStorage;
|
|
370
|
+
const queueKey = (_a = this.offlineQueueOptions.storageKey) !== null && _a !== void 0 ? _a : offline_1.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY;
|
|
710
371
|
const queuedMeasurements = storage === null || storage === void 0 ? void 0 : storage.getObject(queueKey);
|
|
711
372
|
super.clear();
|
|
712
|
-
|
|
713
|
-
storage === null || storage === void 0 ? void 0 : storage.setObject(account_storage_1.LOGINS_STORAGE_KEY, logins);
|
|
714
|
-
}
|
|
715
|
-
if (expiredAccountIds.size > 0) {
|
|
716
|
-
storage === null || storage === void 0 ? void 0 : storage.setObject(account_storage_1.EXPIRED_ACCOUNTS_STORAGE_KEY, Array.from(expiredAccountIds));
|
|
717
|
-
}
|
|
373
|
+
restore === null || restore === void 0 ? void 0 : restore();
|
|
718
374
|
if (Array.isArray(queuedMeasurements)) {
|
|
719
375
|
storage === null || storage === void 0 ? void 0 : storage.setObject(queueKey, queuedMeasurements);
|
|
720
376
|
}
|
|
@@ -728,7 +384,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
728
384
|
}
|
|
729
385
|
createAccountSessionClient(login) {
|
|
730
386
|
const storage = new core_1.ClientStorage(new core_1.MemoryStorage(), this.clientOptions.storagePrefix);
|
|
731
|
-
storage.setObject(
|
|
387
|
+
storage.setObject(account_storage_1.ACTIVE_LOGIN_STORAGE_KEY, login);
|
|
732
388
|
storage.setObject(account_storage_1.LOGINS_STORAGE_KEY, [login]);
|
|
733
389
|
const client = new OvokClient({
|
|
734
390
|
...this.clientOptions,
|
|
@@ -761,7 +417,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
761
417
|
...login,
|
|
762
418
|
profile: { ...login.profile, display: (0, core_1.getDisplayString)(profile) },
|
|
763
419
|
};
|
|
764
|
-
(_b = this.clientStorage) === null || _b === void 0 ? void 0 : _b.setObject(
|
|
420
|
+
(_b = this.clientStorage) === null || _b === void 0 ? void 0 : _b.setObject(account_storage_1.ACTIVE_LOGIN_STORAGE_KEY, updatedLogin);
|
|
765
421
|
(_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; }));
|
|
766
422
|
}
|
|
767
423
|
async restoreActiveLogin(login) {
|
|
@@ -778,10 +434,10 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
778
434
|
resourceType: "Bundle",
|
|
779
435
|
type: "transaction",
|
|
780
436
|
entry: entries,
|
|
781
|
-
}, ...(
|
|
437
|
+
}, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
782
438
|
}
|
|
783
439
|
async executeObservationBundle(bundle, requestOptions) {
|
|
784
|
-
const response = await (0, observation_bundle_utils_1.executeObservationBundle)(bundle, (request) => this.executeBatch(request, ...(
|
|
440
|
+
const response = await (0, observation_bundle_utils_1.executeObservationBundle)(bundle, (request) => this.executeBatch(request, ...(0, request_options_1.optionalArg)(requestOptions)));
|
|
785
441
|
this.invalidateSearches("Observation");
|
|
786
442
|
return response;
|
|
787
443
|
}
|