@ovok/core 0.4.36 → 0.4.37
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/session-fetch.d.ts +35 -0
- package/dist/client/auth/session-fetch.js +207 -0
- package/dist/client/auth/session-http.d.ts +11 -0
- package/dist/client/auth/session-http.js +180 -0
- package/dist/client/experience/methods.js +27 -37
- package/dist/client/notifications/methods.js +4 -5
- package/dist/client/observation/services/creator.js +4 -18
- package/dist/client/observation/services/single-value-services.d.ts +31 -0
- package/dist/client/observation/services/single-value-services.js +95 -0
- package/dist/client/ovok-client.d.ts +2 -13
- package/dist/client/ovok-client.js +8 -356
- package/dist/client/patient/methods.js +2 -11
- package/dist/client/patient-sharing/methods.js +1 -2
- package/dist/client/platform/methods.js +14 -24
- package/dist/client/query-string.d.ts +3 -0
- package/dist/client/query-string.js +15 -0
- package/dist/client/questionnaire-response/methods/extractQuestionnaireObservations.js +1 -2
- package/dist/client/questionnaire-response/methods/populateQuestionnaire.js +1 -2
- package/dist/client/signals/methods.js +16 -17
- package/dist/client/slim/methods.js +58 -68
- package/dist/conformance/capability-requirements.js +1 -1
- package/dist/hooks/account-hooks.js +6 -68
- package/dist/hooks/api-hooks.d.ts +5 -0
- package/dist/hooks/api-hooks.js +22 -67
- package/dist/hooks/email-hooks.js +1 -40
- package/dist/hooks/experience-hooks.js +4 -60
- package/dist/hooks/notification-hooks.js +5 -55
- package/dist/hooks/notification-invalidation.d.ts +6 -0
- package/dist/hooks/notification-invalidation.js +52 -0
- package/dist/hooks/patient-hooks.js +13 -113
- package/dist/hooks/patient-sharing-hooks.js +1 -5
- package/dist/hooks/platform-hooks.js +3 -51
- package/dist/hooks/signals-hooks.js +2 -45
- package/package.json +1 -1
- 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/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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { MeasurementTypeKey } from "../../../types";
|
|
2
|
+
import { RecordResult, RecordType } from "../types/generate-measurement/HealthConnectTypes";
|
|
3
|
+
import { Sample } from "../types/generate-measurement/Sample";
|
|
4
|
+
import { Measurement } from "../types/measurement/Measurement";
|
|
5
|
+
import { ObservationMetadata } from "../types/service/ObservationMetadata";
|
|
6
|
+
/** A measurement whose one value sits under the spec's `valueKey`. */
|
|
7
|
+
type SingleValueMeasurement = Measurement & {
|
|
8
|
+
[value: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
import { BaseObservationService } from "./base-observation-service";
|
|
11
|
+
type SingleValueSpec = {
|
|
12
|
+
measurementTypeKey: MeasurementTypeKey;
|
|
13
|
+
measurementTypeName: string;
|
|
14
|
+
metadata: Omit<ObservationMetadata<SingleValueMeasurement>, "additionalCodes">;
|
|
15
|
+
/** HealthKit quantity type to read; the first sample is used when omitted. */
|
|
16
|
+
sampleType?: string;
|
|
17
|
+
fromRecord: (record: RecordResult<RecordType>) => number | undefined;
|
|
18
|
+
};
|
|
19
|
+
/** An observation with one value: what varies per measurement is data, not code. */
|
|
20
|
+
declare class SingleValueObservationService extends BaseObservationService<SingleValueMeasurement> {
|
|
21
|
+
private readonly spec;
|
|
22
|
+
protected measurementTypeKey: MeasurementTypeKey;
|
|
23
|
+
protected measurementTypeName: string;
|
|
24
|
+
protected observationMetadata: ObservationMetadata<SingleValueMeasurement>[];
|
|
25
|
+
constructor(spec: SingleValueSpec);
|
|
26
|
+
convertSamplesToMeasurement(samples: Sample[]): SingleValueMeasurement;
|
|
27
|
+
convertRecordToMeasurement(record: RecordResult<RecordType>): SingleValueMeasurement;
|
|
28
|
+
private measurement;
|
|
29
|
+
}
|
|
30
|
+
export declare const singleValueObservationServices: () => [MeasurementTypeKey, SingleValueObservationService][];
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.singleValueObservationServices = void 0;
|
|
4
|
+
const types_1 = require("../../../types");
|
|
5
|
+
const base_observation_service_1 = require("./base-observation-service");
|
|
6
|
+
/** An observation with one value: what varies per measurement is data, not code. */
|
|
7
|
+
class SingleValueObservationService extends base_observation_service_1.BaseObservationService {
|
|
8
|
+
constructor(spec) {
|
|
9
|
+
super();
|
|
10
|
+
this.spec = spec;
|
|
11
|
+
this.measurementTypeKey = spec.measurementTypeKey;
|
|
12
|
+
this.measurementTypeName = spec.measurementTypeName;
|
|
13
|
+
this.observationMetadata = [{ ...spec.metadata, additionalCodes: [] }];
|
|
14
|
+
}
|
|
15
|
+
convertSamplesToMeasurement(samples) {
|
|
16
|
+
const sample = this.spec.sampleType
|
|
17
|
+
? samples.find((x) => x.quantityType === this.spec.sampleType)
|
|
18
|
+
: samples[0];
|
|
19
|
+
return this.measurement(sample === null || sample === void 0 ? void 0 : sample.quantity);
|
|
20
|
+
}
|
|
21
|
+
convertRecordToMeasurement(record) {
|
|
22
|
+
return this.measurement(this.spec.fromRecord(record));
|
|
23
|
+
}
|
|
24
|
+
measurement(value) {
|
|
25
|
+
return {
|
|
26
|
+
[this.spec.metadata.valueKey]: value,
|
|
27
|
+
measurementTypeKey: this.measurementTypeKey,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const specs = [
|
|
32
|
+
{
|
|
33
|
+
measurementTypeKey: types_1.MeasurementTypeKey.bloodGlucose,
|
|
34
|
+
measurementTypeName: "blood glucose",
|
|
35
|
+
metadata: { primaryCode: types_1.ObservationCode.BLOOD_GLUCOSE, display: "Blood Glucose", unit: "mg/dL", valueKey: "bloodGlucose", toFixed: 1 },
|
|
36
|
+
fromRecord: (record) => record.level.inMilligramsPerDeciliter,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
measurementTypeKey: types_1.MeasurementTypeKey.temperature,
|
|
40
|
+
measurementTypeName: "body temperature",
|
|
41
|
+
metadata: { primaryCode: types_1.ObservationCode.BODY_TEMPERATURE, display: "Body Temperature", unit: "°C", valueKey: "bodyTemp", toFixed: 2 },
|
|
42
|
+
fromRecord: (record) => record.temperature.inCelsius,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
measurementTypeKey: types_1.MeasurementTypeKey.bodyWeight,
|
|
46
|
+
measurementTypeName: "body weight",
|
|
47
|
+
metadata: { primaryCode: types_1.ObservationCode.BODY_WEIGHT, display: "Body Weight", unit: "kg", valueKey: "bodyWeight", toFixed: 2 },
|
|
48
|
+
fromRecord: (record) => record.weight.inKilograms,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
measurementTypeKey: types_1.MeasurementTypeKey.heartRateVariability,
|
|
52
|
+
measurementTypeName: "heart rate variability",
|
|
53
|
+
metadata: { primaryCode: types_1.ObservationCode.HEART_RATE_VARIABILITY, display: "Heart Rate Variability", unit: "ms", valueKey: "heartRateVariability", toFixed: 1 },
|
|
54
|
+
sampleType: "HKQuantityTypeIdentifierHeartRateVariabilitySDNN",
|
|
55
|
+
fromRecord: (record) => record.heartRateVariabilityMillis,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
measurementTypeKey: types_1.MeasurementTypeKey.heartRate,
|
|
59
|
+
measurementTypeName: "heart rate",
|
|
60
|
+
metadata: { primaryCode: types_1.ObservationCode.HEART_RATE, display: "Heart Rate", unit: "bpm", valueKey: "heartRate", toFixed: 0 },
|
|
61
|
+
sampleType: "HKQuantityTypeIdentifierHeartRate",
|
|
62
|
+
fromRecord: (record) => { var _a, _b; return (_b = (_a = record.samples) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.beatsPerMinute; },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
measurementTypeKey: types_1.MeasurementTypeKey.respiratoryRate,
|
|
66
|
+
measurementTypeName: "respiratory rate",
|
|
67
|
+
metadata: { primaryCode: types_1.ObservationCode.RESPIRATORY_RATE, display: "Respiratory Rate", unit: "breaths/min", valueKey: "respiratoryRate", toFixed: 1 },
|
|
68
|
+
sampleType: "HKQuantityTypeIdentifierRespiratoryRate",
|
|
69
|
+
fromRecord: (record) => record.rate,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
measurementTypeKey: types_1.MeasurementTypeKey.restingHeartRate,
|
|
73
|
+
measurementTypeName: "resting heart rate",
|
|
74
|
+
metadata: { primaryCode: types_1.ObservationCode.RESTING_HEART_RATE, display: "Resting Heart Rate", unit: "bpm", valueKey: "restingHeartRate", toFixed: 0 },
|
|
75
|
+
sampleType: "HKQuantityTypeIdentifierRestingHeartRate",
|
|
76
|
+
fromRecord: (record) => record.beatsPerMinute,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
measurementTypeKey: types_1.MeasurementTypeKey.vo2Max,
|
|
80
|
+
measurementTypeName: "VO2 max",
|
|
81
|
+
metadata: { primaryCode: types_1.ObservationCode.VO2_MAX, display: "VO2 Max", unit: "mL/kg/min", valueKey: "vo2Max", toFixed: 1 },
|
|
82
|
+
sampleType: "HKQuantityTypeIdentifierVO2Max",
|
|
83
|
+
fromRecord: (record) => record.vo2MillilitersPerMinuteKilogram,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
measurementTypeKey: types_1.MeasurementTypeKey.walkingHeartRateAverage,
|
|
87
|
+
measurementTypeName: "walking heart rate average",
|
|
88
|
+
metadata: { primaryCode: types_1.ObservationCode.WALKING_HEART_RATE_AVERAGE, display: "Walking Heart Rate Average", unit: "bpm", valueKey: "walkingHeartRateAverage", toFixed: 0 },
|
|
89
|
+
sampleType: "HKQuantityTypeIdentifierWalkingHeartRateAverage",
|
|
90
|
+
// Health Connect has no walking-heart-rate-average record.
|
|
91
|
+
fromRecord: () => 0,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
const singleValueObservationServices = () => specs.map((spec) => [spec.measurementTypeKey, new SingleValueObservationService(spec)]);
|
|
95
|
+
exports.singleValueObservationServices = singleValueObservationServices;
|
|
@@ -3,6 +3,7 @@ import { Bundle, CapabilityStatement, Observation } from "@medplum/fhirtypes";
|
|
|
3
3
|
import { OvokPublicApiEndpoint } from "../conformance/capability";
|
|
4
4
|
import * as aiFhirMethods from "./ai-fhir/methods";
|
|
5
5
|
import * as authMethods from "./auth/methods";
|
|
6
|
+
import { OvokUnauthenticatedReason } from "./auth/session-fetch";
|
|
6
7
|
import { OvokSavedAccount } from "./auth/types/Account";
|
|
7
8
|
import * as botMethods from "./bot/methods";
|
|
8
9
|
import * as changeHistoryMethods from "./change-history/methods";
|
|
@@ -24,6 +25,7 @@ import * as signalsMethods from "./signals/methods";
|
|
|
24
25
|
import * as slimMethods from "./slim/methods";
|
|
25
26
|
export declare const MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
26
27
|
export { MAX_OBSERVATION_BUNDLE_BYTES } from "./observation/observation-bundle-utils";
|
|
28
|
+
export type { OvokUnauthenticatedReason } from "./auth/session-fetch";
|
|
27
29
|
export type SaveObservationsResult = {
|
|
28
30
|
status: "saved";
|
|
29
31
|
response: Bundle;
|
|
@@ -32,19 +34,6 @@ export type SaveObservationsResult = {
|
|
|
32
34
|
queueId: string;
|
|
33
35
|
};
|
|
34
36
|
export type OvokFhirVersion = "R4" | "R5";
|
|
35
|
-
export type OvokUnauthenticatedReason = {
|
|
36
|
-
type: "refresh-rejected";
|
|
37
|
-
status: 400 | 401 | 403;
|
|
38
|
-
} | {
|
|
39
|
-
type: "refresh-unavailable";
|
|
40
|
-
status?: number;
|
|
41
|
-
} | {
|
|
42
|
-
type: "authentication-rejected";
|
|
43
|
-
status: number;
|
|
44
|
-
} | {
|
|
45
|
-
type: "request-unauthorized";
|
|
46
|
-
status: 401;
|
|
47
|
-
};
|
|
48
37
|
export type OvokClientOptions = Omit<MedplumClientOptions, "onUnauthenticated"> & {
|
|
49
38
|
onUnauthenticated?: (reason: OvokUnauthenticatedReason) => void;
|
|
50
39
|
socialLoginClientId?: string;
|
|
@@ -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"));
|
|
@@ -66,138 +67,6 @@ const slimMethods = __importStar(require("./slim/methods"));
|
|
|
66
67
|
exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
67
68
|
var observation_bundle_utils_2 = require("./observation/observation-bundle-utils");
|
|
68
69
|
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
70
|
const isRetrySafePost = (pathname) => pathname === "/ai/fhir/search";
|
|
202
71
|
class OvokClient extends core_1.MedplumClient {
|
|
203
72
|
/**
|
|
@@ -214,236 +83,19 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
214
83
|
const baseUrl = (_c = config.baseUrl) !== null && _c !== void 0 ? _c : "https://api.medplum.com/";
|
|
215
84
|
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
216
85
|
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
|
-
};
|
|
86
|
+
const session = (0, session_fetch_1.createSessionFetch)({ config, storage, fetchImplementation, normalizedBaseUrl, tokenUrl });
|
|
424
87
|
super({
|
|
425
88
|
...config,
|
|
426
89
|
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,
|
|
90
|
+
onUnauthenticated: session.onUnauthenticated,
|
|
91
|
+
fetch: session.fetch,
|
|
440
92
|
});
|
|
441
93
|
this.socialLoginClientId = "";
|
|
442
|
-
|
|
94
|
+
session.setClearActiveLogin(this.clearActiveLogin.bind(this));
|
|
443
95
|
this.hasExplicitStorageAdapter = config.storage !== undefined;
|
|
444
96
|
this.clientOptions = config;
|
|
445
97
|
this.keepSessionOnRefreshUnavailable = config.keepSessionOnRefreshUnavailable === true;
|
|
446
|
-
this.refreshState = refreshState;
|
|
98
|
+
this.refreshState = session.refreshState;
|
|
447
99
|
this.socialLoginClientId = (_e = config.socialLoginClientId) !== null && _e !== void 0 ? _e : "";
|
|
448
100
|
this.addEventListener("profileRefreshed", () => this.syncSavedAccountProfile());
|
|
449
101
|
this.bindMethods(authMethods);
|
|
@@ -546,7 +198,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
546
198
|
}
|
|
547
199
|
catch (error) {
|
|
548
200
|
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)) {
|
|
201
|
+
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
202
|
return;
|
|
551
203
|
}
|
|
552
204
|
if (!activeLogin &&
|
|
@@ -21,22 +21,13 @@ exports.getPatientDevice = getPatientDevice;
|
|
|
21
21
|
exports.listPatientNotifications = listPatientNotifications;
|
|
22
22
|
exports.getPatientNotification = getPatientNotification;
|
|
23
23
|
exports.getPatientDashboard = getPatientDashboard;
|
|
24
|
-
const
|
|
25
|
-
const values = new URLSearchParams();
|
|
26
|
-
for (const [key, value] of Object.entries(params)) {
|
|
27
|
-
if (value !== undefined) {
|
|
28
|
-
values.set(key, String(value));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const encoded = values.toString();
|
|
32
|
-
return encoded === "" ? "" : `?${encoded}`;
|
|
33
|
-
};
|
|
24
|
+
const query_string_1 = require("../query-string");
|
|
34
25
|
async function getCurrentPatientProfile(requestOptions) {
|
|
35
26
|
return this.get("/auth/me", ...(requestOptions === undefined ? [] : [requestOptions]));
|
|
36
27
|
}
|
|
37
28
|
async function getPatientLatestObservations(params = {}, requestOptions) {
|
|
38
29
|
const code = Array.isArray(params.code) ? params.code.join(",") : params.code;
|
|
39
|
-
return this.get(`/fhir/Observation/$lastn${
|
|
30
|
+
return this.get(`/fhir/Observation/$lastn${(0, query_string_1.toQueryString)({
|
|
40
31
|
patient: params.patient,
|
|
41
32
|
max: params.max,
|
|
42
33
|
code,
|
|
@@ -6,7 +6,6 @@ exports.listPatientSharing = listPatientSharing;
|
|
|
6
6
|
exports.revokePatientSharing = revokePatientSharing;
|
|
7
7
|
exports.getPatientSharingSettings = getPatientSharingSettings;
|
|
8
8
|
exports.updatePatientSharingSettings = updatePatientSharingSettings;
|
|
9
|
-
const segment = (value) => encodeURIComponent(value);
|
|
10
9
|
function createPatientSharingInvite(body, requestOptions) {
|
|
11
10
|
return this.post("/v1/patient-sharing/invites", body, undefined, requestOptions);
|
|
12
11
|
}
|
|
@@ -26,7 +25,7 @@ function listPatientSharing(requestOptions) {
|
|
|
26
25
|
});
|
|
27
26
|
}
|
|
28
27
|
async function revokePatientSharing(consentId, requestOptions) {
|
|
29
|
-
await this.delete(`/v1/patient-sharing/shares/${
|
|
28
|
+
await this.delete(`/v1/patient-sharing/shares/${encodeURIComponent(consentId)}`, ...(requestOptions === undefined ? [] : [requestOptions]));
|
|
30
29
|
this.invalidateUrl("/v1/patient-sharing/shares");
|
|
31
30
|
}
|
|
32
31
|
function getPatientSharingSettings(requestOptions) {
|