@ovok/core 0.2.59 → 0.2.62
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 +29 -0
- package/dist/client/observation/methods/generateObservationBodiesByMeasurement.js +25 -7
- package/dist/client/observation/services/ecg-observation-fragments.d.ts +4 -0
- package/dist/client/observation/services/ecg-observation-fragments.js +96 -0
- package/dist/client/observation/services/ecg.d.ts +3 -0
- package/dist/client/observation/services/ecg.js +6 -0
- package/dist/client/observation/services/uric-acid-service.d.ts +8 -0
- package/dist/client/observation/services/uric-acid-service.js +47 -1
- package/dist/client/observation/types/IObservationService.d.ts +2 -0
- package/dist/client/observation/types/UrineAnalytes.d.ts +12 -0
- package/dist/client/observation/types/UrineAnalytes.js +21 -0
- package/dist/client/observation/types/generate-observation-body/ObservationFragment.d.ts +12 -0
- package/dist/client/observation/types/generate-observation-body/ObservationFragment.js +2 -0
- package/dist/client/observation/types/measurement/EcgMeasurement.d.ts +2 -0
- package/dist/client/observation/types/measurement/UrineAnalyzeMeasurement.d.ts +2 -0
- package/dist/client/offline/index.d.ts +1 -0
- package/dist/client/offline/index.js +17 -0
- package/dist/client/offline/offline-measurement-queue.d.ts +44 -0
- package/dist/client/offline/offline-measurement-queue.js +186 -0
- package/dist/client/ovok-client.d.ts +21 -0
- package/dist/client/ovok-client.js +63 -1
- package/dist/conformance/capability-requirements.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Core TypeScript SDK for healthcare applications. Provides authentication, AI-FHI
|
|
|
13
13
|
- 💬 Chat AI integration
|
|
14
14
|
- 🤖 Bot execution framework
|
|
15
15
|
- 📋 Questionnaire responses
|
|
16
|
+
- 📥 Opt-in durable offline measurement queue with idempotent retries
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -38,3 +39,31 @@ const client = new OvokClient({
|
|
|
38
39
|
## Documentation
|
|
39
40
|
|
|
40
41
|
Full documentation and examples available at **[sdk.ovok.com](https://sdk.ovok.com/)**
|
|
42
|
+
|
|
43
|
+
## Offline measurements
|
|
44
|
+
|
|
45
|
+
Pass a durable Medplum storage adapter and opt in to queue measurements while offline:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const client = new OvokClient({
|
|
49
|
+
storage,
|
|
50
|
+
offlineQueue: { enabled: true },
|
|
51
|
+
// other Medplum configuration
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await client.saveMeasurement({
|
|
55
|
+
patientId: "patient-id",
|
|
56
|
+
measurement: {
|
|
57
|
+
measurementTypeKey: MeasurementTypeKey.temperature,
|
|
58
|
+
bodyTemp: 36.5,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// result.status is "saved" or "queued".
|
|
63
|
+
// Call this from the host app's connectivity and app-resume listeners.
|
|
64
|
+
await client.flushOfflineMeasurementQueue();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Queued observations are persisted in `storage`, retried with exponential backoff, and sent as
|
|
68
|
+
conditional creates using a stable identifier. A retry after a process death therefore cannot
|
|
69
|
+
create a second copy of the same observation. The queue is opt-in and requires durable storage.
|
|
@@ -12,14 +12,31 @@ const getPatientReference = async (client, patientId) => {
|
|
|
12
12
|
return (0, core_1.getReferenceString)(profile);
|
|
13
13
|
};
|
|
14
14
|
async function generateObservationBodiesByMeasurement(params) {
|
|
15
|
+
var _a;
|
|
15
16
|
const { device, measurement } = params;
|
|
16
17
|
const measurementTypeKey = measurement.measurementTypeKey;
|
|
17
18
|
const patientReference = await getPatientReference(this, params.patientId);
|
|
18
19
|
const observationService = creator_1.ObservationServiceCreator.getInstance().getService(measurementTypeKey);
|
|
20
|
+
const deviceObject = (0, getDeviceObject_1.getDeviceObject)(device);
|
|
21
|
+
const observationFragments = (_a = observationService.generateObservationFragments) === null || _a === void 0 ? void 0 : _a.call(observationService, measurement);
|
|
22
|
+
if (observationFragments) {
|
|
23
|
+
return observationFragments
|
|
24
|
+
.map((fragment) => ({
|
|
25
|
+
resourceType: "Observation",
|
|
26
|
+
status: "registered",
|
|
27
|
+
subject: {
|
|
28
|
+
reference: patientReference,
|
|
29
|
+
},
|
|
30
|
+
device: deviceObject,
|
|
31
|
+
...fragment.value,
|
|
32
|
+
code: fragment.code,
|
|
33
|
+
...(fragment.additionalProperties || {}),
|
|
34
|
+
}))
|
|
35
|
+
.filter(hasObservationValue);
|
|
36
|
+
}
|
|
19
37
|
const observationCodes = observationService.getObservationCodes();
|
|
20
38
|
const values = observationService.generateValues(measurement);
|
|
21
39
|
const additionalProperties = observationService.getAdditionalProperties(measurement);
|
|
22
|
-
const deviceObject = (0, getDeviceObject_1.getDeviceObject)(device);
|
|
23
40
|
const observationBodies = observationCodes.map((code, index) => ({
|
|
24
41
|
resourceType: "Observation",
|
|
25
42
|
status: "registered",
|
|
@@ -31,10 +48,11 @@ async function generateObservationBodiesByMeasurement(params) {
|
|
|
31
48
|
code,
|
|
32
49
|
...(additionalProperties[index] || {}),
|
|
33
50
|
}));
|
|
34
|
-
return observationBodies.filter(
|
|
35
|
-
var _a, _b, _c, _d, _e;
|
|
36
|
-
return ((_a = observation.valueQuantity) === null || _a === void 0 ? void 0 : _a.value) ||
|
|
37
|
-
((_b = observation.valueSampledData) === null || _b === void 0 ? void 0 : _b.data) ||
|
|
38
|
-
((_e = (_d = (_c = observation.valueCodeableConcept) === null || _c === void 0 ? void 0 : _c.coding) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.code);
|
|
39
|
-
});
|
|
51
|
+
return observationBodies.filter(hasObservationValue);
|
|
40
52
|
}
|
|
53
|
+
const hasObservationValue = (observation) => {
|
|
54
|
+
var _a, _b, _c, _d, _e;
|
|
55
|
+
return ((_a = observation.valueQuantity) === null || _a === void 0 ? void 0 : _a.value) !== undefined ||
|
|
56
|
+
Boolean((_b = observation.valueSampledData) === null || _b === void 0 ? void 0 : _b.data) ||
|
|
57
|
+
Boolean((_e = (_d = (_c = observation.valueCodeableConcept) === null || _c === void 0 ? void 0 : _c.coding) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.code);
|
|
58
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CodeableConcept } from "@medplum/fhirtypes";
|
|
2
|
+
import { EcgMeasurement } from "../../../types";
|
|
3
|
+
import { ObservationFragment } from "../types/generate-observation-body/ObservationFragment";
|
|
4
|
+
export declare const generateEcgObservationFragments: (measurement: EcgMeasurement, observationCodes: CodeableConcept[]) => ObservationFragment[];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateEcgObservationFragments = void 0;
|
|
4
|
+
const MAX_SEGMENT_DURATION_SECONDS = 5 * 60;
|
|
5
|
+
const generateEcgObservationFragments = (measurement, observationCodes) => {
|
|
6
|
+
const fragments = [];
|
|
7
|
+
const diagramPoints = measurement.diagramPoints;
|
|
8
|
+
const recordedAt = isValidDate(measurement.recordedAt)
|
|
9
|
+
? measurement.recordedAt
|
|
10
|
+
: new Date();
|
|
11
|
+
const duration = typeof measurement.duration === "number" &&
|
|
12
|
+
Number.isFinite(measurement.duration) &&
|
|
13
|
+
measurement.duration > 0
|
|
14
|
+
? measurement.duration
|
|
15
|
+
: undefined;
|
|
16
|
+
if (diagramPoints === null || diagramPoints === void 0 ? void 0 : diagramPoints.length) {
|
|
17
|
+
const maxSamples = duration
|
|
18
|
+
? Math.max(1, Math.floor((MAX_SEGMENT_DURATION_SECONDS * diagramPoints.length) / duration))
|
|
19
|
+
: diagramPoints.length;
|
|
20
|
+
const recordingId = `ecg-${recordedAt.getTime()}-${diagramPoints.length}`;
|
|
21
|
+
const ecgCode = observationCodes[0];
|
|
22
|
+
for (let startIndex = 0, segmentIndex = 0; startIndex < diagramPoints.length; startIndex += maxSamples, segmentIndex++) {
|
|
23
|
+
const segment = diagramPoints.slice(startIndex, startIndex + maxSamples);
|
|
24
|
+
const segmentDuration = duration
|
|
25
|
+
? (duration * segment.length) / diagramPoints.length
|
|
26
|
+
: segment.length / 1000;
|
|
27
|
+
const segmentStart = new Date(recordedAt.getTime() +
|
|
28
|
+
(duration
|
|
29
|
+
? (duration * startIndex * 1000) / diagramPoints.length
|
|
30
|
+
: startIndex));
|
|
31
|
+
const segmentEnd = new Date(segmentStart.getTime() + segmentDuration * 1000);
|
|
32
|
+
const firstId = recordingId;
|
|
33
|
+
fragments.push({
|
|
34
|
+
code: ecgCode,
|
|
35
|
+
value: {
|
|
36
|
+
valueSampledData: {
|
|
37
|
+
origin: { value: 0, unit: "mV" },
|
|
38
|
+
period: duration
|
|
39
|
+
? (duration * 1000) / diagramPoints.length
|
|
40
|
+
: 1,
|
|
41
|
+
dimensions: 1,
|
|
42
|
+
data: segment.map((sample) => Number(sample.toFixed(3))).join(" "),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
additionalProperties: {
|
|
46
|
+
id: segmentIndex === 0 ? firstId : `${recordingId}-${segmentIndex}`,
|
|
47
|
+
effectivePeriod: {
|
|
48
|
+
start: segmentStart.toISOString(),
|
|
49
|
+
end: segmentEnd.toISOString(),
|
|
50
|
+
},
|
|
51
|
+
hasMember: segmentIndex === 0
|
|
52
|
+
? []
|
|
53
|
+
: [{ reference: `Observation/${firstId}` }],
|
|
54
|
+
...(segmentIndex === 0 && measurement.diagnosticResult
|
|
55
|
+
? { interpretation: [{ text: measurement.diagnosticResult }] }
|
|
56
|
+
: {}),
|
|
57
|
+
...(segmentIndex === 0 && duration !== undefined
|
|
58
|
+
? { component: [durationComponent(duration)] }
|
|
59
|
+
: {}),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (typeof measurement.heartRate === "number") {
|
|
65
|
+
fragments.push({
|
|
66
|
+
code: observationCodes[1],
|
|
67
|
+
value: {
|
|
68
|
+
valueQuantity: {
|
|
69
|
+
value: Number(measurement.heartRate.toFixed(0)),
|
|
70
|
+
unit: "bpm",
|
|
71
|
+
code: "8867-4",
|
|
72
|
+
system: "http://loinc.org",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
additionalProperties: {
|
|
76
|
+
effectiveDateTime: recordedAt.toISOString(),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return fragments;
|
|
81
|
+
};
|
|
82
|
+
exports.generateEcgObservationFragments = generateEcgObservationFragments;
|
|
83
|
+
const durationComponent = (duration) => ({
|
|
84
|
+
code: {
|
|
85
|
+
text: "measurement-duration",
|
|
86
|
+
coding: [{ code: "measurement-duration" }],
|
|
87
|
+
},
|
|
88
|
+
id: "measurement-duration",
|
|
89
|
+
valueQuantity: {
|
|
90
|
+
value: duration,
|
|
91
|
+
unit: "s",
|
|
92
|
+
code: "s",
|
|
93
|
+
system: "http://unitsofmeasure.org",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const isValidDate = (value) => value instanceof Date && !Number.isNaN(value.getTime());
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Observation } from "@medplum/fhirtypes";
|
|
2
2
|
import { EcgMeasurement, Measurement, MeasurementTypeKey } from "../../../types";
|
|
3
|
+
import { ObservationFragment } from "../types/generate-observation-body/ObservationFragment";
|
|
3
4
|
import { ObservationValue } from "../types/generate-observation-body/ObservationValue";
|
|
4
5
|
import { ObservationMetadata } from "../types/service/ObservationMetadata";
|
|
5
6
|
import { BaseObservationService } from "./base-observation-service";
|
|
@@ -8,6 +9,8 @@ export declare class EcgService extends BaseObservationService<EcgMeasurement> {
|
|
|
8
9
|
protected measurementTypeName: string;
|
|
9
10
|
protected observationMetadata: ObservationMetadata<EcgMeasurement>[];
|
|
10
11
|
generateValues(measurement: Measurement): ObservationValue[];
|
|
12
|
+
/** Expand long recordings into bounded, timestamped observations. */
|
|
13
|
+
generateObservationFragments(measurement: Measurement): ObservationFragment[];
|
|
11
14
|
getAdditionalProperties(measurement: Measurement): Partial<Observation>[];
|
|
12
15
|
getMeasurementDisplay(measurement: Measurement): string;
|
|
13
16
|
getObservationDisplay(observations: Observation[]): string | undefined;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.EcgService = void 0;
|
|
4
4
|
const types_1 = require("../../../types");
|
|
5
5
|
const base_observation_service_1 = require("./base-observation-service");
|
|
6
|
+
const ecg_observation_fragments_1 = require("./ecg-observation-fragments");
|
|
6
7
|
class EcgService extends base_observation_service_1.BaseObservationService {
|
|
7
8
|
constructor() {
|
|
8
9
|
super(...arguments);
|
|
@@ -59,6 +60,11 @@ class EcgService extends base_observation_service_1.BaseObservationService {
|
|
|
59
60
|
}
|
|
60
61
|
return values;
|
|
61
62
|
}
|
|
63
|
+
/** Expand long recordings into bounded, timestamped observations. */
|
|
64
|
+
generateObservationFragments(measurement) {
|
|
65
|
+
const castedMeasurement = this.getCastedMeasurement(measurement);
|
|
66
|
+
return (0, ecg_observation_fragments_1.generateEcgObservationFragments)(castedMeasurement, this.getObservationCodes());
|
|
67
|
+
}
|
|
62
68
|
getAdditionalProperties(measurement) {
|
|
63
69
|
const castedMeasurement = this.getCastedMeasurement(measurement);
|
|
64
70
|
const additionalProperties = [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Observation } from "@medplum/fhirtypes";
|
|
2
2
|
import { Measurement, MeasurementTypeKey, Sample, UrineAnalyzeMeasurement } from "../../../types";
|
|
3
3
|
import { RecordResult, RecordType } from "../types/generate-measurement/HealthConnectTypes";
|
|
4
|
+
import { ObservationFragment } from "../types/generate-observation-body/ObservationFragment";
|
|
4
5
|
import { ObservationValue } from "../types/generate-observation-body/ObservationValue";
|
|
5
6
|
import { ObservationMetadata } from "../types/service/ObservationMetadata";
|
|
6
7
|
import { BaseObservationService } from "./base-observation-service";
|
|
@@ -11,6 +12,13 @@ export declare class UricAcidService extends BaseObservationService<UrineAnalyze
|
|
|
11
12
|
protected observationMetadata: ObservationMetadata<UrineAnalyzeMeasurement>[];
|
|
12
13
|
generateValues(measurement: Measurement): ObservationValue[];
|
|
13
14
|
getAdditionalProperties(measurement: Measurement): Partial<Observation>[];
|
|
15
|
+
/**
|
|
16
|
+
* A strip contains independent analyte readings. Emit each measured value
|
|
17
|
+
* as its own Observation so zero is retained and decimal values never pass
|
|
18
|
+
* through the integer-only component representation used by the legacy
|
|
19
|
+
* panel implementation.
|
|
20
|
+
*/
|
|
21
|
+
generateObservationFragments(measurement: Measurement): ObservationFragment[];
|
|
14
22
|
getMeasurementDisplay(measurement: Measurement): string;
|
|
15
23
|
getObservationDisplay(observations: Observation[]): string | undefined;
|
|
16
24
|
convertSamplesToMeasurement(_samples: Sample[]): UrineAnalyzeMeasurement;
|
|
@@ -36,7 +36,9 @@ class UricAcidService extends base_observation_service_1.BaseObservationService
|
|
|
36
36
|
getAdditionalProperties(measurement) {
|
|
37
37
|
const castedMeasurement = this.getCastedMeasurement(measurement);
|
|
38
38
|
const components = Object.entries(castedMeasurement)
|
|
39
|
-
.filter(([key]) => key !== "measurementTypeKey" &&
|
|
39
|
+
.filter(([key]) => key !== "measurementTypeKey" &&
|
|
40
|
+
key !== "urineValues" &&
|
|
41
|
+
key !== "recordedAt")
|
|
40
42
|
.map(([key, value]) => this.getComponent(key, value));
|
|
41
43
|
return [
|
|
42
44
|
{
|
|
@@ -45,6 +47,50 @@ class UricAcidService extends base_observation_service_1.BaseObservationService
|
|
|
45
47
|
},
|
|
46
48
|
];
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* A strip contains independent analyte readings. Emit each measured value
|
|
52
|
+
* as its own Observation so zero is retained and decimal values never pass
|
|
53
|
+
* through the integer-only component representation used by the legacy
|
|
54
|
+
* panel implementation.
|
|
55
|
+
*/
|
|
56
|
+
generateObservationFragments(measurement) {
|
|
57
|
+
const castedMeasurement = this.getCastedMeasurement(measurement);
|
|
58
|
+
const effectiveDateTime = castedMeasurement.recordedAt instanceof Date &&
|
|
59
|
+
!Number.isNaN(castedMeasurement.recordedAt.getTime())
|
|
60
|
+
? castedMeasurement.recordedAt.toISOString()
|
|
61
|
+
: new Date().toISOString();
|
|
62
|
+
return types_1.URINE_ANALYTE_DEFINITIONS.flatMap((analyte) => {
|
|
63
|
+
const value = castedMeasurement[analyte.key];
|
|
64
|
+
if (value === undefined || value === null || !Number.isFinite(value)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
code: {
|
|
70
|
+
coding: [
|
|
71
|
+
{
|
|
72
|
+
code: analyte.code,
|
|
73
|
+
display: analyte.display,
|
|
74
|
+
system: "http://loinc.org",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
text: analyte.display,
|
|
78
|
+
},
|
|
79
|
+
value: {
|
|
80
|
+
valueQuantity: {
|
|
81
|
+
value,
|
|
82
|
+
unit: "1",
|
|
83
|
+
code: "1",
|
|
84
|
+
system: "http://unitsofmeasure.org",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
additionalProperties: {
|
|
88
|
+
effectiveDateTime,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
});
|
|
93
|
+
}
|
|
48
94
|
getMeasurementDisplay(measurement) {
|
|
49
95
|
const castedMeasurement = this.getCastedMeasurement(measurement);
|
|
50
96
|
const problemCount = this.countProblems(castedMeasurement);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { CodeableConcept, Observation } from "@medplum/fhirtypes";
|
|
2
2
|
import { RecordResult, RecordType } from "./generate-measurement/HealthConnectTypes";
|
|
3
3
|
import { Sample } from "./generate-measurement/Sample";
|
|
4
|
+
import { ObservationFragment } from "./generate-observation-body/ObservationFragment";
|
|
4
5
|
import { ObservationValue } from "./generate-observation-body/ObservationValue";
|
|
5
6
|
import { Measurement } from "./measurement/Measurement";
|
|
6
7
|
export interface IObservationService {
|
|
7
8
|
generateValues: (measurement: Measurement) => ObservationValue[];
|
|
9
|
+
generateObservationFragments?: (measurement: Measurement) => ObservationFragment[];
|
|
8
10
|
getObservationCodes: () => CodeableConcept[];
|
|
9
11
|
getAdditionalProperties: (measurement: Measurement) => Partial<Observation>[];
|
|
10
12
|
getUnits: () => string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type UrineAnalyteKey = "uro" | "bil" | "ket" | "glu" | "pro" | "ph" | "bld" | "nit" | "leu" | "sg" | "vc";
|
|
2
|
+
export type UrineAnalyteDefinition = {
|
|
3
|
+
key: UrineAnalyteKey;
|
|
4
|
+
code: string;
|
|
5
|
+
display: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* LOINC mappings for the eleven values returned by Contec's BC401 strip.
|
|
9
|
+
* These are exported so Native declarations and consuming apps can describe
|
|
10
|
+
* the same analytes without maintaining a second, drifting table.
|
|
11
|
+
*/
|
|
12
|
+
export declare const URINE_ANALYTE_DEFINITIONS: readonly UrineAnalyteDefinition[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.URINE_ANALYTE_DEFINITIONS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* LOINC mappings for the eleven values returned by Contec's BC401 strip.
|
|
6
|
+
* These are exported so Native declarations and consuming apps can describe
|
|
7
|
+
* the same analytes without maintaining a second, drifting table.
|
|
8
|
+
*/
|
|
9
|
+
exports.URINE_ANALYTE_DEFINITIONS = [
|
|
10
|
+
{ key: "uro", code: "50563-6", display: "Urobilinogen" },
|
|
11
|
+
{ key: "bld", code: "50559-4", display: "Hemoglobin" },
|
|
12
|
+
{ key: "bil", code: "53327-3", display: "Bilirubin" },
|
|
13
|
+
{ key: "ket", code: "50557-8", display: "Ketones" },
|
|
14
|
+
{ key: "glu", code: "53328-1", display: "Glucose" },
|
|
15
|
+
{ key: "pro", code: "50561-0", display: "Protein" },
|
|
16
|
+
{ key: "ph", code: "50560-2", display: "Urine pH" },
|
|
17
|
+
{ key: "nit", code: "50558-6", display: "Nitrite" },
|
|
18
|
+
{ key: "leu", code: "60026-2", display: "Leukocyte esterase" },
|
|
19
|
+
{ key: "sg", code: "53326-5", display: "Specific gravity" },
|
|
20
|
+
{ key: "vc", code: "5768-7", display: "Ascorbate (vitamin C)" },
|
|
21
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CodeableConcept, Observation } from "@medplum/fhirtypes";
|
|
2
|
+
import { ObservationValue } from "./ObservationValue";
|
|
3
|
+
/**
|
|
4
|
+
* A service-specific observation that is expanded by the common observation
|
|
5
|
+
* body generator. This is used when one measurement has to produce more than
|
|
6
|
+
* one FHIR Observation, such as a long ECG or a urine strip's analytes.
|
|
7
|
+
*/
|
|
8
|
+
export type ObservationFragment = {
|
|
9
|
+
code: CodeableConcept;
|
|
10
|
+
value?: ObservationValue;
|
|
11
|
+
additionalProperties?: Partial<Observation>;
|
|
12
|
+
};
|
|
@@ -13,6 +13,8 @@ export type UrineAnalyzeMeasurement = Measurement & {
|
|
|
13
13
|
sg?: number;
|
|
14
14
|
vc?: number;
|
|
15
15
|
urineValues: UrineAnalysisValue[];
|
|
16
|
+
/** Time at which the analyzer recorded the strip. */
|
|
17
|
+
recordedAt?: Date;
|
|
16
18
|
};
|
|
17
19
|
type UrineAnalysisValue = {
|
|
18
20
|
code: CodeableConcept;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./offline-measurement-queue";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./offline-measurement-queue"), exports);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { IClientStorage } from "@medplum/core";
|
|
2
|
+
import { Bundle, BundleEntry, Observation } from "@medplum/fhirtypes";
|
|
3
|
+
declare const DEFAULT_STORAGE_KEY = "@ovok/core/offline-measurements";
|
|
4
|
+
declare const IDENTIFIER_SYSTEM = "https://api.ovok.com/fhir/StructureDefinition/offline-measurement-id";
|
|
5
|
+
export type OfflineMeasurementQueueOptions = {
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
storageKey?: string;
|
|
8
|
+
maxRetries?: number;
|
|
9
|
+
retryBackoffMs?: number;
|
|
10
|
+
maxRetryBackoffMs?: number;
|
|
11
|
+
};
|
|
12
|
+
export type OfflineMeasurementFlushOptions = {
|
|
13
|
+
includeExhausted?: boolean;
|
|
14
|
+
resetExhausted?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type OfflineMeasurementFlushResult = {
|
|
17
|
+
attempted: number;
|
|
18
|
+
saved: number;
|
|
19
|
+
queued: number;
|
|
20
|
+
exhausted: number;
|
|
21
|
+
responses: Record<string, Bundle>;
|
|
22
|
+
};
|
|
23
|
+
type MeasurementTransport = (bundle: Bundle) => Promise<Bundle>;
|
|
24
|
+
declare const entryForObservation: (observation: Observation) => BundleEntry<Observation>;
|
|
25
|
+
export declare class OfflineMeasurementQueue {
|
|
26
|
+
private readonly storage;
|
|
27
|
+
private readonly transport;
|
|
28
|
+
private readonly storageKey;
|
|
29
|
+
private readonly maxRetries;
|
|
30
|
+
private readonly retryBackoffMs;
|
|
31
|
+
private readonly maxRetryBackoffMs;
|
|
32
|
+
private operation;
|
|
33
|
+
constructor(storage: IClientStorage, transport: MeasurementTransport, options?: OfflineMeasurementQueueOptions);
|
|
34
|
+
enqueueAndFlush: (observations: Observation[]) => Promise<{
|
|
35
|
+
id: string;
|
|
36
|
+
flush: OfflineMeasurementFlushResult;
|
|
37
|
+
}>;
|
|
38
|
+
flush: (options?: OfflineMeasurementFlushOptions) => Promise<OfflineMeasurementFlushResult>;
|
|
39
|
+
private runExclusive;
|
|
40
|
+
private readQueue;
|
|
41
|
+
private writeQueue;
|
|
42
|
+
private flushQueue;
|
|
43
|
+
}
|
|
44
|
+
export { DEFAULT_STORAGE_KEY as DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY, IDENTIFIER_SYSTEM as OFFLINE_MEASUREMENT_IDENTIFIER_SYSTEM, entryForObservation, };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.entryForObservation = exports.OFFLINE_MEASUREMENT_IDENTIFIER_SYSTEM = exports.DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY = exports.OfflineMeasurementQueue = void 0;
|
|
4
|
+
const DEFAULT_STORAGE_KEY = "@ovok/core/offline-measurements";
|
|
5
|
+
exports.DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY = DEFAULT_STORAGE_KEY;
|
|
6
|
+
const DEFAULT_MAX_RETRIES = 8;
|
|
7
|
+
const DEFAULT_RETRY_BACKOFF_MS = 5000;
|
|
8
|
+
const DEFAULT_MAX_RETRY_BACKOFF_MS = 60 * 60 * 1000;
|
|
9
|
+
const IDENTIFIER_SYSTEM = "https://api.ovok.com/fhir/StructureDefinition/offline-measurement-id";
|
|
10
|
+
exports.OFFLINE_MEASUREMENT_IDENTIFIER_SYSTEM = IDENTIFIER_SYSTEM;
|
|
11
|
+
const canonicalize = (value) => {
|
|
12
|
+
if (value instanceof Date) {
|
|
13
|
+
return value.toISOString();
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return value.map(canonicalize);
|
|
17
|
+
}
|
|
18
|
+
if (value && typeof value === "object") {
|
|
19
|
+
return Object.fromEntries(Object.entries(value)
|
|
20
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
21
|
+
.map(([key, nested]) => [key, canonicalize(nested)]));
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
const hash = (value) => {
|
|
26
|
+
let first = 0x811c9dc5;
|
|
27
|
+
let second = 0x01000193;
|
|
28
|
+
for (let index = 0; index < value.length; index++) {
|
|
29
|
+
const code = value.charCodeAt(index);
|
|
30
|
+
first ^= code;
|
|
31
|
+
first = Math.imul(first, 0x01000193);
|
|
32
|
+
second ^= code + index;
|
|
33
|
+
second = Math.imul(second, 0x811c9dc5);
|
|
34
|
+
}
|
|
35
|
+
return `${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0)
|
|
36
|
+
.toString(16)
|
|
37
|
+
.padStart(8, "0")}`;
|
|
38
|
+
};
|
|
39
|
+
const stableMeasurementId = (observation) => hash(JSON.stringify(canonicalize(observation)));
|
|
40
|
+
const entryForObservation = (observation) => {
|
|
41
|
+
var _a;
|
|
42
|
+
const id = stableMeasurementId(observation);
|
|
43
|
+
const identifier = {
|
|
44
|
+
system: IDENTIFIER_SYSTEM,
|
|
45
|
+
value: id,
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
fullUrl: `urn:uuid:${id}`,
|
|
49
|
+
request: {
|
|
50
|
+
method: "POST",
|
|
51
|
+
url: "Observation",
|
|
52
|
+
ifNoneExist: `identifier=${IDENTIFIER_SYSTEM}|${id}`,
|
|
53
|
+
},
|
|
54
|
+
resource: {
|
|
55
|
+
...observation,
|
|
56
|
+
identifier: [...((_a = observation.identifier) !== null && _a !== void 0 ? _a : []), identifier],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
exports.entryForObservation = entryForObservation;
|
|
61
|
+
const isFailedResponse = (bundle) => {
|
|
62
|
+
var _a, _b;
|
|
63
|
+
return (_b = (_a = bundle.entry) === null || _a === void 0 ? void 0 : _a.some((entry) => {
|
|
64
|
+
var _a, _b;
|
|
65
|
+
const status = (_b = (_a = entry.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : "";
|
|
66
|
+
return status.startsWith("4") || status.startsWith("5");
|
|
67
|
+
})) !== null && _b !== void 0 ? _b : false;
|
|
68
|
+
};
|
|
69
|
+
class OfflineMeasurementQueue {
|
|
70
|
+
constructor(storage, transport, options = {}) {
|
|
71
|
+
var _a, _b, _c, _d;
|
|
72
|
+
this.storage = storage;
|
|
73
|
+
this.transport = transport;
|
|
74
|
+
this.operation = Promise.resolve();
|
|
75
|
+
this.enqueueAndFlush = (observations) => this.runExclusive(async () => {
|
|
76
|
+
var _a, _b;
|
|
77
|
+
await ((_b = (_a = this.storage).getInitPromise) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
78
|
+
const queue = this.readQueue();
|
|
79
|
+
const entries = observations.map(entryForObservation);
|
|
80
|
+
const id = hash(JSON.stringify(canonicalize(entries)));
|
|
81
|
+
if (!queue.some((item) => item.id === id)) {
|
|
82
|
+
queue.push({
|
|
83
|
+
id,
|
|
84
|
+
queuedAt: new Date().toISOString(),
|
|
85
|
+
attempt: 0,
|
|
86
|
+
nextAttemptAt: 0,
|
|
87
|
+
entries,
|
|
88
|
+
});
|
|
89
|
+
this.writeQueue(queue);
|
|
90
|
+
}
|
|
91
|
+
return { id, flush: await this.flushQueue(queue) };
|
|
92
|
+
});
|
|
93
|
+
this.flush = (options = {}) => this.runExclusive(async () => {
|
|
94
|
+
var _a, _b;
|
|
95
|
+
await ((_b = (_a = this.storage).getInitPromise) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
96
|
+
return this.flushQueue(this.readQueue(), options);
|
|
97
|
+
});
|
|
98
|
+
this.runExclusive = (operation) => {
|
|
99
|
+
const next = this.operation.then(operation, operation);
|
|
100
|
+
this.operation = next.catch(() => undefined);
|
|
101
|
+
return next;
|
|
102
|
+
};
|
|
103
|
+
this.readQueue = () => {
|
|
104
|
+
const stored = this.storage.getObject(this.storageKey);
|
|
105
|
+
if (stored === undefined) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
if (!Array.isArray(stored)) {
|
|
109
|
+
throw new Error("Offline measurement queue is not an array.");
|
|
110
|
+
}
|
|
111
|
+
return stored;
|
|
112
|
+
};
|
|
113
|
+
this.writeQueue = (queue) => {
|
|
114
|
+
this.storage.setObject(this.storageKey, queue);
|
|
115
|
+
};
|
|
116
|
+
this.flushQueue = async (queue, options = {}) => {
|
|
117
|
+
const result = {
|
|
118
|
+
attempted: 0,
|
|
119
|
+
saved: 0,
|
|
120
|
+
queued: 0,
|
|
121
|
+
exhausted: 0,
|
|
122
|
+
responses: {},
|
|
123
|
+
};
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
let changed = false;
|
|
126
|
+
for (const item of [...queue]) {
|
|
127
|
+
if (item.exhausted && options.resetExhausted) {
|
|
128
|
+
item.attempt = 0;
|
|
129
|
+
item.exhausted = false;
|
|
130
|
+
item.nextAttemptAt = 0;
|
|
131
|
+
changed = true;
|
|
132
|
+
}
|
|
133
|
+
if ((item.exhausted && !options.includeExhausted) ||
|
|
134
|
+
item.nextAttemptAt > now) {
|
|
135
|
+
if (item.exhausted) {
|
|
136
|
+
result.exhausted++;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
result.queued++;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
result.attempted++;
|
|
144
|
+
const attempt = item.attempt + 1;
|
|
145
|
+
try {
|
|
146
|
+
const response = await this.transport({
|
|
147
|
+
resourceType: "Bundle",
|
|
148
|
+
type: "transaction",
|
|
149
|
+
entry: item.entries,
|
|
150
|
+
});
|
|
151
|
+
if (isFailedResponse(response)) {
|
|
152
|
+
throw response;
|
|
153
|
+
}
|
|
154
|
+
queue.splice(queue.indexOf(item), 1);
|
|
155
|
+
result.saved++;
|
|
156
|
+
result.responses[item.id] = response;
|
|
157
|
+
changed = true;
|
|
158
|
+
}
|
|
159
|
+
catch (_a) {
|
|
160
|
+
item.attempt = attempt;
|
|
161
|
+
item.exhausted = attempt >= this.maxRetries;
|
|
162
|
+
item.nextAttemptAt = item.exhausted
|
|
163
|
+
? Number.MAX_SAFE_INTEGER
|
|
164
|
+
: Date.now() +
|
|
165
|
+
Math.min(this.maxRetryBackoffMs, this.retryBackoffMs * 2 ** (attempt - 1));
|
|
166
|
+
changed = true;
|
|
167
|
+
if (item.exhausted) {
|
|
168
|
+
result.exhausted++;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
result.queued++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (changed) {
|
|
176
|
+
this.writeQueue(queue);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
};
|
|
180
|
+
this.storageKey = (_a = options.storageKey) !== null && _a !== void 0 ? _a : DEFAULT_STORAGE_KEY;
|
|
181
|
+
this.maxRetries = Math.max(1, (_b = options.maxRetries) !== null && _b !== void 0 ? _b : DEFAULT_MAX_RETRIES);
|
|
182
|
+
this.retryBackoffMs = Math.max(1, (_c = options.retryBackoffMs) !== null && _c !== void 0 ? _c : DEFAULT_RETRY_BACKOFF_MS);
|
|
183
|
+
this.maxRetryBackoffMs = Math.max(this.retryBackoffMs, (_d = options.maxRetryBackoffMs) !== null && _d !== void 0 ? _d : DEFAULT_MAX_RETRY_BACKOFF_MS);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.OfflineMeasurementQueue = OfflineMeasurementQueue;
|
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
import { IClientStorage, MedplumClient, MedplumClientOptions, SubscriptionManager } from "@medplum/core";
|
|
2
|
+
import { Bundle, Observation } from "@medplum/fhirtypes";
|
|
2
3
|
import * as aiFhirMethods from "./ai-fhir/methods";
|
|
3
4
|
import * as authMethods from "./auth/methods";
|
|
4
5
|
import * as botMethods from "./bot/methods";
|
|
5
6
|
import * as aiMethods from "./chat/ai/methods";
|
|
6
7
|
import * as observationMethods from "./observation/methods";
|
|
8
|
+
import { GenerateObservationBodyParams } from "./observation/types/generate-observation-body/GenerateObservationBodyParams";
|
|
9
|
+
import { OfflineMeasurementFlushOptions, OfflineMeasurementFlushResult, OfflineMeasurementQueueOptions } from "./offline";
|
|
7
10
|
import * as questionnaireResponseMethods from "./questionnaire-response/methods";
|
|
11
|
+
export type SaveObservationsResult = {
|
|
12
|
+
status: "saved";
|
|
13
|
+
response: Bundle;
|
|
14
|
+
} | {
|
|
15
|
+
status: "queued";
|
|
16
|
+
queueId: string;
|
|
17
|
+
};
|
|
8
18
|
export declare class OvokClient extends MedplumClient {
|
|
9
19
|
protected socialLoginClientId: string;
|
|
10
20
|
clientStorage: IClientStorage | undefined;
|
|
21
|
+
private readonly offlineQueueOptions;
|
|
22
|
+
private offlineMeasurementQueue;
|
|
11
23
|
/**
|
|
12
24
|
* @param config - The configuration for the client.
|
|
13
25
|
* @param config.socialLoginClientId - The social login client id to use for the client.
|
|
14
26
|
*/
|
|
15
27
|
constructor(config: MedplumClientOptions & {
|
|
16
28
|
socialLoginClientId?: string;
|
|
29
|
+
offlineQueue?: OfflineMeasurementQueueOptions;
|
|
17
30
|
});
|
|
31
|
+
/** Saves a measurement immediately, or durably queues it when the opt-in queue is enabled. */
|
|
32
|
+
saveMeasurement(params: GenerateObservationBodyParams): Promise<SaveObservationsResult>;
|
|
33
|
+
/** Saves observations with conditional creates so a retry cannot create a duplicate. */
|
|
34
|
+
saveObservations(observations: Observation[]): Promise<SaveObservationsResult>;
|
|
35
|
+
/** Flushes measurements queued while offline. Call this on connectivity/app-resume events. */
|
|
36
|
+
flushOfflineMeasurementQueue(options?: OfflineMeasurementFlushOptions): Promise<OfflineMeasurementFlushResult>;
|
|
37
|
+
private getOfflineMeasurementQueue;
|
|
38
|
+
private executeConditionalObservationCreates;
|
|
18
39
|
private bindMethods;
|
|
19
40
|
/**
|
|
20
41
|
* Gets the `SubscriptionManager` for WebSocket subscriptions.
|
|
@@ -41,6 +41,7 @@ const authMethods = __importStar(require("./auth/methods"));
|
|
|
41
41
|
const botMethods = __importStar(require("./bot/methods"));
|
|
42
42
|
const aiMethods = __importStar(require("./chat/ai/methods"));
|
|
43
43
|
const observationMethods = __importStar(require("./observation/methods"));
|
|
44
|
+
const offline_1 = require("./offline");
|
|
44
45
|
const questionnaireResponseMethods = __importStar(require("./questionnaire-response/methods"));
|
|
45
46
|
class OvokClient extends core_1.MedplumClient {
|
|
46
47
|
/**
|
|
@@ -48,7 +49,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
48
49
|
* @param config.socialLoginClientId - The social login client id to use for the client.
|
|
49
50
|
*/
|
|
50
51
|
constructor(config) {
|
|
51
|
-
var _a;
|
|
52
|
+
var _a, _b;
|
|
52
53
|
super(config);
|
|
53
54
|
this.socialLoginClientId = "";
|
|
54
55
|
this.socialLoginClientId = (_a = config.socialLoginClientId) !== null && _a !== void 0 ? _a : "";
|
|
@@ -59,6 +60,67 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
59
60
|
this.bindMethods(aiFhirMethods);
|
|
60
61
|
this.bindMethods(botMethods);
|
|
61
62
|
this.clientStorage = config.storage;
|
|
63
|
+
this.offlineQueueOptions = (_b = config.offlineQueue) !== null && _b !== void 0 ? _b : {};
|
|
64
|
+
}
|
|
65
|
+
/** Saves a measurement immediately, or durably queues it when the opt-in queue is enabled. */
|
|
66
|
+
async saveMeasurement(params) {
|
|
67
|
+
const observations = await this.generateObservationBodiesByMeasurement(params);
|
|
68
|
+
return this.saveObservations(observations);
|
|
69
|
+
}
|
|
70
|
+
/** Saves observations with conditional creates so a retry cannot create a duplicate. */
|
|
71
|
+
async saveObservations(observations) {
|
|
72
|
+
const queue = this.getOfflineMeasurementQueue();
|
|
73
|
+
if (queue) {
|
|
74
|
+
const { id, flush } = await queue.enqueueAndFlush(observations);
|
|
75
|
+
const response = flush.responses[id];
|
|
76
|
+
return response
|
|
77
|
+
? { status: "saved", response }
|
|
78
|
+
: { status: "queued", queueId: id };
|
|
79
|
+
}
|
|
80
|
+
const response = await this.executeConditionalObservationCreates(observations);
|
|
81
|
+
return { status: "saved", response };
|
|
82
|
+
}
|
|
83
|
+
/** Flushes measurements queued while offline. Call this on connectivity/app-resume events. */
|
|
84
|
+
async flushOfflineMeasurementQueue(options = {}) {
|
|
85
|
+
const queue = this.getOfflineMeasurementQueue();
|
|
86
|
+
if (!queue) {
|
|
87
|
+
return {
|
|
88
|
+
attempted: 0,
|
|
89
|
+
saved: 0,
|
|
90
|
+
queued: 0,
|
|
91
|
+
exhausted: 0,
|
|
92
|
+
responses: {},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return queue.flush(options);
|
|
96
|
+
}
|
|
97
|
+
getOfflineMeasurementQueue() {
|
|
98
|
+
var _a;
|
|
99
|
+
if (this.offlineQueueOptions.enabled !== true) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
if (!this.clientStorage) {
|
|
103
|
+
throw new Error("Offline measurement queue requires a durable client storage adapter.");
|
|
104
|
+
}
|
|
105
|
+
(_a = this.offlineMeasurementQueue) !== null && _a !== void 0 ? _a : (this.offlineMeasurementQueue = new offline_1.OfflineMeasurementQueue(this.clientStorage, (bundle) => this.executeBatch(bundle), this.offlineQueueOptions));
|
|
106
|
+
return this.offlineMeasurementQueue;
|
|
107
|
+
}
|
|
108
|
+
async executeConditionalObservationCreates(observations) {
|
|
109
|
+
var _a;
|
|
110
|
+
const entries = observations.map(offline_1.entryForObservation);
|
|
111
|
+
const bundle = await this.executeBatch({
|
|
112
|
+
resourceType: "Bundle",
|
|
113
|
+
type: "transaction",
|
|
114
|
+
entry: entries,
|
|
115
|
+
});
|
|
116
|
+
if ((_a = bundle.entry) === null || _a === void 0 ? void 0 : _a.some((entry) => {
|
|
117
|
+
var _a, _b;
|
|
118
|
+
const status = (_b = (_a = entry.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : "";
|
|
119
|
+
return status.startsWith("4") || status.startsWith("5");
|
|
120
|
+
})) {
|
|
121
|
+
throw bundle;
|
|
122
|
+
}
|
|
123
|
+
return bundle;
|
|
62
124
|
}
|
|
63
125
|
bindMethods(methods) {
|
|
64
126
|
for (const [name, method] of Object.entries(methods)) {
|
|
@@ -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.2.
|
|
51
|
+
version: "0.2.61",
|
|
52
52
|
},
|
|
53
53
|
fhirVersion: "4.0.1",
|
|
54
54
|
format: ["json"],
|
package/dist/index.d.ts
CHANGED
|
@@ -923,6 +923,8 @@ declare const Medplum: {
|
|
|
923
923
|
};
|
|
924
924
|
export { Medplum };
|
|
925
925
|
export { OvokClient } from "./client/ovok-client";
|
|
926
|
+
export type { SaveObservationsResult } from "./client/ovok-client";
|
|
927
|
+
export * from "./client/offline";
|
|
926
928
|
export * from "./hooks";
|
|
927
929
|
export * from "./utils";
|
|
928
930
|
export * from "./client/observation/services";
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ const { MedplumClient, ...Medplum } = MedplumCore;
|
|
|
44
44
|
exports.Medplum = Medplum;
|
|
45
45
|
var ovok_client_1 = require("./client/ovok-client");
|
|
46
46
|
Object.defineProperty(exports, "OvokClient", { enumerable: true, get: function () { return ovok_client_1.OvokClient; } });
|
|
47
|
+
__exportStar(require("./client/offline"), exports);
|
|
47
48
|
__exportStar(require("./hooks"), exports);
|
|
48
49
|
__exportStar(require("./utils"), exports);
|
|
49
50
|
__exportStar(require("./client/observation/services"), exports);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "../client/auth/types/RequestDeleteUserBody";
|
|
|
6
6
|
export * from "../client/auth/types/ResetPasswordBody";
|
|
7
7
|
export * from "../client/observation/types/MeasurementTypeKey";
|
|
8
8
|
export * from "../client/observation/types/ObservationCode";
|
|
9
|
+
export * from "../client/observation/types/UrineAnalytes";
|
|
9
10
|
export * from "../client/observation/types/measurement";
|
|
10
11
|
export * from "../client/observation/types/generate-measurement/Sample";
|
|
11
12
|
export * from "../client/observation/types/GetLatestObservationsByCodeParams";
|
package/dist/types/index.js
CHANGED
|
@@ -24,6 +24,7 @@ __exportStar(require("../client/auth/types/ResetPasswordBody"), exports);
|
|
|
24
24
|
// Measurement types
|
|
25
25
|
__exportStar(require("../client/observation/types/MeasurementTypeKey"), exports);
|
|
26
26
|
__exportStar(require("../client/observation/types/ObservationCode"), exports);
|
|
27
|
+
__exportStar(require("../client/observation/types/UrineAnalytes"), exports);
|
|
27
28
|
__exportStar(require("../client/observation/types/measurement"), exports);
|
|
28
29
|
__exportStar(require("../client/observation/types/generate-measurement/Sample"), exports);
|
|
29
30
|
// Observation types
|