@ovok/core 0.4.40 → 0.4.41
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/observation/methods/saveMeasurement.d.ts +2 -2
- package/dist/client/observation/methods/saveMeasurement.js +1 -1
- package/dist/client/observation/observation-bundle-utils.d.ts +5 -0
- package/dist/client/observation/observation-bundle-utils.js +34 -7
- package/dist/client/observation/series.d.ts +33 -0
- package/dist/client/observation/series.js +83 -0
- package/dist/client/observation/types/measurement/MeasurementSeries.d.ts +14 -0
- package/dist/client/observation/types/measurement/MeasurementSeries.js +2 -0
- package/dist/client/observation/types/measurement/index.d.ts +1 -0
- package/dist/client/observation/types/measurement/index.js +1 -0
- package/dist/client/offline/offline-measurement-queue.d.ts +12 -2
- package/dist/client/offline/offline-measurement-queue.js +44 -17
- package/dist/client/ovok-client.d.ts +32 -6
- package/dist/client/ovok-client.js +59 -19
- package/dist/conformance/capability-requirements.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OvokClient, SaveObservationsResult } from "../../ovok-client";
|
|
1
|
+
import type { OvokClient, SaveObservationsOptions, SaveObservationsResult } from "../../ovok-client";
|
|
2
2
|
import { GenerateObservationBodyParams } from "../types/generate-observation-body/GenerateObservationBodyParams";
|
|
3
3
|
/** Framework-free save helper for apps that do not use React hooks. */
|
|
4
|
-
export declare const saveMeasurement: (client: OvokClient, params: GenerateObservationBodyParams) => Promise<SaveObservationsResult>;
|
|
4
|
+
export declare const saveMeasurement: (client: OvokClient, params: GenerateObservationBodyParams, options?: SaveObservationsOptions) => Promise<SaveObservationsResult>;
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.saveMeasurement = void 0;
|
|
4
4
|
/** Framework-free save helper for apps that do not use React hooks. */
|
|
5
|
-
const saveMeasurement = (client, params) => client.saveMeasurement(params);
|
|
5
|
+
const saveMeasurement = (client, params, options) => client.saveMeasurement(params, options);
|
|
6
6
|
exports.saveMeasurement = saveMeasurement;
|
|
@@ -4,3 +4,8 @@ export declare const MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
|
4
4
|
export declare const linkObservationMembers: (observations: Observation[]) => Observation[];
|
|
5
5
|
export declare const splitObservationBundleEntries: (bundle: Bundle) => BundleEntry[][] | undefined;
|
|
6
6
|
export declare const executeObservationBundle: (bundle: Bundle, executeBatch: (bundle: Bundle) => Promise<Bundle>) => Promise<Bundle>;
|
|
7
|
+
/**
|
|
8
|
+
* Readings' entries packed into as few requests as fit `MAX_OBSERVATION_BUNDLE_BYTES`, each reading whole. A reading
|
|
9
|
+
* too big for one request is a request by itself, which `executeObservationBundle` splits.
|
|
10
|
+
*/
|
|
11
|
+
export declare const packObservationGroups: (groups: BundleEntry[][]) => BundleEntry[][][];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.executeObservationBundle = exports.splitObservationBundleEntries = exports.linkObservationMembers = exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = exports.MAX_OBSERVATION_BUNDLE_BYTES = void 0;
|
|
3
|
+
exports.packObservationGroups = exports.executeObservationBundle = exports.splitObservationBundleEntries = exports.linkObservationMembers = exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = exports.MAX_OBSERVATION_BUNDLE_BYTES = void 0;
|
|
4
4
|
const offline_1 = require("../offline");
|
|
5
5
|
exports.MAX_OBSERVATION_BUNDLE_BYTES = 900000;
|
|
6
6
|
exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
@@ -78,7 +78,7 @@ const splitObservationBundleEntries = (bundle) => {
|
|
|
78
78
|
};
|
|
79
79
|
exports.splitObservationBundleEntries = splitObservationBundleEntries;
|
|
80
80
|
const executeObservationBundle = async (bundle, executeBatch) => {
|
|
81
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
81
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
82
82
|
const entries = (_a = bundle.entry) !== null && _a !== void 0 ? _a : [];
|
|
83
83
|
if (bundle.type === "transaction" &&
|
|
84
84
|
entries.length > exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES) {
|
|
@@ -129,13 +129,40 @@ const executeObservationBundle = async (bundle, executeBatch) => {
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
const response = await executeBatch(bundle);
|
|
132
|
-
if ((_m = response.entry) === null || _m === void 0 ? void 0 : _m.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
if (((_o = (_m = response.entry) === null || _m === void 0 ? void 0 : _m.length) !== null && _o !== void 0 ? _o : 0) !== entries.length ||
|
|
133
|
+
((_p = response.entry) === null || _p === void 0 ? void 0 : _p.some((entry) => {
|
|
134
|
+
var _a, _b;
|
|
135
|
+
const status = (_b = (_a = entry.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : "";
|
|
136
|
+
return status.startsWith("4") || status.startsWith("5");
|
|
137
|
+
}))) {
|
|
137
138
|
throw response;
|
|
138
139
|
}
|
|
139
140
|
return response;
|
|
140
141
|
};
|
|
141
142
|
exports.executeObservationBundle = executeObservationBundle;
|
|
143
|
+
/**
|
|
144
|
+
* Readings' entries packed into as few requests as fit `MAX_OBSERVATION_BUNDLE_BYTES`, each reading whole. A reading
|
|
145
|
+
* too big for one request is a request by itself, which `executeObservationBundle` splits.
|
|
146
|
+
*/
|
|
147
|
+
const packObservationGroups = (groups) => {
|
|
148
|
+
const envelope = JSON.stringify({ resourceType: "Bundle", type: "batch", entry: [] }).length;
|
|
149
|
+
const requests = [];
|
|
150
|
+
let request = [];
|
|
151
|
+
let size = envelope;
|
|
152
|
+
for (const group of groups) {
|
|
153
|
+
// Each entry adds its JSON and a separating comma.
|
|
154
|
+
const groupSize = group.reduce((total, entry) => total + JSON.stringify(entry).length + 1, 0);
|
|
155
|
+
if (request.length > 0 && size + groupSize > exports.MAX_OBSERVATION_BUNDLE_BYTES) {
|
|
156
|
+
requests.push(request);
|
|
157
|
+
request = [];
|
|
158
|
+
size = envelope;
|
|
159
|
+
}
|
|
160
|
+
request.push(group);
|
|
161
|
+
size += groupSize;
|
|
162
|
+
}
|
|
163
|
+
if (request.length > 0) {
|
|
164
|
+
requests.push(request);
|
|
165
|
+
}
|
|
166
|
+
return requests;
|
|
167
|
+
};
|
|
168
|
+
exports.packObservationGroups = packObservationGroups;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Observation } from "@medplum/fhirtypes";
|
|
2
|
+
import { MeasurementDevice } from "./types/measurement/MeasurementDevice";
|
|
3
|
+
import { MeasurementSeries } from "./types/measurement/MeasurementSeries";
|
|
4
|
+
export type SeriesObservationParams = {
|
|
5
|
+
series: MeasurementSeries;
|
|
6
|
+
patientId: string;
|
|
7
|
+
device?: MeasurementDevice;
|
|
8
|
+
/** `preliminary` for values a device's own recording may still replace. */
|
|
9
|
+
status?: Observation["status"];
|
|
10
|
+
};
|
|
11
|
+
/** The values of a series observation, one per slot from `start`. */
|
|
12
|
+
export type ReadSeries = {
|
|
13
|
+
start: Date;
|
|
14
|
+
periodMs: number;
|
|
15
|
+
values: Array<number | null>;
|
|
16
|
+
};
|
|
17
|
+
export type SeriesSummary = {
|
|
18
|
+
min: number;
|
|
19
|
+
max: number;
|
|
20
|
+
mean: number;
|
|
21
|
+
count: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A series as observations: one per measured property, in `MEASUREMENT_FIELDS` order with the main one first, each
|
|
25
|
+
* holding its values as `valueSampledData` across the series' `effectivePeriod`. They carry the same codes as a
|
|
26
|
+
* single reading of that kind. Save them with `saveObservations`, which links the rest to the first. ECG recordings
|
|
27
|
+
* keep their own path, `generateObservationBodiesByMeasurement`.
|
|
28
|
+
*/
|
|
29
|
+
export declare const seriesObservations: ({ series, patientId, device, status, }: SeriesObservationParams) => Observation[];
|
|
30
|
+
/** The values of a series observation (`valueSampledData` with one dimension), or `undefined` for any other. */
|
|
31
|
+
export declare const readSeries: (observation: Observation) => ReadSeries | undefined;
|
|
32
|
+
/** The lowest, highest and mean of a series' values, or `undefined` when it holds none. */
|
|
33
|
+
export declare const summarizeSeries: (values: ReadonlyArray<number | null>) => SeriesSummary | undefined;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summarizeSeries = exports.readSeries = exports.seriesObservations = void 0;
|
|
4
|
+
const creator_1 = require("./services/creator");
|
|
5
|
+
const getDeviceObject_1 = require("./utils/getDeviceObject");
|
|
6
|
+
/** FHIR's mark for a slot with no value. */
|
|
7
|
+
const NO_VALUE = "E";
|
|
8
|
+
const sampledData = (values, decimals) => values
|
|
9
|
+
.map((value) => (value === null || !Number.isFinite(value) ? NO_VALUE : String(Number(value.toFixed(decimals)))))
|
|
10
|
+
.join(" ");
|
|
11
|
+
/**
|
|
12
|
+
* A series as observations: one per measured property, in `MEASUREMENT_FIELDS` order with the main one first, each
|
|
13
|
+
* holding its values as `valueSampledData` across the series' `effectivePeriod`. They carry the same codes as a
|
|
14
|
+
* single reading of that kind. Save them with `saveObservations`, which links the rest to the first. ECG recordings
|
|
15
|
+
* keep their own path, `generateObservationBodiesByMeasurement`.
|
|
16
|
+
*/
|
|
17
|
+
const seriesObservations = ({ series, patientId, device, status = "registered", }) => {
|
|
18
|
+
const service = creator_1.ObservationServiceCreator.getInstance().getService(series.measurementTypeKey);
|
|
19
|
+
const codes = service.getObservationCodes();
|
|
20
|
+
const slots = Math.max(0, ...Object.values(series.values).map((values) => { var _a; return (_a = values === null || values === void 0 ? void 0 : values.length) !== null && _a !== void 0 ? _a : 0; }));
|
|
21
|
+
const effectivePeriod = {
|
|
22
|
+
start: series.start.toISOString(),
|
|
23
|
+
end: new Date(series.start.getTime() + slots * series.periodMs).toISOString(),
|
|
24
|
+
};
|
|
25
|
+
return service.getFields().flatMap((field, index) => {
|
|
26
|
+
const values = series.values[field.key];
|
|
27
|
+
const code = codes[index];
|
|
28
|
+
if (!code || !(values === null || values === void 0 ? void 0 : values.some((value) => value !== null))) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return [{
|
|
32
|
+
resourceType: "Observation",
|
|
33
|
+
status,
|
|
34
|
+
subject: { reference: `Patient/${patientId}` },
|
|
35
|
+
device: (0, getDeviceObject_1.getDeviceObject)(device),
|
|
36
|
+
code,
|
|
37
|
+
effectivePeriod,
|
|
38
|
+
valueSampledData: {
|
|
39
|
+
origin: { value: 0, unit: field.unit },
|
|
40
|
+
period: series.periodMs,
|
|
41
|
+
dimensions: 1,
|
|
42
|
+
data: sampledData(values, field.decimals),
|
|
43
|
+
},
|
|
44
|
+
}];
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
exports.seriesObservations = seriesObservations;
|
|
48
|
+
/** The values of a series observation (`valueSampledData` with one dimension), or `undefined` for any other. */
|
|
49
|
+
const readSeries = (observation) => {
|
|
50
|
+
var _a, _b, _c, _d, _e, _f;
|
|
51
|
+
const sampled = observation.valueSampledData;
|
|
52
|
+
const start = (_b = (_a = observation.effectivePeriod) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : observation.effectiveDateTime;
|
|
53
|
+
if (!(sampled === null || sampled === void 0 ? void 0 : sampled.data) || !sampled.period || !start || ((_c = sampled.dimensions) !== null && _c !== void 0 ? _c : 1) !== 1) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const origin = (_e = (_d = sampled.origin) === null || _d === void 0 ? void 0 : _d.value) !== null && _e !== void 0 ? _e : 0;
|
|
57
|
+
const factor = (_f = sampled.factor) !== null && _f !== void 0 ? _f : 1;
|
|
58
|
+
return {
|
|
59
|
+
start: new Date(start),
|
|
60
|
+
periodMs: sampled.period,
|
|
61
|
+
values: sampled.data.trim().split(/\s+/).map((token) => {
|
|
62
|
+
const value = Number(token);
|
|
63
|
+
// E, L and U mark a slot without a value.
|
|
64
|
+
return Number.isFinite(value) ? origin + factor * value : null;
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
exports.readSeries = readSeries;
|
|
69
|
+
/** The lowest, highest and mean of a series' values, or `undefined` when it holds none. */
|
|
70
|
+
const summarizeSeries = (values) => {
|
|
71
|
+
let min = Infinity, max = -Infinity, total = 0, count = 0;
|
|
72
|
+
// A loop, not Math.min(...values): an ECG holds more values than a call takes arguments.
|
|
73
|
+
for (const value of values) {
|
|
74
|
+
if (value !== null) {
|
|
75
|
+
min = Math.min(min, value);
|
|
76
|
+
max = Math.max(max, value);
|
|
77
|
+
total += value;
|
|
78
|
+
count++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return count === 0 ? undefined : { min, max, mean: total / count, count };
|
|
82
|
+
};
|
|
83
|
+
exports.summarizeSeries = summarizeSeries;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MeasurementTypeKey } from "../MeasurementTypeKey";
|
|
2
|
+
/**
|
|
3
|
+
* One kind of measurement taken at a fixed interval, such as the heart rate a strap sends every second: one value
|
|
4
|
+
* per slot for each measured property, and `null` for a slot nothing arrived in.
|
|
5
|
+
*/
|
|
6
|
+
export type MeasurementSeries = {
|
|
7
|
+
measurementTypeKey: MeasurementTypeKey;
|
|
8
|
+
/** When the first slot starts. */
|
|
9
|
+
start: Date;
|
|
10
|
+
/** Milliseconds from one slot to the next. */
|
|
11
|
+
periodMs: number;
|
|
12
|
+
/** By measured property (the keys `MEASUREMENT_FIELDS` lists), one value per slot. */
|
|
13
|
+
values: Partial<Record<string, ReadonlyArray<number | null>>>;
|
|
14
|
+
};
|
|
@@ -34,3 +34,4 @@ __exportStar(require("./WalkingHeartRateAverageMeasurement"), exports);
|
|
|
34
34
|
__exportStar(require("./LipidPanelMeasurement"), exports);
|
|
35
35
|
__exportStar(require("./RespiratoryTherapyMeasurement"), exports);
|
|
36
36
|
__exportStar(require("./BabySleepMeasurement"), exports);
|
|
37
|
+
__exportStar(require("./MeasurementSeries"), exports);
|
|
@@ -31,7 +31,16 @@ export type OfflineMeasurementFailure = {
|
|
|
31
31
|
};
|
|
32
32
|
type MeasurementTransport = (bundle: Bundle, requestOptions?: MedplumRequestOptions) => Promise<Bundle>;
|
|
33
33
|
declare const stableMeasurementId: (observation: Observation) => string;
|
|
34
|
-
|
|
34
|
+
/** How a save names and writes each observation; `SaveObservationsOptions` describes both. */
|
|
35
|
+
type ObservationEntryOptions = {
|
|
36
|
+
identifierSystems?: readonly string[];
|
|
37
|
+
replace?: boolean;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The bundle entry that saves `observation` once: created only if no observation of its subject has its content id
|
|
41
|
+
* or one of its reading identifiers, or with `replace`, written over the one saved under its reading identifiers.
|
|
42
|
+
*/
|
|
43
|
+
declare const entryForObservation: (observation: Observation, { identifierSystems, replace }?: ObservationEntryOptions) => BundleEntry<Observation>;
|
|
35
44
|
export declare class OfflineMeasurementQueue {
|
|
36
45
|
private readonly storage;
|
|
37
46
|
private readonly transport;
|
|
@@ -42,7 +51,7 @@ export declare class OfflineMeasurementQueue {
|
|
|
42
51
|
private readonly getOwnerKey?;
|
|
43
52
|
private operation;
|
|
44
53
|
constructor(storage: IClientStorage, transport: MeasurementTransport, options?: OfflineMeasurementQueueOptions);
|
|
45
|
-
enqueueAndFlush: (observations: Observation[], requestOptions?: MedplumRequestOptions) => Promise<{
|
|
54
|
+
enqueueAndFlush: (observations: Observation[], requestOptions?: MedplumRequestOptions, entryOptions?: ObservationEntryOptions) => Promise<{
|
|
46
55
|
id: string;
|
|
47
56
|
flush: OfflineMeasurementFlushResult;
|
|
48
57
|
}>;
|
|
@@ -52,4 +61,5 @@ export declare class OfflineMeasurementQueue {
|
|
|
52
61
|
private writeQueue;
|
|
53
62
|
private flushQueue;
|
|
54
63
|
}
|
|
64
|
+
export type { ObservationEntryOptions };
|
|
55
65
|
export { OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY as DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY, IDENTIFIER_SYSTEM as OFFLINE_MEASUREMENT_IDENTIFIER_SYSTEM, entryForObservation, stableMeasurementId, };
|
|
@@ -62,28 +62,55 @@ const stableMeasurementId = (observation) => {
|
|
|
62
62
|
return stableValueId(legacyObservation);
|
|
63
63
|
};
|
|
64
64
|
exports.stableMeasurementId = stableMeasurementId;
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
/** The observation's identifiers in `systems`, as `system|value` search tokens. */
|
|
66
|
+
const readingIdentifiers = (observation, systems) => {
|
|
67
|
+
var _a;
|
|
68
|
+
return ((_a = observation.identifier) !== null && _a !== void 0 ? _a : [])
|
|
69
|
+
.filter(({ system, value }) => system && value && systems.includes(system))
|
|
70
|
+
.map(({ system, value }) => `${system}|${value}`);
|
|
71
|
+
};
|
|
72
|
+
/** A search for an observation of the same subject carrying any of `identifiers`. */
|
|
73
|
+
const searchFor = (observation, identifiers) => {
|
|
74
|
+
var _a;
|
|
75
|
+
const subject = (_a = observation.subject) === null || _a === void 0 ? void 0 : _a.reference;
|
|
76
|
+
return [
|
|
77
|
+
`identifier=${identifiers.map(encodeURIComponent).join(",")}`,
|
|
78
|
+
...(subject ? [`subject=${encodeURIComponent(subject)}`] : []),
|
|
79
|
+
].join("&");
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The bundle entry that saves `observation` once: created only if no observation of its subject has its content id
|
|
83
|
+
* or one of its reading identifiers, or with `replace`, written over the one saved under its reading identifiers.
|
|
84
|
+
*/
|
|
85
|
+
const entryForObservation = (observation, { identifierSystems = [], replace = false } = {}) => {
|
|
86
|
+
var _a;
|
|
67
87
|
const id = stableMeasurementId(observation);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
const fullUrl = `urn:uuid:${id}`;
|
|
89
|
+
const resource = {
|
|
90
|
+
...observation,
|
|
91
|
+
identifier: [...((_a = observation.identifier) !== null && _a !== void 0 ? _a : []), { system: IDENTIFIER_SYSTEM, value: id }],
|
|
71
92
|
};
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
const readings = readingIdentifiers(observation, [
|
|
94
|
+
fhir_systems_1.DEVICE_READING_IDENTIFIER_SYSTEM,
|
|
95
|
+
...identifierSystems,
|
|
96
|
+
]);
|
|
97
|
+
if (replace && readings.length > 0) {
|
|
98
|
+
// Medplum refuses a create-by-update that names an id.
|
|
99
|
+
delete resource.id;
|
|
100
|
+
return {
|
|
101
|
+
fullUrl,
|
|
102
|
+
request: { method: "PUT", url: `Observation?${searchFor(observation, readings)}` },
|
|
103
|
+
resource,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
76
106
|
return {
|
|
77
|
-
fullUrl
|
|
107
|
+
fullUrl,
|
|
78
108
|
request: {
|
|
79
109
|
method: "POST",
|
|
80
110
|
url: "Observation",
|
|
81
|
-
ifNoneExist,
|
|
82
|
-
},
|
|
83
|
-
resource: {
|
|
84
|
-
...observation,
|
|
85
|
-
identifier: [...((_b = observation.identifier) !== null && _b !== void 0 ? _b : []), identifier],
|
|
111
|
+
ifNoneExist: searchFor(observation, [`${IDENTIFIER_SYSTEM}|${id}`, ...readings]),
|
|
86
112
|
},
|
|
113
|
+
resource,
|
|
87
114
|
};
|
|
88
115
|
};
|
|
89
116
|
exports.entryForObservation = entryForObservation;
|
|
@@ -119,14 +146,14 @@ class OfflineMeasurementQueue {
|
|
|
119
146
|
this.storage = storage;
|
|
120
147
|
this.transport = transport;
|
|
121
148
|
this.operation = Promise.resolve();
|
|
122
|
-
this.enqueueAndFlush = (observations, requestOptions) => {
|
|
149
|
+
this.enqueueAndFlush = (observations, requestOptions, entryOptions) => {
|
|
123
150
|
var _a;
|
|
124
151
|
const ownerKey = (_a = this.getOwnerKey) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
125
152
|
return this.runExclusive(async () => {
|
|
126
153
|
var _a, _b;
|
|
127
154
|
await ((_b = (_a = this.storage).getInitPromise) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
128
155
|
const queue = this.readQueue();
|
|
129
|
-
const entries = observations.map(entryForObservation);
|
|
156
|
+
const entries = observations.map((observation) => entryForObservation(observation, entryOptions));
|
|
130
157
|
if (this.getOwnerKey && !ownerKey) {
|
|
131
158
|
throw new Error("Offline measurements require an active account to prevent cross-account delivery.");
|
|
132
159
|
}
|
|
@@ -32,6 +32,20 @@ export type SaveObservationsResult = {
|
|
|
32
32
|
status: "queued";
|
|
33
33
|
queueId: string;
|
|
34
34
|
};
|
|
35
|
+
/** Medplum's request options, and how a save names and writes its observations. */
|
|
36
|
+
export type SaveObservationsOptions = MedplumRequestOptions & {
|
|
37
|
+
/**
|
|
38
|
+
* Identifier systems that name a reading for good, such as an app's own reading key. An observation carrying an
|
|
39
|
+
* identifier in one of them is created only if no observation of its subject has that identifier, so a reading
|
|
40
|
+
* delivered again, or saved before under that key, is saved once. The SDK's device-reading id always counts.
|
|
41
|
+
*/
|
|
42
|
+
identifierSystems?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Write over the observation saved under the same reading identifiers instead of keeping it, for a value that
|
|
45
|
+
* changes, such as a day's step total. Needs a reading identifier; an observation without one is created once.
|
|
46
|
+
*/
|
|
47
|
+
replace?: boolean;
|
|
48
|
+
};
|
|
35
49
|
export type OvokFhirVersion = "R4" | "R5";
|
|
36
50
|
export type OvokClientOptions = Omit<MedplumClientOptions, "onUnauthenticated"> & {
|
|
37
51
|
onUnauthenticated?: (reason: OvokUnauthenticatedReason) => void;
|
|
@@ -81,18 +95,27 @@ export declare class OvokClient extends MedplumClient {
|
|
|
81
95
|
/** Reauthentication clears the expired marker for the account that signed in. */
|
|
82
96
|
setActiveLogin(login: LoginState): Promise<void>;
|
|
83
97
|
/** Saves a measurement immediately, or durably queues it when the opt-in queue is enabled. */
|
|
84
|
-
saveMeasurement(params: GenerateObservationBodyParams,
|
|
98
|
+
saveMeasurement(params: GenerateObservationBodyParams, options?: SaveObservationsOptions): Promise<SaveObservationsResult>;
|
|
85
99
|
/** Reads Ovok's composed FHIR and public-route capability statement. */
|
|
86
100
|
getCapabilityStatement(version?: OvokFhirVersion, requestOptions?: MedplumRequestOptions): Promise<CapabilityStatement>;
|
|
87
101
|
/** Returns the non-FHIR public routes published by the backend statement. */
|
|
88
102
|
getPublicApiEndpoints(version?: OvokFhirVersion, requestOptions?: MedplumRequestOptions): Promise<OvokPublicApiEndpoint[]>;
|
|
89
|
-
/**
|
|
90
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Saves one reading's observations, main first, with conditional creates so a retry cannot create a duplicate.
|
|
105
|
+
* A reading too big for one request goes over several, and stops if the signed-in account changes in between.
|
|
106
|
+
*/
|
|
107
|
+
saveObservations(observations: Observation[], options?: SaveObservationsOptions): Promise<SaveObservationsResult>;
|
|
108
|
+
/**
|
|
109
|
+
* Saves many readings at once, such as a page of a health import: each group is one reading's observations, main
|
|
110
|
+
* first, linked and named as `saveObservations` does. Readings share batch requests as far as they fit, each whole;
|
|
111
|
+
* one too big for a request goes alone and is split. It is never queued offline: an import reads its source again.
|
|
112
|
+
*/
|
|
113
|
+
saveObservationGroups(groups: Observation[][], options?: SaveObservationsOptions): Promise<Bundle>;
|
|
91
114
|
/** Flushes measurements queued while offline. Call this on connectivity/app-resume events. */
|
|
92
115
|
flushOfflineMeasurementQueue(options?: OfflineMeasurementFlushOptions, requestOptions?: MedplumRequestOptions): Promise<OfflineMeasurementFlushResult>;
|
|
93
116
|
private getOfflineMeasurementQueue;
|
|
94
|
-
/**
|
|
95
|
-
private
|
|
117
|
+
/** The signed-in account, on its server and project: what queued measurements and a running save belong to. */
|
|
118
|
+
private accountKey;
|
|
96
119
|
/**
|
|
97
120
|
* Medplum clears its entire client storage after an unrecoverable 401. Keep
|
|
98
121
|
* saved accounts (marking only the active one expired) and queued measurements.
|
|
@@ -108,7 +131,10 @@ export declare class OvokClient extends MedplumClient {
|
|
|
108
131
|
private removeSavedAccount;
|
|
109
132
|
private syncSavedAccountProfile;
|
|
110
133
|
private restoreActiveLogin;
|
|
111
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Sends a save's bundle, over several requests when it must. A later request isn't sent once the signed-in
|
|
136
|
+
* account differs from `account`: the rest of a reading is never written under someone else's sign-in.
|
|
137
|
+
*/
|
|
112
138
|
private executeObservationBundle;
|
|
113
139
|
private bindMethods;
|
|
114
140
|
/**
|
|
@@ -68,6 +68,14 @@ const slimMethods = __importStar(require("./slim/methods"));
|
|
|
68
68
|
var observation_bundle_utils_2 = require("./observation/observation-bundle-utils");
|
|
69
69
|
Object.defineProperty(exports, "MAX_CONDITIONAL_OBSERVATION_ENTRIES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_CONDITIONAL_OBSERVATION_ENTRIES; } });
|
|
70
70
|
Object.defineProperty(exports, "MAX_OBSERVATION_BUNDLE_BYTES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_OBSERVATION_BUNDLE_BYTES; } });
|
|
71
|
+
/** A save's own options apart from the request options it passes on. */
|
|
72
|
+
const splitSaveOptions = (options) => {
|
|
73
|
+
if (!options) {
|
|
74
|
+
return [{}, undefined];
|
|
75
|
+
}
|
|
76
|
+
const { identifierSystems, replace, ...requestOptions } = options;
|
|
77
|
+
return [{ identifierSystems, replace }, requestOptions];
|
|
78
|
+
};
|
|
71
79
|
const isRetrySafePost = (pathname) => pathname === "/ai/fhir/search";
|
|
72
80
|
/** A failed profile load is tried again after this long, doubling up to the maximum. */
|
|
73
81
|
const PROFILE_RETRY_MS = 2000;
|
|
@@ -312,9 +320,9 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
312
320
|
}
|
|
313
321
|
}
|
|
314
322
|
/** Saves a measurement immediately, or durably queues it when the opt-in queue is enabled. */
|
|
315
|
-
async saveMeasurement(params,
|
|
323
|
+
async saveMeasurement(params, options) {
|
|
316
324
|
const observations = await this.generateObservationBodiesByMeasurement(params);
|
|
317
|
-
return this.saveObservations(observations, ...(0, request_options_1.optionalArg)(
|
|
325
|
+
return this.saveObservations(observations, ...(0, request_options_1.optionalArg)(options));
|
|
318
326
|
}
|
|
319
327
|
/** Reads Ovok's composed FHIR and public-route capability statement. */
|
|
320
328
|
async getCapabilityStatement(version = "R4", requestOptions) {
|
|
@@ -324,20 +332,51 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
324
332
|
async getPublicApiEndpoints(version = "R4", requestOptions) {
|
|
325
333
|
return (0, capability_1.getOvokPublicApiEndpoints)(await this.getCapabilityStatement(version, ...(0, request_options_1.optionalArg)(requestOptions)));
|
|
326
334
|
}
|
|
327
|
-
/**
|
|
328
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Saves one reading's observations, main first, with conditional creates so a retry cannot create a duplicate.
|
|
337
|
+
* A reading too big for one request goes over several, and stops if the signed-in account changes in between.
|
|
338
|
+
*/
|
|
339
|
+
async saveObservations(observations, options) {
|
|
340
|
+
const [entryOptions, requestOptions] = splitSaveOptions(options);
|
|
329
341
|
const linkedObservations = (0, observation_bundle_utils_1.linkObservationMembers)(observations);
|
|
330
342
|
const queue = this.getOfflineMeasurementQueue();
|
|
331
343
|
if (queue) {
|
|
332
|
-
const { id, flush } = await queue.enqueueAndFlush(linkedObservations,
|
|
344
|
+
const { id, flush } = await queue.enqueueAndFlush(linkedObservations, requestOptions, entryOptions);
|
|
333
345
|
const response = flush.responses[id];
|
|
334
346
|
return response
|
|
335
347
|
? { status: "saved", response }
|
|
336
348
|
: { status: "queued", queueId: id };
|
|
337
349
|
}
|
|
338
|
-
const response = await this.
|
|
350
|
+
const response = await this.executeObservationBundle({
|
|
351
|
+
resourceType: "Bundle",
|
|
352
|
+
type: "transaction",
|
|
353
|
+
entry: linkedObservations.map((observation) => (0, offline_1.entryForObservation)(observation, entryOptions)),
|
|
354
|
+
}, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
339
355
|
return { status: "saved", response };
|
|
340
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Saves many readings at once, such as a page of a health import: each group is one reading's observations, main
|
|
359
|
+
* first, linked and named as `saveObservations` does. Readings share batch requests as far as they fit, each whole;
|
|
360
|
+
* one too big for a request goes alone and is split. It is never queued offline: an import reads its source again.
|
|
361
|
+
*/
|
|
362
|
+
async saveObservationGroups(groups, options) {
|
|
363
|
+
const [entryOptions, requestOptions] = splitSaveOptions(options);
|
|
364
|
+
const account = this.accountKey();
|
|
365
|
+
const entryGroups = groups
|
|
366
|
+
.filter((group) => group.length > 0)
|
|
367
|
+
.map((group) => (0, observation_bundle_utils_1.linkObservationMembers)(group).map((observation) => (0, offline_1.entryForObservation)(observation, entryOptions)));
|
|
368
|
+
const responses = [];
|
|
369
|
+
for (const request of (0, observation_bundle_utils_1.packObservationGroups)(entryGroups)) {
|
|
370
|
+
// A reading alone keeps a single save's atomicity; several share a batch.
|
|
371
|
+
const type = request.length === 1 ? "transaction" : "batch";
|
|
372
|
+
responses.push(await this.executeObservationBundle({ resourceType: "Bundle", type, entry: request.flat() }, requestOptions, account));
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
resourceType: "Bundle",
|
|
376
|
+
type: "batch-response",
|
|
377
|
+
entry: responses.flatMap((response) => { var _a; return (_a = response.entry) !== null && _a !== void 0 ? _a : []; }),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
341
380
|
/** Flushes measurements queued while offline. Call this on connectivity/app-resume events. */
|
|
342
381
|
async flushOfflineMeasurementQueue(options = {}, requestOptions) {
|
|
343
382
|
const queue = this.getOfflineMeasurementQueue();
|
|
@@ -363,12 +402,12 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
363
402
|
}
|
|
364
403
|
(_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)), {
|
|
365
404
|
...this.offlineQueueOptions,
|
|
366
|
-
getOwnerKey: () => this.
|
|
405
|
+
getOwnerKey: () => this.accountKey(),
|
|
367
406
|
}));
|
|
368
407
|
return this.offlineMeasurementQueue;
|
|
369
408
|
}
|
|
370
|
-
/**
|
|
371
|
-
|
|
409
|
+
/** The signed-in account, on its server and project: what queued measurements and a running save belong to. */
|
|
410
|
+
accountKey() {
|
|
372
411
|
var _a, _b, _c;
|
|
373
412
|
const login = this.getActiveLogin();
|
|
374
413
|
const profileReference = (_a = login === null || login === void 0 ? void 0 : login.profile) === null || _a === void 0 ? void 0 : _a.reference;
|
|
@@ -474,16 +513,17 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
474
513
|
// Keep the saved login available for a later retry if restoration is offline.
|
|
475
514
|
}
|
|
476
515
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Sends a save's bundle, over several requests when it must. A later request isn't sent once the signed-in
|
|
518
|
+
* account differs from `account`: the rest of a reading is never written under someone else's sign-in.
|
|
519
|
+
*/
|
|
520
|
+
async executeObservationBundle(bundle, requestOptions, account = this.accountKey()) {
|
|
521
|
+
const response = await (0, observation_bundle_utils_1.executeObservationBundle)(bundle, (request) => {
|
|
522
|
+
if (this.accountKey() !== account) {
|
|
523
|
+
throw new Error("The signed-in account changed during the save; the rest of it was not sent.");
|
|
524
|
+
}
|
|
525
|
+
return this.executeBatch(request, ...(0, request_options_1.optionalArg)(requestOptions));
|
|
526
|
+
});
|
|
487
527
|
this.invalidateSearches("Observation");
|
|
488
528
|
return response;
|
|
489
529
|
}
|
|
@@ -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.41",
|
|
52
52
|
},
|
|
53
53
|
fhirVersion: "4.0.1",
|
|
54
54
|
format: ["json"],
|
package/dist/index.d.ts
CHANGED
|
@@ -934,13 +934,14 @@ export type { OvokSavedAccount } from "./client/auth/types/Account";
|
|
|
934
934
|
export { LoginError } from "./client/auth/types/LoginError";
|
|
935
935
|
export type { LoginProfile, TenantLoginStart } from "./client/auth/types/LoginError";
|
|
936
936
|
export { MAX_CONDITIONAL_OBSERVATION_ENTRIES, MAX_OBSERVATION_BUNDLE_BYTES, } from "./client/ovok-client";
|
|
937
|
-
export type { SaveObservationsResult } from "./client/ovok-client";
|
|
937
|
+
export type { SaveObservationsOptions, SaveObservationsResult } from "./client/ovok-client";
|
|
938
938
|
export * from "./client/offline";
|
|
939
939
|
export * from "./hooks";
|
|
940
940
|
export * from "./utils";
|
|
941
941
|
export * from "./client/observation/services";
|
|
942
942
|
export * from "./client/observation/methods";
|
|
943
943
|
export * from "./client/observation/measurement-fields";
|
|
944
|
+
export * from "./client/observation/series";
|
|
944
945
|
export { saveMeasurement } from "./client/observation/methods/saveMeasurement";
|
|
945
946
|
export * from "./conformance/capability-requirements";
|
|
946
947
|
export * from "./conformance/api-requirements";
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,7 @@ __exportStar(require("./utils"), exports);
|
|
|
59
59
|
__exportStar(require("./client/observation/services"), exports);
|
|
60
60
|
__exportStar(require("./client/observation/methods"), exports);
|
|
61
61
|
__exportStar(require("./client/observation/measurement-fields"), exports);
|
|
62
|
+
__exportStar(require("./client/observation/series"), exports);
|
|
62
63
|
var saveMeasurement_1 = require("./client/observation/methods/saveMeasurement");
|
|
63
64
|
Object.defineProperty(exports, "saveMeasurement", { enumerable: true, get: function () { return saveMeasurement_1.saveMeasurement; } });
|
|
64
65
|
__exportStar(require("./conformance/capability-requirements"), exports);
|