@ovok/core 0.4.42 → 0.4.44
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/README.md +38 -5
- package/dist/client/auth/session-fetch.js +42 -4
- package/dist/client/auth/session-http.d.ts +4 -2
- package/dist/client/auth/session-http.js +22 -13
- package/dist/client/auth/types/LoginError.d.ts +5 -4
- package/dist/client/auth/types/LoginError.js +4 -3
- package/dist/client/change-history/methods.js +4 -3
- package/dist/client/change-history/types.d.ts +26 -13
- package/dist/client/errors/ovok-error.js +2 -2
- package/dist/client/notifications/types.d.ts +3 -1
- package/dist/client/ovok-client.d.ts +1 -0
- package/dist/client/ovok-client.js +33 -14
- package/dist/client/slim/methods.d.ts +17 -1
- package/dist/client/slim/methods.js +25 -8
- package/dist/client/slim/types.d.ts +8 -5
- package/dist/conformance/api-requirements.js +0 -1
- package/dist/conformance/capability-requirements.js +1 -1
- package/dist/hooks/notification-hooks.d.ts +1 -0
- package/dist/hooks/notification-hooks.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -330,6 +330,28 @@ validation errors or 424 when the baseline has not been collected. Hooks never r
|
|
|
330
330
|
a stale body; transient 503 transport retries do not clear the session.
|
|
331
331
|
The unconfirmed-limit list and repush routes require project-admin access; a 403 remains an error.
|
|
332
332
|
|
|
333
|
+
Resident deletion returns 409 while the resident has a device; unassign the device with
|
|
334
|
+
`updateSlimPatient()` before deleting. Resident, room, and device writes can also return 422 when
|
|
335
|
+
their device, room, and zone assignments disagree; show the server message and reload before
|
|
336
|
+
retrying. Device serial conflicts return 409 when another customer holds the serial and 422 when
|
|
337
|
+
another device in the project already uses it. Renaming a device moves its serial: old-serial
|
|
338
|
+
readings stop reaching it, and live values return after a reading arrives under the new serial.
|
|
339
|
+
Resident updates replace the editable fields; omitting `locationId` unassigns its devices, and names
|
|
340
|
+
are trimmed by the server.
|
|
341
|
+
|
|
342
|
+
A resident threshold save can return 409 while another save is running; nothing changed, so wait
|
|
343
|
+
briefly and send it again. A home threshold save can return 502 when Signals did not take every
|
|
344
|
+
resident's limits; nothing was written. A home with no saved limits reads as the default template
|
|
345
|
+
with heart rate 40–110 and respiratory rate 8–25 enabled, and `id: null`.
|
|
346
|
+
|
|
347
|
+
Notification feeds and episode reads can wait 5 seconds for Signals before returning 503
|
|
348
|
+
`signals_unavailable`; realtime trends can wait 12 seconds. Realtime trends may also be shed
|
|
349
|
+
immediately under load. Wait for `retryAfterMs` before trying again. The paged medical feed returns
|
|
350
|
+
503 during an outage, while recent medical notifications may return the last good page, up to 10
|
|
351
|
+
minutes old, without a marker. Episode `values` contains only readings the episode counted, not
|
|
352
|
+
valueless or history readings. Acknowledgements can take about 12 seconds and are safe to repeat.
|
|
353
|
+
Realtime trends allow 20 calls per minute per caller; load once per view instead of polling.
|
|
354
|
+
|
|
333
355
|
`getLegalConsent()` returns the signed-in practitioner's own terms status. If
|
|
334
356
|
`needsLegalConsentReacceptance(response.legal)` is true, show the current terms before continuing.
|
|
335
357
|
|
|
@@ -424,8 +446,9 @@ The backend allows three kinds of read:
|
|
|
424
446
|
Signed out, a read rejects with `SignInRequiredError` (`isSignInRequiredError(error)`) before
|
|
425
447
|
any request is sent, unless it passes `allowSignedOut: true` and a `tenantCode`. `getLegalPage`,
|
|
426
448
|
`getTranslations` and their hooks turn `allowSignedOut` on by default; pass it yourself only for
|
|
427
|
-
another collection the backend serves without a key or session. A
|
|
428
|
-
clear the session storage and call `onUnauthenticated
|
|
449
|
+
another collection the backend serves without a key or session. A 401 from an ordinary request can
|
|
450
|
+
clear the session storage and call `onUnauthenticated`; a 401 from password sign-in is instead a
|
|
451
|
+
`LoginError` and leaves the active account untouched. A hook that rendered before sign-in needs
|
|
429
452
|
`reload()` once the session exists.
|
|
430
453
|
|
|
431
454
|
`locale` takes an app language or a CMS locale (`en`, `de`, `fr`, `es`). `toCmsLocale` maps
|
|
@@ -483,9 +506,11 @@ create a second copy of the same observation. The queue is opt-in and requires d
|
|
|
483
506
|
|
|
484
507
|
## Rate limits
|
|
485
508
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
509
|
+
Ovok-defined methods, including measurement saves, reject with `RateLimitError` when the server
|
|
510
|
+
returns 429. A 429 or 503 with a retry delay is returned without an automatic retry; wait for
|
|
511
|
+
`retryAfterMs` before trying again. Use `isRateLimitError(error)` and `retryAfterMs` to show a
|
|
512
|
+
translated wait message without parsing backend text. Inherited Medplum FHIR methods keep their
|
|
513
|
+
`OperationOutcomeError`; use `readOvokError(error)` to read their status and delay.
|
|
489
514
|
|
|
490
515
|
## Session management
|
|
491
516
|
|
|
@@ -517,6 +542,14 @@ if ("nextStep" in result && result.nextStep === "mfa") {
|
|
|
517
542
|
}
|
|
518
543
|
```
|
|
519
544
|
|
|
545
|
+
### Password sign-in errors
|
|
546
|
+
|
|
547
|
+
Password sign-in failures throw `LoginError`. A 401 has `code: "invalid_credentials"` and the
|
|
548
|
+
server's message; it does not refresh tokens, clear an existing account, or call
|
|
549
|
+
`onUnauthenticated`. A 429 has `code: "rate_limited"` and includes `retryAfterMs` when the server
|
|
550
|
+
provides a retry delay. Show the error or a wait message to the user, then honor that delay before
|
|
551
|
+
another attempt. The backend controls the lockout threshold and duration.
|
|
552
|
+
|
|
520
553
|
## Patient registration invitations
|
|
521
554
|
|
|
522
555
|
When a project invites people with a business email address to join as clinicians,
|
|
@@ -3,8 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createSessionFetch = createSessionFetch;
|
|
4
4
|
/* eslint-disable max-lines -- one refresh state machine; splitting it would scatter its shared session state. */
|
|
5
5
|
const core_1 = require("@medplum/core");
|
|
6
|
+
const ovok_error_1 = require("../errors/ovok-error");
|
|
6
7
|
const account_storage_1 = require("./account-storage");
|
|
7
8
|
const session_http_1 = require("./session-http");
|
|
9
|
+
const LoginError_1 = require("./types/LoginError");
|
|
10
|
+
/** These POSTs authenticate a new session; a failed attempt must not invalidate an existing one. */
|
|
11
|
+
const isSignInRequest = (url, options, normalizedBaseUrl) => {
|
|
12
|
+
var _a;
|
|
13
|
+
if (((_a = options === null || options === void 0 ? void 0 : options.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== "POST") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const baseUrl = new URL(normalizedBaseUrl);
|
|
17
|
+
const requestUrl = new URL(url, normalizedBaseUrl);
|
|
18
|
+
if (requestUrl.origin !== baseUrl.origin) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const path = (requestUrl.pathname.startsWith(baseUrl.pathname)
|
|
22
|
+
? requestUrl.pathname.slice(baseUrl.pathname.length)
|
|
23
|
+
: requestUrl.pathname.slice(1)).replace(/^\/+|\/+$/g, "");
|
|
24
|
+
return [
|
|
25
|
+
/^auth\/login$/,
|
|
26
|
+
/^v2\/auth\/login$/,
|
|
27
|
+
/^auth\/tenant\/(?:Patient|Practitioner)\/login\/(?:start|mfa|token)$/,
|
|
28
|
+
/^v1\/saas\/login(?:\/token)?$/,
|
|
29
|
+
].some((route) => route.test(path));
|
|
30
|
+
};
|
|
8
31
|
/**
|
|
9
32
|
* The client's `fetch`: request timeouts, token-refresh retries within the access token's
|
|
10
33
|
* lifetime, and tracking why a session ended, which `onUnauthenticated` reports.
|
|
@@ -28,6 +51,15 @@ function createSessionFetch({ config, storage, fetchImplementation, normalizedBa
|
|
|
28
51
|
};
|
|
29
52
|
const fetchOnce = async (url, options) => {
|
|
30
53
|
const response = await (0, session_http_1.fetchWithTimeout)(fetchImplementation, url, options, config.requestTimeoutMs);
|
|
54
|
+
const signInRequest = isSignInRequest(url, options, normalizedBaseUrl);
|
|
55
|
+
const retry = !response.ok && !isTokenRefresh(url, options)
|
|
56
|
+
? { milliseconds: await (0, session_http_1.retryAfterMs)(response) }
|
|
57
|
+
: undefined;
|
|
58
|
+
if ((retry === null || retry === void 0 ? void 0 : retry.milliseconds) !== undefined &&
|
|
59
|
+
(response.status === 429 || response.status === 503) &&
|
|
60
|
+
options) {
|
|
61
|
+
options.maxRetryTime = 0;
|
|
62
|
+
}
|
|
31
63
|
const requestUrl = new URL(url, normalizedBaseUrl);
|
|
32
64
|
if (isTokenRefresh(url, options)) {
|
|
33
65
|
if (response.status === 400) {
|
|
@@ -63,12 +95,18 @@ function createSessionFetch({ config, storage, fetchImplementation, normalizedBa
|
|
|
63
95
|
status: response.status,
|
|
64
96
|
};
|
|
65
97
|
}
|
|
66
|
-
else if (response.status === 401) {
|
|
98
|
+
else if (response.status === 401 && !signInRequest) {
|
|
67
99
|
unauthenticatedReason = { type: "request-unauthorized", status: 401 };
|
|
68
100
|
}
|
|
69
|
-
|
|
101
|
+
const responseWithMetadata = isTokenRefresh(url, options)
|
|
70
102
|
? response
|
|
71
|
-
: (0, session_http_1.withHttpErrorMetadata)(response);
|
|
103
|
+
: await (0, session_http_1.withHttpErrorMetadata)(response, retry);
|
|
104
|
+
if (signInRequest && (response.status === 401 || response.status === 429)) {
|
|
105
|
+
const status = response.status;
|
|
106
|
+
const details = (0, ovok_error_1.readOvokError)({ outcome: await responseWithMetadata.clone().json() });
|
|
107
|
+
throw new LoginError_1.LoginError(status === 401 ? "invalid_credentials" : "rate_limited", details.message, [], details.requestId, status, details.retryAfterMs);
|
|
108
|
+
}
|
|
109
|
+
return responseWithMetadata;
|
|
72
110
|
};
|
|
73
111
|
const fetchWithRefreshRetry = async (url, options) => {
|
|
74
112
|
var _a, _b, _c, _d, _e;
|
|
@@ -150,7 +188,7 @@ function createSessionFetch({ config, storage, fetchImplementation, normalizedBa
|
|
|
150
188
|
break;
|
|
151
189
|
}
|
|
152
190
|
const backoffMs = (0, session_http_1.refreshBackoffMs)(attempt);
|
|
153
|
-
const waitMs = Math.min(Math.max((_b = (0, session_http_1.retryAfterMs)(lastResponse)) !== null && _b !== void 0 ? _b : 0, backoffMs), remainingMs);
|
|
191
|
+
const waitMs = Math.min(Math.max((_b = (await (0, session_http_1.retryAfterMs)(lastResponse))) !== null && _b !== void 0 ? _b : 0, backoffMs), remainingMs);
|
|
154
192
|
if (waitMs >= remainingMs) {
|
|
155
193
|
await (0, session_http_1.waitForRetry)(remainingMs, signal);
|
|
156
194
|
break;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export declare const retryAfterMs: (response: Response) => number | undefined
|
|
2
|
-
export declare const withHttpErrorMetadata: (response: Response
|
|
1
|
+
export declare const retryAfterMs: (response: Response) => Promise<number | undefined>;
|
|
2
|
+
export declare const withHttpErrorMetadata: (response: Response, retry?: {
|
|
3
|
+
milliseconds: number | undefined;
|
|
4
|
+
}) => Promise<Response>;
|
|
3
5
|
export declare const waitForRetry: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
|
|
4
6
|
export declare const isTemporaryRefreshFailure: (status: number) => boolean;
|
|
5
7
|
export declare const refreshUnavailableError: (response?: Response) => Error;
|
|
@@ -2,23 +2,31 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.refreshBackoffMs = exports.fetchWithTimeout = exports.isNetworkError = exports.refreshResponseError = exports.refreshUnavailableError = exports.isTemporaryRefreshFailure = exports.waitForRetry = exports.withHttpErrorMetadata = exports.retryAfterMs = void 0;
|
|
4
4
|
const ovok_error_utils_1 = require("../errors/ovok-error-utils");
|
|
5
|
-
const retryAfterMs = (response) => {
|
|
5
|
+
const retryAfterMs = async (response) => {
|
|
6
6
|
var _a, _b;
|
|
7
7
|
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");
|
|
8
|
-
if (
|
|
9
|
-
|
|
8
|
+
if (value) {
|
|
9
|
+
const seconds = Number(value);
|
|
10
|
+
if (Number.isFinite(seconds)) {
|
|
11
|
+
return Math.max(0, seconds * 1000);
|
|
12
|
+
}
|
|
13
|
+
const date = Date.parse(value);
|
|
14
|
+
if (Number.isFinite(date)) {
|
|
15
|
+
return Math.max(0, date - Date.now());
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const body = await response.clone().json();
|
|
20
|
+
return typeof body.retryAfterSec === "number" ? Math.max(0, body.retryAfterSec * 1000) : undefined;
|
|
10
21
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return Math.max(0, seconds * 1000);
|
|
22
|
+
catch (_c) {
|
|
23
|
+
return undefined;
|
|
14
24
|
}
|
|
15
|
-
const date = Date.parse(value);
|
|
16
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
|
|
17
25
|
};
|
|
18
26
|
exports.retryAfterMs = retryAfterMs;
|
|
19
27
|
const timeoutError = () => (0, ovok_error_utils_1.namedError)("TimeoutError", "The request timed out.");
|
|
20
|
-
const withHttpErrorMetadata = async (response) => {
|
|
21
|
-
var _a
|
|
28
|
+
const withHttpErrorMetadata = async (response, retry) => {
|
|
29
|
+
var _a;
|
|
22
30
|
if (response.ok || typeof response.clone !== "function") {
|
|
23
31
|
return response;
|
|
24
32
|
}
|
|
@@ -28,7 +36,7 @@ const withHttpErrorMetadata = async (response) => {
|
|
|
28
36
|
bodyText = await response.clone().text();
|
|
29
37
|
body = JSON.parse(bodyText);
|
|
30
38
|
}
|
|
31
|
-
catch (
|
|
39
|
+
catch (_b) {
|
|
32
40
|
body = undefined;
|
|
33
41
|
}
|
|
34
42
|
const outcome = body && typeof body === "object" && body.resourceType === "OperationOutcome"
|
|
@@ -42,8 +50,8 @@ const withHttpErrorMetadata = async (response) => {
|
|
|
42
50
|
? Object.fromEntries(Object.entries(envelope).filter(([key]) => ["code", "error", "message", "requestId", "retryAfterSec", "status", "statusCode", "errors", "missing"].includes(key)))
|
|
43
51
|
: { message: typeof body === "string" ? body : response.statusText };
|
|
44
52
|
const status = response.status;
|
|
45
|
-
const retryAfter =
|
|
46
|
-
const requestId = (
|
|
53
|
+
const retryAfter = retry ? retry.milliseconds : await (0, exports.retryAfterMs)(response);
|
|
54
|
+
const requestId = (_a = response.headers.get("x-request-id")) !== null && _a !== void 0 ? _a : (typeof (envelope === null || envelope === void 0 ? void 0 : envelope.requestId) === "string" ? envelope.requestId : undefined);
|
|
47
55
|
const message = typeof (envelope === null || envelope === void 0 ? void 0 : envelope.message) === "string"
|
|
48
56
|
? envelope.message
|
|
49
57
|
: typeof (nestedError === null || nestedError === void 0 ? void 0 : nestedError.message) === "string"
|
|
@@ -79,6 +87,7 @@ const withHttpErrorMetadata = async (response) => {
|
|
|
79
87
|
];
|
|
80
88
|
const headers = new Headers(response.headers);
|
|
81
89
|
headers.delete("content-length");
|
|
90
|
+
headers.delete("content-encoding");
|
|
82
91
|
headers.set("content-type", "application/fhir+json");
|
|
83
92
|
return new Response(JSON.stringify(enrichedOutcome), {
|
|
84
93
|
status,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { LoginResponse } from "./LoginResponse";
|
|
2
2
|
import type { StartLoginResponse } from "./StartLoginResponse";
|
|
3
|
-
/** A
|
|
3
|
+
/** A typed sign-in error with fields apps can branch on without parsing prose. */
|
|
4
4
|
export declare class LoginError extends Error {
|
|
5
|
-
readonly code: "profile_not_found" | "tenant_code_required" | "tenant_code_not_found";
|
|
5
|
+
readonly code: "profile_not_found" | "tenant_code_required" | "tenant_code_not_found" | "invalid_credentials" | "rate_limited";
|
|
6
6
|
readonly profiles: LoginProfile[];
|
|
7
|
-
readonly status
|
|
7
|
+
readonly status: 400 | 401 | 429;
|
|
8
8
|
readonly requestId: string | undefined;
|
|
9
|
-
|
|
9
|
+
readonly retryAfterMs: number | undefined;
|
|
10
|
+
constructor(code: "profile_not_found" | "tenant_code_required" | "tenant_code_not_found" | "invalid_credentials" | "rate_limited", message: string, profiles?: LoginProfile[], requestId?: string, status?: 400 | 401 | 429, retryAfterMs?: number);
|
|
10
11
|
}
|
|
11
12
|
export type LoginProfile = NonNullable<StartLoginResponse["profiles"]>[number];
|
|
12
13
|
export type TenantLoginStart = {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LoginError = void 0;
|
|
4
|
-
/** A
|
|
4
|
+
/** A typed sign-in error with fields apps can branch on without parsing prose. */
|
|
5
5
|
class LoginError extends Error {
|
|
6
|
-
constructor(code, message, profiles = [], requestId) {
|
|
6
|
+
constructor(code, message, profiles = [], requestId, status = 400, retryAfterMs) {
|
|
7
7
|
super(message);
|
|
8
8
|
this.code = code;
|
|
9
9
|
this.profiles = profiles;
|
|
10
|
-
this.status = 400;
|
|
11
10
|
this.name = "LoginError";
|
|
11
|
+
this.status = status;
|
|
12
12
|
this.requestId = requestId;
|
|
13
|
+
this.retryAfterMs = retryAfterMs;
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
exports.LoginError = LoginError;
|
|
@@ -9,9 +9,10 @@ function serializeChangeHistoryQuery(params = {}) {
|
|
|
9
9
|
if (value === undefined) {
|
|
10
10
|
continue;
|
|
11
11
|
}
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
const name = key === "types" ? "types[]" : key;
|
|
14
|
+
for (const item of value) {
|
|
15
|
+
search.append(name, item);
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
else {
|
|
@@ -1,28 +1,41 @@
|
|
|
1
|
-
export type ChangeHistoryKey = "deviceCreated" | "deviceDeleted" | "deviceProjectChanged" | "deviceLocationChanged" | "deviceZoneChanged" | "deviceRenamed" | "locationCreated" | "locationDeleted" | "locationRenamed" | "locationZoneChanged" | "locationFloorChanged" | "locationRoomChanged" | "locationBedChanged" | "zoneCreated" | "zoneDeleted" | "zoneRenamed";
|
|
1
|
+
export type ChangeHistoryKey = "residentDeleted" | "residentMaidenNameChanged" | "orgThresholdRangeChanged" | "patientSignalsThresholdSet" | "patientSignalsThresholdRemoved" | "orgSignalsSettingsChanged" | "userInvited" | "userRenamed" | "userRoleChanged" | "userRemoved" | "roleCreated" | "roleDeleted" | "rolePermissionsChanged" | "residentAssignedToDeviceAndLocation" | "residentUnassignedFromDevicesAndLocation" | "residentUnassignedFromDevicesAndLocationWithZone" | "residentUnassignedFromDeviceAndLocation" | "residentDeviceAssigned" | "residentDeviceUnassigned" | "residentMovedToLocation" | "residentRemovedFromLocation" | "residentMovedToZone" | "residentRemovedFromZone" | "patientThresholdMetricSetToIndividual" | "patientThresholdMetricSetToGlobal" | "patientTimeOutOfBedMetricSetToIndividual" | "patientTimeOutOfBedMetricSetToGlobal" | "orgTimeOutOfBedMetricUpdated" | "residentDischarged" | "residentReactivated" | "residentArchived" | "residentNameChanged" | "orgThresholdMetricUpdated" | "patientThresholdMetricUpdated" | "deviceCreated" | "deviceDeleted" | "deviceProjectChanged" | "deviceLocationChanged" | "deviceZoneChanged" | "deviceRenamed" | "locationCreated" | "locationDeleted" | "locationRenamed" | "locationZoneChanged" | "locationFloorChanged" | "locationRoomChanged" | "locationBedChanged" | "zoneCreated" | "zoneDeleted" | "zoneRenamed";
|
|
2
|
+
export type ChangeHistoryType = "Threshold" | "Location" | "Zone" | "Device" | "Resident" | "Access";
|
|
3
|
+
export type ChangeHistoryScope = "all" | "global";
|
|
4
|
+
export type ChangeHistorySort = "asc" | "desc";
|
|
5
|
+
export type ChangeHistoryAction = "created" | "deleted" | "renamed" | "moved" | "assigned" | "unassigned" | "discharged" | "reactivated" | "thresholdChanged";
|
|
6
|
+
export type ChangeHistoryResident = {
|
|
7
|
+
patientId: string;
|
|
8
|
+
residentId: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type ChangeHistoryReference = `Patient/${string}` | `Device/${string}` | `Location/${string}` | `Organization/${string}` | `Practitioner/${string}` | `AccessPolicy/${string}`;
|
|
2
11
|
export type ChangeHistoryRow = {
|
|
3
12
|
id: string;
|
|
4
|
-
type:
|
|
5
|
-
user:
|
|
6
|
-
residentId: string
|
|
7
|
-
deviceId: string
|
|
13
|
+
type: ChangeHistoryType;
|
|
14
|
+
user: string;
|
|
15
|
+
residentId: string;
|
|
16
|
+
deviceId: string;
|
|
17
|
+
resident: ChangeHistoryResident | null;
|
|
8
18
|
locationId?: string | null;
|
|
9
19
|
zoneId?: string | null;
|
|
10
20
|
time: string;
|
|
11
21
|
details: {
|
|
12
22
|
key: ChangeHistoryKey | (string & {});
|
|
13
|
-
values: Record<string,
|
|
23
|
+
values: Record<string, string | number | null>;
|
|
14
24
|
};
|
|
15
25
|
};
|
|
16
26
|
export type ChangeHistoryQuery = {
|
|
17
27
|
page?: number;
|
|
18
28
|
count?: number;
|
|
19
|
-
types?: readonly
|
|
20
|
-
scope?:
|
|
21
|
-
userId?: string;
|
|
22
|
-
residentId?: string;
|
|
23
|
-
deviceId?: string;
|
|
24
|
-
locationId?: string;
|
|
25
|
-
zoneId?: string;
|
|
29
|
+
types?: readonly ChangeHistoryType[];
|
|
30
|
+
scope?: ChangeHistoryScope;
|
|
31
|
+
userId?: string | readonly string[];
|
|
32
|
+
residentId?: string | readonly string[];
|
|
33
|
+
deviceId?: string | readonly string[];
|
|
34
|
+
locationId?: string | readonly string[];
|
|
35
|
+
zoneId?: string | readonly string[];
|
|
36
|
+
relatedTo?: ChangeHistoryReference | readonly ChangeHistoryReference[];
|
|
37
|
+
actions?: ChangeHistoryAction | readonly ChangeHistoryAction[];
|
|
38
|
+
sort?: ChangeHistorySort;
|
|
26
39
|
startDate?: string;
|
|
27
40
|
endDate?: string;
|
|
28
41
|
};
|
|
@@ -103,8 +103,8 @@ function mapOvokApiError(error, options = {}) {
|
|
|
103
103
|
}
|
|
104
104
|
const details = (0, exports.readOvokError)(error);
|
|
105
105
|
const typedStatuses = options.includeAccessErrors
|
|
106
|
-
? [403, 404, 409, 422, 424, 503]
|
|
107
|
-
: [409, 422, 424, 503];
|
|
106
|
+
? [403, 404, 409, 410, 422, 424, 503]
|
|
107
|
+
: [409, 410, 422, 424, 503];
|
|
108
108
|
return typedStatuses.includes((_a = details.status) !== null && _a !== void 0 ? _a : 0)
|
|
109
109
|
? new OvokApiError(details, error)
|
|
110
110
|
: error;
|
|
@@ -128,6 +128,8 @@ export type SlimMedicalEpisodeValue = {
|
|
|
128
128
|
observedAt: string | null;
|
|
129
129
|
recordedAt: string;
|
|
130
130
|
};
|
|
131
|
+
/** STALE closes an out-of-bed episode that stayed open for 48 hours. */
|
|
132
|
+
export type SlimEpisodeCloseReason = "RECOVERED" | "TIMEOUT" | "ACKNOWLEDGED" | "CROSSED" | "THRESHOLD_CHANGED" | "AWAY" | "STALE" | "MANUAL";
|
|
131
133
|
export type SlimMedicalEpisodeCore = {
|
|
132
134
|
id: string;
|
|
133
135
|
patientId: string;
|
|
@@ -137,7 +139,7 @@ export type SlimMedicalEpisodeCore = {
|
|
|
137
139
|
status: string;
|
|
138
140
|
openedAt: string;
|
|
139
141
|
closedAt: string | null;
|
|
140
|
-
closeReason: string | null;
|
|
142
|
+
closeReason: SlimEpisodeCloseReason | (string & {}) | null;
|
|
141
143
|
lastSampleAt: string | null;
|
|
142
144
|
durationMs: number | null;
|
|
143
145
|
observationCount: number | null;
|
|
@@ -136,6 +136,7 @@ export declare class OvokClient extends MedplumClient {
|
|
|
136
136
|
* account differs from `account`: the rest of a reading is never written under someone else's sign-in.
|
|
137
137
|
*/
|
|
138
138
|
private executeObservationBundle;
|
|
139
|
+
private mapError;
|
|
139
140
|
private bindMethods;
|
|
140
141
|
/**
|
|
141
142
|
* Gets the `SubscriptionManager` for WebSocket subscriptions.
|
|
@@ -43,6 +43,7 @@ const errors_1 = require("./auth/errors");
|
|
|
43
43
|
const authMethods = __importStar(require("./auth/methods"));
|
|
44
44
|
const session_fetch_1 = require("./auth/session-fetch");
|
|
45
45
|
const session_http_1 = require("./auth/session-http");
|
|
46
|
+
const LoginError_1 = require("./auth/types/LoginError");
|
|
46
47
|
const botMethods = __importStar(require("./bot/methods"));
|
|
47
48
|
const changeHistoryMethods = __importStar(require("./change-history/methods"));
|
|
48
49
|
const aiMethods = __importStar(require("./chat/ai/methods"));
|
|
@@ -125,7 +126,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
125
126
|
this.refreshState = session.refreshState;
|
|
126
127
|
this.socialLoginClientId = (_e = config.socialLoginClientId) !== null && _e !== void 0 ? _e : "";
|
|
127
128
|
this.addEventListener("profileRefreshed", () => this.syncSavedAccountProfile());
|
|
128
|
-
this.bindMethods(authMethods);
|
|
129
|
+
this.bindMethods(authMethods, (error) => error instanceof LoginError_1.LoginError ? error : this.mapError(error));
|
|
129
130
|
this.bindMethods(observationMethods);
|
|
130
131
|
this.bindMethods(questionnaireResponseMethods);
|
|
131
132
|
this.bindMethods(aiMethods);
|
|
@@ -261,7 +262,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
261
262
|
((_b = previousLogin.profile) === null || _b === void 0 ? void 0 : _b.reference) !== accountId) {
|
|
262
263
|
await this.restoreActiveLogin(previousLogin);
|
|
263
264
|
}
|
|
264
|
-
throw error;
|
|
265
|
+
throw this.mapError(error);
|
|
265
266
|
}
|
|
266
267
|
}
|
|
267
268
|
/**
|
|
@@ -284,7 +285,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
284
285
|
}
|
|
285
286
|
catch (error) {
|
|
286
287
|
if (!sessionClient.getSavedAccounts().some((account) => account.id === accountId && account.expired)) {
|
|
287
|
-
throw error;
|
|
288
|
+
throw this.mapError(error);
|
|
288
289
|
}
|
|
289
290
|
}
|
|
290
291
|
this.removeSavedAccount(accountId);
|
|
@@ -326,11 +327,21 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
326
327
|
}
|
|
327
328
|
/** Reads Ovok's composed FHIR and public-route capability statement. */
|
|
328
329
|
async getCapabilityStatement(version = "R4", requestOptions) {
|
|
329
|
-
|
|
330
|
+
try {
|
|
331
|
+
return await this.get(`/fhir/${version}/metadata`, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
throw this.mapError(error);
|
|
335
|
+
}
|
|
330
336
|
}
|
|
331
337
|
/** Returns the non-FHIR public routes published by the backend statement. */
|
|
332
338
|
async getPublicApiEndpoints(version = "R4", requestOptions) {
|
|
333
|
-
|
|
339
|
+
try {
|
|
340
|
+
return (0, capability_1.getOvokPublicApiEndpoints)(await this.getCapabilityStatement(version, ...(0, request_options_1.optionalArg)(requestOptions)));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
throw this.mapError(error);
|
|
344
|
+
}
|
|
334
345
|
}
|
|
335
346
|
/**
|
|
336
347
|
* Saves one reading's observations, main first, with conditional creates so a retry cannot create a duplicate.
|
|
@@ -518,16 +529,24 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
518
529
|
* account differs from `account`: the rest of a reading is never written under someone else's sign-in.
|
|
519
530
|
*/
|
|
520
531
|
async executeObservationBundle(bundle, requestOptions, account = this.accountKey()) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
532
|
+
try {
|
|
533
|
+
const response = await (0, observation_bundle_utils_1.executeObservationBundle)(bundle, (request) => {
|
|
534
|
+
if (this.accountKey() !== account) {
|
|
535
|
+
throw new Error("The signed-in account changed during the save; the rest of it was not sent.");
|
|
536
|
+
}
|
|
537
|
+
return this.executeBatch(request, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
538
|
+
});
|
|
539
|
+
this.invalidateSearches("Observation");
|
|
540
|
+
return response;
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
throw this.mapError(error);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
mapError(error) {
|
|
547
|
+
return (0, ovok_error_1.mapOvokApiError)((0, rate_limit_error_1.mapRateLimitError)(error));
|
|
529
548
|
}
|
|
530
|
-
bindMethods(methods, mapError = (error) =>
|
|
549
|
+
bindMethods(methods, mapError = (error) => this.mapError(error)) {
|
|
531
550
|
for (const [name, method] of Object.entries(methods)) {
|
|
532
551
|
const boundMethod = method.bind(this);
|
|
533
552
|
this[name] = async (...args) => {
|
|
@@ -7,18 +7,22 @@ export declare function listSlimPatients(this: OvokClient, params?: SlimPatientL
|
|
|
7
7
|
export declare function getSlimPatient(this: OvokClient, patientId: string, requestOptions?: MedplumRequestOptions): Promise<SlimPatient>;
|
|
8
8
|
export declare function getSlimPatientEverything(this: OvokClient, patientId: string, requestOptions?: MedplumRequestOptions): Promise<Bundle<Resource>>;
|
|
9
9
|
export declare function getSlimPatientCurrentVitals(this: OvokClient, patientId: string, requestOptions?: MedplumRequestOptions): Promise<Record<string, unknown>>;
|
|
10
|
+
/** 422 when organizationId has no locationId or deviceId, or the assigned devices span zones. */
|
|
10
11
|
export declare function createSlimPatient(this: OvokClient, body: SlimCreatePatient, requestOptions?: MedplumRequestOptions): Promise<SlimPatient>;
|
|
12
|
+
/** Replaces editable fields; omitting locationId unassigns devices; 422 for null, empty, or [] deviceId with a location or devices across zones. Names are trimmed. */
|
|
11
13
|
export declare function updateSlimPatient(this: OvokClient, patientId: string, body: SlimUpdatePatient, requestOptions?: MedplumRequestOptions): Promise<SlimPatient>;
|
|
14
|
+
/** 409 while the resident has a device; unassign it first with updateSlimPatient and no locationId. */
|
|
12
15
|
export declare function deleteSlimPatient(this: OvokClient, patientId: string, requestOptions?: MedplumRequestOptions): Promise<void>;
|
|
13
16
|
export declare function listSlimDevices(this: OvokClient, params?: SlimDeviceListQuery, requestOptions?: MedplumRequestOptions): Promise<SlimPage<SlimDeviceListItem>>;
|
|
14
17
|
export declare function getSlimDevice(this: OvokClient, deviceId: string, requestOptions?: MedplumRequestOptions): Promise<SlimDevice>;
|
|
18
|
+
/** 409 when another customer holds the serial; 422 when this project already uses it or its organization differs from the room's zone. */
|
|
15
19
|
export declare function createSlimDevice(this: OvokClient, body: SlimCreateDevice, requestOptions?: MedplumRequestOptions): Promise<SlimDevice>;
|
|
20
|
+
/** 409 when another customer holds the serial; 422 when this project already uses it or its organization differs from the room's zone. Renaming moves the serial, so old-serial readings stop and live values return after a reading under the new serial. */
|
|
16
21
|
export declare function updateSlimDevice(this: OvokClient, deviceId: string, body: SlimUpdateDevice, requestOptions?: MedplumRequestOptions): Promise<SlimDevice>;
|
|
17
22
|
export declare function deleteSlimDevice(this: OvokClient, deviceId: string, requestOptions?: MedplumRequestOptions): Promise<void>;
|
|
18
23
|
export declare function assignSlimDeviceToProject(this: OvokClient, body: SlimAssignDevice, requestOptions?: MedplumRequestOptions): Promise<SlimDevice>;
|
|
19
24
|
export declare function listSlimLiveDevices(this: OvokClient, params?: SlimLiveQuery, requestOptions?: MedplumRequestOptions): Promise<SlimPage<Record<string, unknown>>>;
|
|
20
25
|
export declare function listSlimLiveTelemetry(this: OvokClient, params?: SlimLiveQuery, requestOptions?: MedplumRequestOptions): Promise<SlimLiveTelemetry>;
|
|
21
|
-
export declare function getSlimDeviceSleepStatus(this: OvokClient, deviceId: string, requestOptions?: MedplumRequestOptions): Promise<Record<string, unknown>>;
|
|
22
26
|
export declare function getSlimDeviceTelemetry(this: OvokClient, deviceId: string, requestOptions?: MedplumRequestOptions): Promise<SlimDeviceTelemetry>;
|
|
23
27
|
export declare function listSlimEffectiveObservationIds(this: OvokClient, requestOptions?: MedplumRequestOptions): Promise<Record<string, SlimObservationIds>>;
|
|
24
28
|
export declare function listSlimLastObservationIds(this: OvokClient, requestOptions?: MedplumRequestOptions): Promise<Record<string, SlimObservationIds>>;
|
|
@@ -28,6 +32,7 @@ export declare function getSlimDeviceLastObservationIds(this: OvokClient, device
|
|
|
28
32
|
export declare function listSlimNotifications(this: OvokClient, params?: SlimNotificationsQuery, requestOptions?: MedplumRequestOptions): Promise<SlimNotificationsResponse>;
|
|
29
33
|
export declare function getRecentSlimNotifications(this: OvokClient, count?: number, requestOptions?: MedplumRequestOptions): Promise<SlimRecentNotificationsResponse>;
|
|
30
34
|
export declare function listSlimAlarms(this: OvokClient, params?: SlimAlarmQuery, requestOptions?: MedplumRequestOptions): Promise<SlimAlarmsPage>;
|
|
35
|
+
/** Can take about 12 s; a 503 signals_unavailable means it was not confirmed, so repeat after retryAfterMs (safe to repeat). */
|
|
31
36
|
export declare function acknowledgeSlimAlarm(this: OvokClient, notificationId: string, requestOptions?: MedplumRequestOptions): Promise<SlimNotification>;
|
|
32
37
|
export declare function getRecentSlimAlarms(this: OvokClient, count?: number, requestOptions?: MedplumRequestOptions): Promise<SlimRecentAlarms>;
|
|
33
38
|
/** Same as `getRecentSlimAlarms`. */
|
|
@@ -35,24 +40,33 @@ export declare function getRecentAlarms(this: OvokClient, count?: number, reques
|
|
|
35
40
|
export declare function getRecentSlimOutOfBed(this: OvokClient, count?: number, requestOptions?: MedplumRequestOptions): Promise<SlimRecentOutOfBed>;
|
|
36
41
|
/** Same as `getRecentSlimOutOfBed`. */
|
|
37
42
|
export declare function getRecentOutOfBed(this: OvokClient, count?: number, requestOptions?: MedplumRequestOptions): Promise<SlimRecentOutOfBed>;
|
|
43
|
+
/** Omitting status lists new and reviewed rows; active is new, completed reviewed, and revoked hand-closed. */
|
|
38
44
|
export declare function listSlimOutOfBedNotifications(this: OvokClient, params?: Omit<SlimNotificationsQuery, "type">, requestOptions?: MedplumRequestOptions): Promise<SlimNotificationsResponse>;
|
|
39
45
|
export declare function listSlimMedicalNotifications(this: OvokClient, params?: SlimMedicalNotificationsQuery, requestOptions?: MedplumRequestOptions): Promise<SlimMedicalNotificationsPage>;
|
|
46
|
+
/** During a Signals outage, this may return the last good page from up to 10 minutes ago, without a marker. */
|
|
40
47
|
export declare function getRecentSlimMedicalNotifications(this: OvokClient, count?: number, requestOptions?: MedplumRequestOptions): Promise<SlimRecentNotificationsResponse>;
|
|
41
48
|
export declare function getSlimMedicalNotification(this: OvokClient, episodeId: string, requestOptions?: MedplumRequestOptions): Promise<SlimMedicalNotificationDetail>;
|
|
49
|
+
/** Can take about 12 s; a 503 signals_unavailable means it was not confirmed, so repeat after retryAfterMs (safe to repeat). */
|
|
42
50
|
export declare function acknowledgeSlimMedicalNotification(this: OvokClient, episodeId: string, requestOptions?: MedplumRequestOptions): Promise<SlimMedicalNotificationRow>;
|
|
43
51
|
/** Same as `acknowledgeSlimAlarm`. */
|
|
44
52
|
export declare function acknowledgeSlimNotification(this: OvokClient, notificationId: string, requestOptions?: MedplumRequestOptions): Promise<SlimNotification>;
|
|
53
|
+
/** A home with no stored limits reads as the default template, not as unset. */
|
|
45
54
|
export declare function getSlimThreshold(this: OvokClient, requestOptions?: MedplumRequestOptions): Promise<SlimThreshold>;
|
|
55
|
+
/** A 502 means Signals did not take every resident's limits and nothing was written. */
|
|
46
56
|
export declare function updateSlimThreshold(this: OvokClient, body: {
|
|
47
57
|
settings: SlimThresholdSettingsUpdateV1;
|
|
48
58
|
}, requestOptions?: MedplumRequestOptions): Promise<SlimThreshold>;
|
|
49
59
|
export declare function getSlimPatientThreshold(this: OvokClient, patientId: string, requestOptions?: MedplumRequestOptions): Promise<Record<string, unknown>>;
|
|
60
|
+
/** 409 means another save for this resident is running and nothing changed; wait briefly, then send again. */
|
|
50
61
|
export declare function updateSlimPatientThreshold(this: OvokClient, patientId: string, body: {
|
|
51
62
|
settings: SlimThresholdSettingsUpdateV1;
|
|
52
63
|
}, requestOptions?: MedplumRequestOptions): Promise<Record<string, unknown>>;
|
|
64
|
+
/** A home with no stored limits reads as the default template, not as unset. */
|
|
53
65
|
export declare function getSlimThresholdV2(this: OvokClient, requestOptions?: MedplumRequestOptions): Promise<SlimThresholdV2>;
|
|
66
|
+
/** A 502 means Signals did not take every resident's limits and nothing was written. */
|
|
54
67
|
export declare function updateSlimThresholdV2(this: OvokClient, body: SlimThresholdV2Update, requestOptions?: MedplumRequestOptions): Promise<SlimThresholdV2>;
|
|
55
68
|
export declare function getSlimPatientThresholdV2(this: OvokClient, patientId: string, requestOptions?: MedplumRequestOptions): Promise<SlimPatientThresholdV2>;
|
|
69
|
+
/** 409 means another save for this resident is running and nothing changed; wait briefly, then send again. */
|
|
56
70
|
export declare function updateSlimPatientThresholdV2(this: OvokClient, patientId: string, body: SlimThresholdV2Update, requestOptions?: MedplumRequestOptions): Promise<SlimPatientThresholdV2>;
|
|
57
71
|
export declare function getSlimTrends(this: OvokClient, params: {
|
|
58
72
|
patientId: string;
|
|
@@ -60,6 +74,7 @@ export declare function getSlimTrends(this: OvokClient, params: {
|
|
|
60
74
|
endDate: string;
|
|
61
75
|
graphType: SlimHistoricalGraphType | readonly SlimHistoricalGraphType[];
|
|
62
76
|
}, requestOptions?: MedplumRequestOptions): Promise<SlimTrends>;
|
|
77
|
+
/** At most 20 calls per minute per caller; load once per view and wait retryAfterMs after 503. */
|
|
63
78
|
export declare function getSlimRealtimeTrends(this: OvokClient, params: {
|
|
64
79
|
patientId: string;
|
|
65
80
|
startTime: string;
|
|
@@ -74,6 +89,7 @@ export declare function getSlimSleepStatistics(this: OvokClient, params: {
|
|
|
74
89
|
export declare function listSlimLocations(this: OvokClient, params?: SlimLocationQuery, requestOptions?: MedplumRequestOptions): Promise<SlimPage<SlimLocation>>;
|
|
75
90
|
export declare function getSlimLocation(this: OvokClient, locationId: string, requestOptions?: MedplumRequestOptions): Promise<SlimLocation>;
|
|
76
91
|
export declare function createSlimLocation(this: OvokClient, body: SlimCreateLocation, requestOptions?: MedplumRequestOptions): Promise<SlimLocation>;
|
|
92
|
+
/** 422 Device not found means the requested device is unreadable; nothing was saved. */
|
|
77
93
|
export declare function updateSlimLocation(this: OvokClient, locationId: string, body: SlimUpdateLocation, requestOptions?: MedplumRequestOptions): Promise<SlimLocation>;
|
|
78
94
|
export declare function deleteSlimLocation(this: OvokClient, locationId: string, requestOptions?: MedplumRequestOptions): Promise<void>;
|
|
79
95
|
export declare function listSlimZones(this: OvokClient, params?: {
|
|
@@ -15,7 +15,6 @@ exports.deleteSlimDevice = deleteSlimDevice;
|
|
|
15
15
|
exports.assignSlimDeviceToProject = assignSlimDeviceToProject;
|
|
16
16
|
exports.listSlimLiveDevices = listSlimLiveDevices;
|
|
17
17
|
exports.listSlimLiveTelemetry = listSlimLiveTelemetry;
|
|
18
|
-
exports.getSlimDeviceSleepStatus = getSlimDeviceSleepStatus;
|
|
19
18
|
exports.getSlimDeviceTelemetry = getSlimDeviceTelemetry;
|
|
20
19
|
exports.listSlimEffectiveObservationIds = listSlimEffectiveObservationIds;
|
|
21
20
|
exports.listSlimLastObservationIds = listSlimLastObservationIds;
|
|
@@ -97,12 +96,15 @@ async function getSlimPatientEverything(patientId, requestOptions) {
|
|
|
97
96
|
async function getSlimPatientCurrentVitals(patientId, requestOptions) {
|
|
98
97
|
return this.get(`/v1/slim/patient/${encodeURIComponent(patientId)}/current-vitals`, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
99
98
|
}
|
|
99
|
+
/** 422 when organizationId has no locationId or deviceId, or the assigned devices span zones. */
|
|
100
100
|
async function createSlimPatient(body, requestOptions) {
|
|
101
101
|
return this.post("/v1/slim/patient", body, undefined, requestOptions);
|
|
102
102
|
}
|
|
103
|
+
/** Replaces editable fields; omitting locationId unassigns devices; 422 for null, empty, or [] deviceId with a location or devices across zones. Names are trimmed. */
|
|
103
104
|
async function updateSlimPatient(patientId, body, requestOptions) {
|
|
104
105
|
return (await this.put(`/v1/slim/patient/${encodeURIComponent(patientId)}`, body, undefined, requestOptions));
|
|
105
106
|
}
|
|
107
|
+
/** 409 while the resident has a device; unassign it first with updateSlimPatient and no locationId. */
|
|
106
108
|
async function deleteSlimPatient(patientId, requestOptions) {
|
|
107
109
|
await this.delete(`/v1/slim/patient/${encodeURIComponent(patientId)}`, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
108
110
|
}
|
|
@@ -112,9 +114,11 @@ async function listSlimDevices(params = {}, requestOptions) {
|
|
|
112
114
|
async function getSlimDevice(deviceId, requestOptions) {
|
|
113
115
|
return this.get(`/v1/slim/device/${encodeURIComponent(deviceId)}`, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
114
116
|
}
|
|
117
|
+
/** 409 when another customer holds the serial; 422 when this project already uses it or its organization differs from the room's zone. */
|
|
115
118
|
async function createSlimDevice(body, requestOptions) {
|
|
116
119
|
return this.post("/v1/slim/device", body, undefined, requestOptions);
|
|
117
120
|
}
|
|
121
|
+
/** 409 when another customer holds the serial; 422 when this project already uses it or its organization differs from the room's zone. Renaming moves the serial, so old-serial readings stop and live values return after a reading under the new serial. */
|
|
118
122
|
async function updateSlimDevice(deviceId, body, requestOptions) {
|
|
119
123
|
return (await this.put(`/v1/slim/device/${encodeURIComponent(deviceId)}`, body, undefined, requestOptions));
|
|
120
124
|
}
|
|
@@ -132,9 +136,6 @@ async function listSlimLiveTelemetry(params = {}, requestOptions) {
|
|
|
132
136
|
validateLiveCount(params);
|
|
133
137
|
return (0, live_read_1.liveGet)(this, `/v1/slim/device/live/telemetry${(0, query_string_1.toQueryString)(params)}`, requestOptions);
|
|
134
138
|
}
|
|
135
|
-
async function getSlimDeviceSleepStatus(deviceId, requestOptions) {
|
|
136
|
-
return (0, live_read_1.liveGet)(this, `/v1/slim/device/live/cloudwatch/${encodeURIComponent(deviceId)}`, requestOptions);
|
|
137
|
-
}
|
|
138
139
|
async function getSlimDeviceTelemetry(deviceId, requestOptions) {
|
|
139
140
|
return (0, live_read_1.liveGet)(this, `/v1/slim/telemetry/${encodeURIComponent(deviceId)}`, requestOptions);
|
|
140
141
|
}
|
|
@@ -160,6 +161,7 @@ async function getRecentSlimNotifications(count, requestOptions) {
|
|
|
160
161
|
async function listSlimAlarms(params = {}, requestOptions) {
|
|
161
162
|
return this.get(`/v1/slim/notifications/alarms${(0, query_string_1.toQueryString)(params)}`, { cache: "reload", ...requestOptions });
|
|
162
163
|
}
|
|
164
|
+
/** Can take about 12 s; a 503 signals_unavailable means it was not confirmed, so repeat after retryAfterMs (safe to repeat). */
|
|
163
165
|
async function acknowledgeSlimAlarm(notificationId, requestOptions) {
|
|
164
166
|
return (await this.patch(`/v1/slim/notifications/${encodeURIComponent(notificationId)}/ack`, [], ...(0, request_options_1.optionalArg)(requestOptions)));
|
|
165
167
|
}
|
|
@@ -177,6 +179,7 @@ async function getRecentSlimOutOfBed(count, requestOptions) {
|
|
|
177
179
|
function getRecentOutOfBed(count, requestOptions) {
|
|
178
180
|
return getRecentSlimOutOfBed.call(this, count, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
179
181
|
}
|
|
182
|
+
/** Omitting status lists new and reviewed rows; active is new, completed reviewed, and revoked hand-closed. */
|
|
180
183
|
async function listSlimOutOfBedNotifications(params = {}, requestOptions) {
|
|
181
184
|
return this.get(`/v1/slim/notifications/oob${(0, query_string_1.toQueryString)(params)}`, { cache: "reload", ...requestOptions });
|
|
182
185
|
}
|
|
@@ -191,12 +194,14 @@ function validateMedicalCodes(codes) {
|
|
|
191
194
|
throw new TypeError("codes must contain at most 32 valid LOINC codes or PI.");
|
|
192
195
|
}
|
|
193
196
|
}
|
|
197
|
+
/** During a Signals outage, this may return the last good page from up to 10 minutes ago, without a marker. */
|
|
194
198
|
async function getRecentSlimMedicalNotifications(count, requestOptions) {
|
|
195
199
|
return this.get(`/v1/slim/notifications/recent/medical${(0, query_string_1.toQueryString)({ count })}`, { cache: "reload", ...requestOptions });
|
|
196
200
|
}
|
|
197
201
|
async function getSlimMedicalNotification(episodeId, requestOptions) {
|
|
198
202
|
return this.get(`/v1/slim/notifications/medical/${encodeURIComponent(episodeId)}`, { cache: "reload", ...requestOptions });
|
|
199
203
|
}
|
|
204
|
+
/** Can take about 12 s; a 503 signals_unavailable means it was not confirmed, so repeat after retryAfterMs (safe to repeat). */
|
|
200
205
|
async function acknowledgeSlimMedicalNotification(episodeId, requestOptions) {
|
|
201
206
|
return this.post(`/v1/slim/notifications/medical/${encodeURIComponent(episodeId)}/ack`, undefined, undefined, requestOptions);
|
|
202
207
|
}
|
|
@@ -204,33 +209,44 @@ async function acknowledgeSlimMedicalNotification(episodeId, requestOptions) {
|
|
|
204
209
|
function acknowledgeSlimNotification(notificationId, requestOptions) {
|
|
205
210
|
return acknowledgeSlimAlarm.call(this, notificationId, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
206
211
|
}
|
|
212
|
+
/** A home with no stored limits reads as the default template, not as unset. */
|
|
207
213
|
async function getSlimThreshold(requestOptions) {
|
|
208
214
|
return this.get("/v1/slim/threshold", ...(0, request_options_1.optionalArg)(requestOptions));
|
|
209
215
|
}
|
|
216
|
+
/** A 502 means Signals did not take every resident's limits and nothing was written. */
|
|
210
217
|
async function updateSlimThreshold(body, requestOptions) {
|
|
211
|
-
return (await this.put("/v1/slim/threshold", body, undefined,
|
|
218
|
+
return (await this.put("/v1/slim/threshold", body, undefined, {
|
|
219
|
+
...requestOptions, maxRetries: 0, maxRetryTime: 0,
|
|
220
|
+
}));
|
|
212
221
|
}
|
|
213
222
|
async function getSlimPatientThreshold(patientId, requestOptions) {
|
|
214
223
|
return this.get(`/v1/slim/threshold/patient/${encodeURIComponent(patientId)}`, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
215
224
|
}
|
|
225
|
+
/** 409 means another save for this resident is running and nothing changed; wait briefly, then send again. */
|
|
216
226
|
async function updateSlimPatientThreshold(patientId, body, requestOptions) {
|
|
217
|
-
return (await this.put(`/v1/slim/threshold/patient/${encodeURIComponent(patientId)}`, body, undefined, requestOptions));
|
|
227
|
+
return (await this.put(`/v1/slim/threshold/patient/${encodeURIComponent(patientId)}`, body, undefined, { ...requestOptions, maxRetries: 0, maxRetryTime: 0 }));
|
|
218
228
|
}
|
|
229
|
+
/** A home with no stored limits reads as the default template, not as unset. */
|
|
219
230
|
async function getSlimThresholdV2(requestOptions) {
|
|
220
231
|
return this.get("/v2/slim/threshold", ...(0, request_options_1.optionalArg)(requestOptions));
|
|
221
232
|
}
|
|
233
|
+
/** A 502 means Signals did not take every resident's limits and nothing was written. */
|
|
222
234
|
async function updateSlimThresholdV2(body, requestOptions) {
|
|
223
|
-
return (await this.put("/v2/slim/threshold", body, undefined,
|
|
235
|
+
return (await this.put("/v2/slim/threshold", body, undefined, {
|
|
236
|
+
...requestOptions, maxRetries: 0, maxRetryTime: 0,
|
|
237
|
+
}));
|
|
224
238
|
}
|
|
225
239
|
async function getSlimPatientThresholdV2(patientId, requestOptions) {
|
|
226
240
|
return this.get(`/v2/slim/threshold/patient/${encodeURIComponent(patientId)}`, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
227
241
|
}
|
|
242
|
+
/** 409 means another save for this resident is running and nothing changed; wait briefly, then send again. */
|
|
228
243
|
async function updateSlimPatientThresholdV2(patientId, body, requestOptions) {
|
|
229
|
-
return (await this.put(`/v2/slim/threshold/patient/${encodeURIComponent(patientId)}`, body, undefined, requestOptions));
|
|
244
|
+
return (await this.put(`/v2/slim/threshold/patient/${encodeURIComponent(patientId)}`, body, undefined, { ...requestOptions, maxRetries: 0, maxRetryTime: 0 }));
|
|
230
245
|
}
|
|
231
246
|
async function getSlimTrends(params, requestOptions) {
|
|
232
247
|
return this.get(`/v1/slim/trends/graphs${(0, query_string_1.toQueryString)(params)}`, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
233
248
|
}
|
|
249
|
+
/** At most 20 calls per minute per caller; load once per view and wait retryAfterMs after 503. */
|
|
234
250
|
async function getSlimRealtimeTrends(params, requestOptions) {
|
|
235
251
|
return this.get(`/v1/slim/trends/realtime${(0, query_string_1.toQueryString)(params)}`, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
236
252
|
}
|
|
@@ -246,6 +262,7 @@ async function getSlimLocation(locationId, requestOptions) {
|
|
|
246
262
|
async function createSlimLocation(body, requestOptions) {
|
|
247
263
|
return this.post("/v1/slim/location", body, undefined, requestOptions);
|
|
248
264
|
}
|
|
265
|
+
/** 422 Device not found means the requested device is unreadable; nothing was saved. */
|
|
249
266
|
async function updateSlimLocation(locationId, body, requestOptions) {
|
|
250
267
|
return (await this.put(`/v1/slim/location/${encodeURIComponent(locationId)}`, body, undefined, requestOptions));
|
|
251
268
|
}
|
|
@@ -38,9 +38,10 @@ export type SlimPatientListItem = {
|
|
|
38
38
|
locationName: string | null;
|
|
39
39
|
deviceId: string | null;
|
|
40
40
|
deviceName: string | null;
|
|
41
|
-
medicalSettings:
|
|
42
|
-
bedActivitySettings:
|
|
41
|
+
medicalSettings: SlimSettingsScope | null;
|
|
42
|
+
bedActivitySettings: SlimSettingsScope | null;
|
|
43
43
|
};
|
|
44
|
+
export type SlimSettingsScope = "inactive" | "global" | "individual";
|
|
44
45
|
export type SlimPatientListQuery = {
|
|
45
46
|
page?: number;
|
|
46
47
|
count?: number;
|
|
@@ -51,8 +52,8 @@ export type SlimPatientListQuery = {
|
|
|
51
52
|
includeDischargedRecent?: boolean;
|
|
52
53
|
organizationId?: string;
|
|
53
54
|
hasDevice?: boolean;
|
|
54
|
-
medicalSettings?:
|
|
55
|
-
bedActivitySettings?:
|
|
55
|
+
medicalSettings?: SlimSettingsScope;
|
|
56
|
+
bedActivitySettings?: SlimSettingsScope;
|
|
56
57
|
};
|
|
57
58
|
export type SlimCreatePatient = {
|
|
58
59
|
name: SlimHumanName[];
|
|
@@ -63,6 +64,7 @@ export type SlimCreatePatient = {
|
|
|
63
64
|
locationId?: string;
|
|
64
65
|
admissionDate?: string;
|
|
65
66
|
};
|
|
67
|
+
/** Replaces the editable fields; omitting locationId unassigns the resident's devices. */
|
|
66
68
|
export type SlimUpdatePatient = Partial<SlimCreatePatient> & {
|
|
67
69
|
residentState?: SlimResidentState;
|
|
68
70
|
dischargeDate?: string;
|
|
@@ -222,6 +224,7 @@ export type SlimThresholdSettingsUpdateV1 = Record<string, SlimThresholdSettingU
|
|
|
222
224
|
export type SlimThresholdV2Setting = {
|
|
223
225
|
mode: string;
|
|
224
226
|
deviationPercent: number | null;
|
|
227
|
+
/** Null when the bed window is 23 hours 59 minutes or longer. */
|
|
225
228
|
baselineAverage: number | null;
|
|
226
229
|
effectiveBand: {
|
|
227
230
|
min: number;
|
|
@@ -239,7 +242,7 @@ export type SignalsSyncStatus = {
|
|
|
239
242
|
reason: string;
|
|
240
243
|
} | null;
|
|
241
244
|
export type SlimThresholdV2 = {
|
|
242
|
-
id
|
|
245
|
+
id: string | null;
|
|
243
246
|
projectId?: string;
|
|
244
247
|
status?: "active" | "retired";
|
|
245
248
|
date?: string;
|
|
@@ -122,7 +122,6 @@ exports.OVOK_CORE_API_REQUIREMENTS = {
|
|
|
122
122
|
{ method: "PUT", path: "/v1/slim/device/assign/project", group: "Slim/Device" },
|
|
123
123
|
{ method: "GET", path: "/v1/slim/device/live", group: "Slim/Device" },
|
|
124
124
|
{ method: "GET", path: "/v1/slim/device/live/telemetry", group: "Slim/Device" },
|
|
125
|
-
{ method: "GET", path: "/v1/slim/device/live/cloudwatch/:deviceId", group: "Slim/Device" },
|
|
126
125
|
{ method: "GET", path: "/v1/slim/telemetry/:deviceId", group: "Slim/Telemetry" },
|
|
127
126
|
{ method: "GET", path: "/v1/slim/telemetry/effectiveIds", group: "Slim/Telemetry" },
|
|
128
127
|
{ method: "GET", path: "/v1/slim/telemetry/lastIds", group: "Slim/Telemetry" },
|
|
@@ -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.
|
|
51
|
+
version: "0.4.44",
|
|
52
52
|
},
|
|
53
53
|
fhirVersion: "4.0.1",
|
|
54
54
|
format: ["json"],
|
|
@@ -7,6 +7,7 @@ export declare const useRecentSlimNotifications: (count?: number) => OvokRequest
|
|
|
7
7
|
export declare const useSlimAlarms: (params?: SlimAlarmQuery) => OvokRequestState<SlimAlarmsPage>;
|
|
8
8
|
export declare const useRecentAlarms: (count?: number) => OvokRequestState<SlimRecentAlarms>;
|
|
9
9
|
export declare const useRecentOutOfBed: (count?: number) => OvokRequestState<SlimRecentOutOfBed>;
|
|
10
|
+
/** Without status, loads new and reviewed rows together; active is new, completed reviewed, and revoked hand-closed. */
|
|
10
11
|
export declare const useSlimOutOfBedNotifications: (params?: Omit<SlimNotificationsQuery, "type">) => OvokRequestState<SlimNotificationsResponse>;
|
|
11
12
|
/** Fetches every cursor page; a short page is not treated as the end of the feed. */
|
|
12
13
|
export declare const useSlimMedicalNotifications: (params?: Omit<SlimMedicalNotificationsQuery, "cursor">, options?: {
|
|
@@ -73,6 +73,7 @@ const useRecentAlarms = (count) => useSlimNotificationFeed((client, signal) => c
|
|
|
73
73
|
exports.useRecentAlarms = useRecentAlarms;
|
|
74
74
|
const useRecentOutOfBed = (count) => useSlimNotificationFeed((client, signal) => client.getRecentOutOfBed(count, { signal }), count, "recent");
|
|
75
75
|
exports.useRecentOutOfBed = useRecentOutOfBed;
|
|
76
|
+
/** Without status, loads new and reviewed rows together; active is new, completed reviewed, and revoked hand-closed. */
|
|
76
77
|
const useSlimOutOfBedNotifications = (params = {}) => useSlimNotificationFeed((client, signal) => client.listSlimOutOfBedNotifications(params, { signal }), params, "resources");
|
|
77
78
|
exports.useSlimOutOfBedNotifications = useSlimOutOfBedNotifications;
|
|
78
79
|
/** Fetches every cursor page; a short page is not treated as the end of the feed. */
|