@ovok/core 0.3.3 → 0.3.5
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/auth/methods/index.d.ts +1 -0
- package/dist/client/auth/methods/index.js +1 -0
- package/dist/client/auth/methods/setPasswordFromReset.d.ts +4 -0
- package/dist/client/auth/methods/setPasswordFromReset.js +6 -0
- package/dist/client/auth/types/CompletePasswordResetBody.d.ts +5 -0
- package/dist/client/auth/types/CompletePasswordResetBody.js +2 -0
- package/dist/client/observation/methods/generateObservationBodiesByMeasurement.js +9 -2
- package/dist/client/observation/observation-bundle-utils.d.ts +6 -0
- package/dist/client/observation/observation-bundle-utils.js +90 -0
- package/dist/client/observation/types/generate-observation-body/GenerateObservationBodyParams.d.ts +1 -0
- package/dist/client/observation/types/measurement/MeasurementDevice.d.ts +2 -0
- package/dist/client/observation/utils/getDeviceObject.js +10 -0
- package/dist/client/ovok-client.d.ts +10 -2
- package/dist/client/ovok-client.js +22 -14
- package/dist/conformance/api-requirements.d.ts +34 -0
- package/dist/conformance/api-requirements.js +38 -0
- package/dist/conformance/capability-requirements.js +2 -2
- package/dist/conformance/capability.d.ts +15 -0
- package/dist/conformance/capability.js +40 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Core TypeScript SDK for healthcare applications. Provides authentication, AI-FHI
|
|
|
14
14
|
- 🤖 Bot execution framework
|
|
15
15
|
- 📋 Questionnaire responses
|
|
16
16
|
- 📥 Opt-in durable offline measurement queue with idempotent retries
|
|
17
|
+
- 🧭 Runtime discovery of the backend FHIR and public API capabilities
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
@@ -40,6 +41,20 @@ const client = new OvokClient({
|
|
|
40
41
|
|
|
41
42
|
Full documentation and examples available at **[sdk.ovok.com](https://sdk.ovok.com/)**
|
|
42
43
|
|
|
44
|
+
## Backend capability discovery
|
|
45
|
+
|
|
46
|
+
`@ovok/core` can read the capability statement composed by `../ovok-core` and expose the
|
|
47
|
+
non-FHIR public routes published in Ovok's `ovok-public-api` extension:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const statement = await client.getCapabilityStatement();
|
|
51
|
+
const publicEndpoints = await client.getPublicApiEndpoints();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The exported `OVOK_CORE_REQUIREMENTS` describes the FHIR operations used by the SDK, while
|
|
55
|
+
`OVOK_CORE_API_REQUIREMENTS` describes its ordinary HTTP dependencies. Billing is intentionally
|
|
56
|
+
not part of this SDK contract yet.
|
|
57
|
+
|
|
43
58
|
## Offline measurements
|
|
44
59
|
|
|
45
60
|
Pass a durable Medplum storage adapter and opt in to queue measurements while offline:
|
|
@@ -103,3 +118,17 @@ if ("nextStep" in result && result.nextStep === "mfa") {
|
|
|
103
118
|
const authenticated = await result.verify(oneTimeCode);
|
|
104
119
|
}
|
|
105
120
|
```
|
|
121
|
+
|
|
122
|
+
## Large measurements
|
|
123
|
+
|
|
124
|
+
`saveMeasurement` and `saveObservations` use conditional creates for idempotent saves. Medplum
|
|
125
|
+
limits a serializable transaction with conditional operations to eight entries, so the SDK keeps
|
|
126
|
+
the first observation in a transaction and sends additional observations in a FHIR batch request.
|
|
127
|
+
This supports measurements such as urine strips and long ECG recordings without changing retry
|
|
128
|
+
behavior. The first observation should remain first when supplying observations manually.
|
|
129
|
+
|
|
130
|
+
The offline queue applies the same split when it flushes a measurement. A partial retry is safe:
|
|
131
|
+
the first conditional create finds the existing observation and the batch only creates missing
|
|
132
|
+
members. Applications building their own bundles can use the exported
|
|
133
|
+
`MAX_CONDITIONAL_OBSERVATION_ENTRIES` limit. See [FHIR batch requests](https://www.medplum.com/docs/fhir-datastore/fhir-batch-requests)
|
|
134
|
+
for the transaction and batch semantics.
|
|
@@ -6,6 +6,7 @@ export * from "./requestDeleteUser";
|
|
|
6
6
|
export * from "./googleLogin";
|
|
7
7
|
export * from "./appleLogin";
|
|
8
8
|
export * from "./resetPassword";
|
|
9
|
+
export * from "./setPasswordFromReset";
|
|
9
10
|
export * from "./logout";
|
|
10
11
|
export * from "./getSessions";
|
|
11
12
|
export * from "./revokeSessions";
|
|
@@ -22,6 +22,7 @@ __exportStar(require("./requestDeleteUser"), exports);
|
|
|
22
22
|
__exportStar(require("./googleLogin"), exports);
|
|
23
23
|
__exportStar(require("./appleLogin"), exports);
|
|
24
24
|
__exportStar(require("./resetPassword"), exports);
|
|
25
|
+
__exportStar(require("./setPasswordFromReset"), exports);
|
|
25
26
|
__exportStar(require("./logout"), exports);
|
|
26
27
|
__exportStar(require("./getSessions"), exports);
|
|
27
28
|
__exportStar(require("./revokeSessions"), exports);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { OperationOutcome } from "@medplum/fhirtypes";
|
|
2
|
+
import { OvokClient } from "../../ovok-client";
|
|
3
|
+
import { CompletePasswordResetBody } from "../types/CompletePasswordResetBody";
|
|
4
|
+
export declare function setPasswordFromReset(this: OvokClient, body: CompletePasswordResetBody): Promise<OperationOutcome>;
|
|
@@ -19,6 +19,12 @@ async function generateObservationBodiesByMeasurement(params) {
|
|
|
19
19
|
const observationService = creator_1.ObservationServiceCreator.getInstance().getService(measurementTypeKey);
|
|
20
20
|
const deviceObject = (0, getDeviceObject_1.getDeviceObject)(device);
|
|
21
21
|
const observationFragments = (_a = observationService.generateObservationFragments) === null || _a === void 0 ? void 0 : _a.call(observationService, measurement);
|
|
22
|
+
const applyEffectiveDateTime = (observation) => params.effectiveDateTime && !observation.effectivePeriod
|
|
23
|
+
? {
|
|
24
|
+
...observation,
|
|
25
|
+
effectiveDateTime: params.effectiveDateTime.toISOString(),
|
|
26
|
+
}
|
|
27
|
+
: observation;
|
|
22
28
|
if (observationFragments) {
|
|
23
29
|
return observationFragments
|
|
24
30
|
.map((fragment) => ({
|
|
@@ -32,7 +38,8 @@ async function generateObservationBodiesByMeasurement(params) {
|
|
|
32
38
|
code: fragment.code,
|
|
33
39
|
...(fragment.additionalProperties || {}),
|
|
34
40
|
}))
|
|
35
|
-
.filter(hasObservationValue)
|
|
41
|
+
.filter(hasObservationValue)
|
|
42
|
+
.map(applyEffectiveDateTime);
|
|
36
43
|
}
|
|
37
44
|
const observationCodes = observationService.getObservationCodes();
|
|
38
45
|
const values = observationService.generateValues(measurement);
|
|
@@ -48,7 +55,7 @@ async function generateObservationBodiesByMeasurement(params) {
|
|
|
48
55
|
code,
|
|
49
56
|
...(additionalProperties[index] || {}),
|
|
50
57
|
}));
|
|
51
|
-
return observationBodies.filter(hasObservationValue);
|
|
58
|
+
return observationBodies.filter(hasObservationValue).map(applyEffectiveDateTime);
|
|
52
59
|
}
|
|
53
60
|
const hasObservationValue = (observation) => {
|
|
54
61
|
var _a, _b, _c, _d, _e;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Bundle, BundleEntry, Observation } from "@medplum/fhirtypes";
|
|
2
|
+
export declare const MAX_OBSERVATION_BUNDLE_BYTES = 900000;
|
|
3
|
+
export declare const MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
4
|
+
export declare const linkObservationMembers: (observations: Observation[]) => Observation[];
|
|
5
|
+
export declare const splitObservationBundleEntries: (bundle: Bundle) => BundleEntry[][] | undefined;
|
|
6
|
+
export declare const executeObservationBundle: (bundle: Bundle, executeBatch: (bundle: Bundle) => Promise<Bundle>) => Promise<Bundle>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
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;
|
|
4
|
+
const offline_1 = require("../offline");
|
|
5
|
+
exports.MAX_OBSERVATION_BUNDLE_BYTES = 900000;
|
|
6
|
+
exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
7
|
+
const linkObservationMembers = (observations) => {
|
|
8
|
+
const main = observations[0];
|
|
9
|
+
if (!main) {
|
|
10
|
+
return observations;
|
|
11
|
+
}
|
|
12
|
+
const mainReference = `urn:uuid:${(0, offline_1.stableMeasurementId)(main)}`;
|
|
13
|
+
return observations.map((observation, index) => {
|
|
14
|
+
var _a;
|
|
15
|
+
return index === 0
|
|
16
|
+
? observation
|
|
17
|
+
: {
|
|
18
|
+
...observation,
|
|
19
|
+
hasMember: [
|
|
20
|
+
...((_a = observation.hasMember) !== null && _a !== void 0 ? _a : []),
|
|
21
|
+
{ reference: mainReference },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
exports.linkObservationMembers = linkObservationMembers;
|
|
27
|
+
const splitObservationBundleEntries = (bundle) => {
|
|
28
|
+
var _a;
|
|
29
|
+
const entries = (_a = bundle.entry) !== null && _a !== void 0 ? _a : [];
|
|
30
|
+
if (entries.length <= 1 || JSON.stringify(bundle).length <= exports.MAX_OBSERVATION_BUNDLE_BYTES) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const chunks = [];
|
|
34
|
+
let chunk = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const candidate = [...chunk, entry];
|
|
37
|
+
if (chunk.length > 0 &&
|
|
38
|
+
JSON.stringify({ ...bundle, entry: candidate }).length > exports.MAX_OBSERVATION_BUNDLE_BYTES) {
|
|
39
|
+
chunks.push(chunk);
|
|
40
|
+
chunk = [entry];
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
chunk = candidate;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (chunk.length > 0) {
|
|
47
|
+
chunks.push(chunk);
|
|
48
|
+
}
|
|
49
|
+
return chunks;
|
|
50
|
+
};
|
|
51
|
+
exports.splitObservationBundleEntries = splitObservationBundleEntries;
|
|
52
|
+
const executeObservationBundle = async (bundle, executeBatch) => {
|
|
53
|
+
var _a, _b, _c, _d;
|
|
54
|
+
const entries = (_a = bundle.entry) !== null && _a !== void 0 ? _a : [];
|
|
55
|
+
if (bundle.type === "transaction" &&
|
|
56
|
+
entries.length > exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES) {
|
|
57
|
+
const transactionResponse = await (0, exports.executeObservationBundle)({ ...bundle, entry: entries.slice(0, 1) }, executeBatch);
|
|
58
|
+
const batchResponse = await (0, exports.executeObservationBundle)({ ...bundle, type: "batch", entry: entries.slice(1) }, executeBatch);
|
|
59
|
+
return {
|
|
60
|
+
resourceType: "Bundle",
|
|
61
|
+
type: "batch-response",
|
|
62
|
+
entry: [
|
|
63
|
+
...((_b = transactionResponse.entry) !== null && _b !== void 0 ? _b : []),
|
|
64
|
+
...((_c = batchResponse.entry) !== null && _c !== void 0 ? _c : []),
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const chunks = (0, exports.splitObservationBundleEntries)(bundle);
|
|
69
|
+
if (chunks) {
|
|
70
|
+
const responses = [];
|
|
71
|
+
for (const entry of chunks) {
|
|
72
|
+
responses.push(await (0, exports.executeObservationBundle)({ ...bundle, entry }, executeBatch));
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
resourceType: "Bundle",
|
|
76
|
+
type: "batch-response",
|
|
77
|
+
entry: responses.flatMap((response) => { var _a; return (_a = response.entry) !== null && _a !== void 0 ? _a : []; }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const response = await executeBatch(bundle);
|
|
81
|
+
if ((_d = response.entry) === null || _d === void 0 ? void 0 : _d.some((entry) => {
|
|
82
|
+
var _a, _b;
|
|
83
|
+
const status = (_b = (_a = entry.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : "";
|
|
84
|
+
return status.startsWith("4") || status.startsWith("5");
|
|
85
|
+
})) {
|
|
86
|
+
throw response;
|
|
87
|
+
}
|
|
88
|
+
return response;
|
|
89
|
+
};
|
|
90
|
+
exports.executeObservationBundle = executeObservationBundle;
|
package/dist/client/observation/types/generate-observation-body/GenerateObservationBodyParams.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Measurement } from "../../../../types";
|
|
|
2
2
|
import { MeasurementDevice } from "../measurement/MeasurementDevice";
|
|
3
3
|
export type GenerateObservationBodyParams = {
|
|
4
4
|
device?: MeasurementDevice;
|
|
5
|
+
effectiveDateTime?: Date;
|
|
5
6
|
patientId?: string;
|
|
6
7
|
measurement: Measurement;
|
|
7
8
|
};
|
|
@@ -4,6 +4,8 @@ exports.getDeviceObject = void 0;
|
|
|
4
4
|
const EXTENSION_BASE_URL = "http://ovok.com/fhir/StructureDefinition";
|
|
5
5
|
const LOCAL_NAME_URL = `${EXTENSION_BASE_URL}/device-local-name`;
|
|
6
6
|
const MANUFACTURER_DATA_URL = `${EXTENSION_BASE_URL}/device-manufacturer-data`;
|
|
7
|
+
const MANUFACTURER_NAME_URL = `${EXTENSION_BASE_URL}/device-manufacturer-name`;
|
|
8
|
+
const MODEL_URL = `${EXTENSION_BASE_URL}/device-model`;
|
|
7
9
|
const SERIAL_NUMBER_URL = `${EXTENSION_BASE_URL}/device-serial-number`;
|
|
8
10
|
const BATTERY_PERCENTAGE_URL = `${EXTENSION_BASE_URL}/device-battery-percentage`;
|
|
9
11
|
/**
|
|
@@ -21,6 +23,12 @@ const getBatteryExtension = (batteryPercentage) => {
|
|
|
21
23
|
},
|
|
22
24
|
];
|
|
23
25
|
};
|
|
26
|
+
const getOptionalStringExtension = (url, value) => {
|
|
27
|
+
if (!value) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return [{ url, valueString: value }];
|
|
31
|
+
};
|
|
24
32
|
const getDeviceObject = (measurementDevice) => {
|
|
25
33
|
var _a, _b;
|
|
26
34
|
if (!measurementDevice) {
|
|
@@ -44,6 +52,8 @@ const getDeviceObject = (measurementDevice) => {
|
|
|
44
52
|
url: MANUFACTURER_DATA_URL,
|
|
45
53
|
valueString: measurementDevice.manufacturerData || "no-manufacturer-data",
|
|
46
54
|
},
|
|
55
|
+
...getOptionalStringExtension(MANUFACTURER_NAME_URL, measurementDevice.manufacturerName),
|
|
56
|
+
...getOptionalStringExtension(MODEL_URL, measurementDevice.model),
|
|
47
57
|
{
|
|
48
58
|
url: SERIAL_NUMBER_URL,
|
|
49
59
|
valueString: measurementDevice.sn || "no-serial-number",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IClientStorage, MedplumClient, MedplumClientOptions, SubscriptionManager } from "@medplum/core";
|
|
2
|
-
import { Bundle, Observation } from "@medplum/fhirtypes";
|
|
2
|
+
import { Bundle, CapabilityStatement, Observation } from "@medplum/fhirtypes";
|
|
3
|
+
import { OvokPublicApiEndpoint } from "../conformance/capability";
|
|
3
4
|
import * as aiFhirMethods from "./ai-fhir/methods";
|
|
4
5
|
import * as authMethods from "./auth/methods";
|
|
5
6
|
import * as botMethods from "./bot/methods";
|
|
@@ -8,6 +9,8 @@ import * as observationMethods from "./observation/methods";
|
|
|
8
9
|
import { GenerateObservationBodyParams } from "./observation/types/generate-observation-body/GenerateObservationBodyParams";
|
|
9
10
|
import { OfflineMeasurementFlushOptions, OfflineMeasurementFlushResult, OfflineMeasurementQueueOptions } from "./offline";
|
|
10
11
|
import * as questionnaireResponseMethods from "./questionnaire-response/methods";
|
|
12
|
+
export declare const MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
13
|
+
export { MAX_OBSERVATION_BUNDLE_BYTES } from "./observation/observation-bundle-utils";
|
|
11
14
|
export type SaveObservationsResult = {
|
|
12
15
|
status: "saved";
|
|
13
16
|
response: Bundle;
|
|
@@ -15,6 +18,7 @@ export type SaveObservationsResult = {
|
|
|
15
18
|
status: "queued";
|
|
16
19
|
queueId: string;
|
|
17
20
|
};
|
|
21
|
+
export type OvokFhirVersion = "R4" | "R5";
|
|
18
22
|
export declare class OvokClient extends MedplumClient {
|
|
19
23
|
protected socialLoginClientId: string;
|
|
20
24
|
clientStorage: IClientStorage | undefined;
|
|
@@ -30,12 +34,17 @@ export declare class OvokClient extends MedplumClient {
|
|
|
30
34
|
});
|
|
31
35
|
/** Saves a measurement immediately, or durably queues it when the opt-in queue is enabled. */
|
|
32
36
|
saveMeasurement(params: GenerateObservationBodyParams): Promise<SaveObservationsResult>;
|
|
37
|
+
/** Reads Ovok's composed FHIR and public-route capability statement. */
|
|
38
|
+
getCapabilityStatement(version?: OvokFhirVersion): Promise<CapabilityStatement>;
|
|
39
|
+
/** Returns the non-FHIR public routes published by the backend statement. */
|
|
40
|
+
getPublicApiEndpoints(version?: OvokFhirVersion): Promise<OvokPublicApiEndpoint[]>;
|
|
33
41
|
/** Saves observations with conditional creates so a retry cannot create a duplicate. */
|
|
34
42
|
saveObservations(observations: Observation[]): Promise<SaveObservationsResult>;
|
|
35
43
|
/** Flushes measurements queued while offline. Call this on connectivity/app-resume events. */
|
|
36
44
|
flushOfflineMeasurementQueue(options?: OfflineMeasurementFlushOptions): Promise<OfflineMeasurementFlushResult>;
|
|
37
45
|
private getOfflineMeasurementQueue;
|
|
38
46
|
private executeConditionalObservationCreates;
|
|
47
|
+
private executeObservationBundle;
|
|
39
48
|
private bindMethods;
|
|
40
49
|
/**
|
|
41
50
|
* Gets the `SubscriptionManager` for WebSocket subscriptions.
|
|
@@ -53,4 +62,3 @@ declare module "./ovok-client" {
|
|
|
53
62
|
clientStorage: IClientStorage | undefined;
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
|
-
export {};
|
|
@@ -33,17 +33,22 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.OvokClient = void 0;
|
|
36
|
+
exports.OvokClient = exports.MAX_OBSERVATION_BUNDLE_BYTES = exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = void 0;
|
|
37
37
|
/* eslint-disable @typescript-eslint/no-unsafe-function-type */ // TODO: fix this whenever find time
|
|
38
38
|
const core_1 = require("@medplum/core");
|
|
39
|
+
const capability_1 = require("../conformance/capability");
|
|
39
40
|
const aiFhirMethods = __importStar(require("./ai-fhir/methods"));
|
|
40
41
|
const authMethods = __importStar(require("./auth/methods"));
|
|
41
42
|
const botMethods = __importStar(require("./bot/methods"));
|
|
42
43
|
const aiMethods = __importStar(require("./chat/ai/methods"));
|
|
43
44
|
const rate_limit_error_1 = require("./errors/rate-limit-error");
|
|
44
45
|
const observationMethods = __importStar(require("./observation/methods"));
|
|
46
|
+
const observation_bundle_utils_1 = require("./observation/observation-bundle-utils");
|
|
45
47
|
const offline_1 = require("./offline");
|
|
46
48
|
const questionnaireResponseMethods = __importStar(require("./questionnaire-response/methods"));
|
|
49
|
+
exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = 8;
|
|
50
|
+
var observation_bundle_utils_2 = require("./observation/observation-bundle-utils");
|
|
51
|
+
Object.defineProperty(exports, "MAX_OBSERVATION_BUNDLE_BYTES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_OBSERVATION_BUNDLE_BYTES; } });
|
|
47
52
|
class OvokClient extends core_1.MedplumClient {
|
|
48
53
|
/**
|
|
49
54
|
* @param config - The configuration for the client.
|
|
@@ -68,17 +73,26 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
68
73
|
const observations = await this.generateObservationBodiesByMeasurement(params);
|
|
69
74
|
return this.saveObservations(observations);
|
|
70
75
|
}
|
|
76
|
+
/** Reads Ovok's composed FHIR and public-route capability statement. */
|
|
77
|
+
async getCapabilityStatement(version = "R4") {
|
|
78
|
+
return this.get(`/fhir/${version}/metadata`);
|
|
79
|
+
}
|
|
80
|
+
/** Returns the non-FHIR public routes published by the backend statement. */
|
|
81
|
+
async getPublicApiEndpoints(version = "R4") {
|
|
82
|
+
return (0, capability_1.getOvokPublicApiEndpoints)(await this.getCapabilityStatement(version));
|
|
83
|
+
}
|
|
71
84
|
/** Saves observations with conditional creates so a retry cannot create a duplicate. */
|
|
72
85
|
async saveObservations(observations) {
|
|
86
|
+
const linkedObservations = (0, observation_bundle_utils_1.linkObservationMembers)(observations);
|
|
73
87
|
const queue = this.getOfflineMeasurementQueue();
|
|
74
88
|
if (queue) {
|
|
75
|
-
const { id, flush } = await queue.enqueueAndFlush(
|
|
89
|
+
const { id, flush } = await queue.enqueueAndFlush(linkedObservations);
|
|
76
90
|
const response = flush.responses[id];
|
|
77
91
|
return response
|
|
78
92
|
? { status: "saved", response }
|
|
79
93
|
: { status: "queued", queueId: id };
|
|
80
94
|
}
|
|
81
|
-
const response = await this.executeConditionalObservationCreates(
|
|
95
|
+
const response = await this.executeConditionalObservationCreates(linkedObservations);
|
|
82
96
|
return { status: "saved", response };
|
|
83
97
|
}
|
|
84
98
|
/** Flushes measurements queued while offline. Call this on connectivity/app-resume events. */
|
|
@@ -103,25 +117,19 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
103
117
|
if (!this.clientStorage) {
|
|
104
118
|
throw new Error("Offline measurement queue requires a durable client storage adapter.");
|
|
105
119
|
}
|
|
106
|
-
(_a = this.offlineMeasurementQueue) !== null && _a !== void 0 ? _a : (this.offlineMeasurementQueue = new offline_1.OfflineMeasurementQueue(this.clientStorage, (bundle) => this.
|
|
120
|
+
(_a = this.offlineMeasurementQueue) !== null && _a !== void 0 ? _a : (this.offlineMeasurementQueue = new offline_1.OfflineMeasurementQueue(this.clientStorage, (bundle) => this.executeObservationBundle(bundle), this.offlineQueueOptions));
|
|
107
121
|
return this.offlineMeasurementQueue;
|
|
108
122
|
}
|
|
109
123
|
async executeConditionalObservationCreates(observations) {
|
|
110
|
-
var _a;
|
|
111
124
|
const entries = observations.map(offline_1.entryForObservation);
|
|
112
|
-
|
|
125
|
+
return this.executeObservationBundle({
|
|
113
126
|
resourceType: "Bundle",
|
|
114
127
|
type: "transaction",
|
|
115
128
|
entry: entries,
|
|
116
129
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return status.startsWith("4") || status.startsWith("5");
|
|
121
|
-
})) {
|
|
122
|
-
throw bundle;
|
|
123
|
-
}
|
|
124
|
-
return bundle;
|
|
130
|
+
}
|
|
131
|
+
async executeObservationBundle(bundle) {
|
|
132
|
+
return (0, observation_bundle_utils_1.executeObservationBundle)(bundle, (request) => this.executeBatch(request));
|
|
125
133
|
}
|
|
126
134
|
bindMethods(methods) {
|
|
127
135
|
for (const [name, method] of Object.entries(methods)) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type OvokHttpMethod = "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
|
|
2
|
+
export type OvokApiRequirement = {
|
|
3
|
+
method: OvokHttpMethod;
|
|
4
|
+
path: string;
|
|
5
|
+
group: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The non-FHIR HTTP routes used by @ovok/core itself.
|
|
9
|
+
*
|
|
10
|
+
* This is deliberately separate from OVOK_CORE_REQUIREMENTS: a FHIR
|
|
11
|
+
* CapabilityStatement cannot describe ordinary auth, AI, or bot routes.
|
|
12
|
+
* Billing is intentionally absent until Ovok has a supported billing SDK
|
|
13
|
+
* contract.
|
|
14
|
+
*/
|
|
15
|
+
export declare const OVOK_CORE_API_REQUIREMENTS: {
|
|
16
|
+
readonly capability: {
|
|
17
|
+
readonly method: "GET";
|
|
18
|
+
readonly path: "/fhir/{version}/metadata";
|
|
19
|
+
readonly group: "FHIR";
|
|
20
|
+
};
|
|
21
|
+
readonly nonFhir: ({
|
|
22
|
+
method: "POST";
|
|
23
|
+
path: string;
|
|
24
|
+
group: string;
|
|
25
|
+
} | {
|
|
26
|
+
method: "GET";
|
|
27
|
+
path: string;
|
|
28
|
+
group: string;
|
|
29
|
+
} | {
|
|
30
|
+
method: "DELETE";
|
|
31
|
+
path: string;
|
|
32
|
+
group: string;
|
|
33
|
+
})[];
|
|
34
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OVOK_CORE_API_REQUIREMENTS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The non-FHIR HTTP routes used by @ovok/core itself.
|
|
6
|
+
*
|
|
7
|
+
* This is deliberately separate from OVOK_CORE_REQUIREMENTS: a FHIR
|
|
8
|
+
* CapabilityStatement cannot describe ordinary auth, AI, or bot routes.
|
|
9
|
+
* Billing is intentionally absent until Ovok has a supported billing SDK
|
|
10
|
+
* contract.
|
|
11
|
+
*/
|
|
12
|
+
exports.OVOK_CORE_API_REQUIREMENTS = {
|
|
13
|
+
capability: {
|
|
14
|
+
method: "GET",
|
|
15
|
+
path: "/fhir/{version}/metadata",
|
|
16
|
+
group: "FHIR",
|
|
17
|
+
},
|
|
18
|
+
nonFhir: [
|
|
19
|
+
{ method: "POST", path: "/auth/tenant/Patient/login/start", group: "Auth" },
|
|
20
|
+
{ method: "POST", path: "/auth/tenant/Practitioner/login/start", group: "Auth" },
|
|
21
|
+
{ method: "POST", path: "/auth/tenant/Patient/login/token", group: "Auth" },
|
|
22
|
+
{ method: "POST", path: "/auth/tenant/Practitioner/login/token", group: "Auth" },
|
|
23
|
+
{ method: "POST", path: "/auth/tenant/Practitioner/login/mfa", group: "Auth" },
|
|
24
|
+
{ method: "POST", path: "/auth/tenant/Patient/register", group: "Auth" },
|
|
25
|
+
{ method: "POST", path: "/auth/external/google", group: "Auth" },
|
|
26
|
+
{ method: "POST", path: "/auth/external/apple", group: "Auth" },
|
|
27
|
+
{ method: "POST", path: "/auth/change-password", group: "Auth" },
|
|
28
|
+
{ method: "POST", path: "/v2/auth/reset-password", group: "Auth" },
|
|
29
|
+
{ method: "POST", path: "/v2/auth/reset-password/process", group: "Auth" },
|
|
30
|
+
{ method: "GET", path: "/auth/session", group: "Auth" },
|
|
31
|
+
{ method: "DELETE", path: "/auth/session/current", group: "Auth" },
|
|
32
|
+
{ method: "DELETE", path: "/auth/session/:option", group: "Auth" },
|
|
33
|
+
{ method: "DELETE", path: "/auth/delete", group: "Auth" },
|
|
34
|
+
{ method: "POST", path: "/ai/session", group: "AI" },
|
|
35
|
+
{ method: "POST", path: "/ai/fhir/search", group: "AI" },
|
|
36
|
+
{ method: "POST", path: "/bots", group: "Bots" },
|
|
37
|
+
],
|
|
38
|
+
};
|
|
@@ -43,12 +43,12 @@ exports.OVOK_CORE_REQUIREMENTS = {
|
|
|
43
43
|
* report change where there is none. Bump it with `software.version` when
|
|
44
44
|
* the declared surface actually moves.
|
|
45
45
|
*/
|
|
46
|
-
date: "2026-09-
|
|
46
|
+
date: "2026-09-26",
|
|
47
47
|
kind: "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.3.
|
|
51
|
+
version: "0.3.4",
|
|
52
52
|
},
|
|
53
53
|
fhirVersion: "4.0.1",
|
|
54
54
|
format: ["json"],
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CapabilityStatement } from "@medplum/fhirtypes";
|
|
2
|
+
/** The extension used by ovok-core to publish non-FHIR public routes. */
|
|
3
|
+
export declare const OVOK_PUBLIC_API_EXTENSION = "https://fhir.ovok.com/fhir/StructureDefinition/ovok-public-api";
|
|
4
|
+
export type OvokPublicApiEndpoint = {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
group: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Reads the public non-FHIR route catalog emitted by ../ovok-core.
|
|
11
|
+
*
|
|
12
|
+
* FHIR clients can ignore this extension safely. React applications can use
|
|
13
|
+
* it to feature-detect the rest of the Ovok API without hard-coding routes.
|
|
14
|
+
*/
|
|
15
|
+
export declare const getOvokPublicApiEndpoints: (statement: CapabilityStatement) => OvokPublicApiEndpoint[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getOvokPublicApiEndpoints = exports.OVOK_PUBLIC_API_EXTENSION = void 0;
|
|
4
|
+
/** The extension used by ovok-core to publish non-FHIR public routes. */
|
|
5
|
+
exports.OVOK_PUBLIC_API_EXTENSION = "https://fhir.ovok.com/fhir/StructureDefinition/ovok-public-api";
|
|
6
|
+
const ENDPOINT_EXTENSION = "endpoint";
|
|
7
|
+
const stringValue = (extension) => { var _a; return (_a = extension.valueCode) !== null && _a !== void 0 ? _a : extension.valueString; };
|
|
8
|
+
/**
|
|
9
|
+
* Reads the public non-FHIR route catalog emitted by ../ovok-core.
|
|
10
|
+
*
|
|
11
|
+
* FHIR clients can ignore this extension safely. React applications can use
|
|
12
|
+
* it to feature-detect the rest of the Ovok API without hard-coding routes.
|
|
13
|
+
*/
|
|
14
|
+
const getOvokPublicApiEndpoints = (statement) => {
|
|
15
|
+
var _a, _b;
|
|
16
|
+
const publicApi = (_a = statement.extension) === null || _a === void 0 ? void 0 : _a.find((extension) => extension.url === exports.OVOK_PUBLIC_API_EXTENSION);
|
|
17
|
+
return ((_b = publicApi === null || publicApi === void 0 ? void 0 : publicApi.extension) !== null && _b !== void 0 ? _b : []).flatMap((endpoint) => {
|
|
18
|
+
var _a, _b, _c;
|
|
19
|
+
if (endpoint.url !== ENDPOINT_EXTENSION) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const method = (_a = endpoint.extension) === null || _a === void 0 ? void 0 : _a.find((extension) => extension.url === "method");
|
|
23
|
+
const path = (_b = endpoint.extension) === null || _b === void 0 ? void 0 : _b.find((extension) => extension.url === "path");
|
|
24
|
+
const group = (_c = endpoint.extension) === null || _c === void 0 ? void 0 : _c.find((extension) => extension.url === "group");
|
|
25
|
+
const methodValue = method === undefined ? undefined : stringValue(method);
|
|
26
|
+
const pathValue = path === undefined ? undefined : stringValue(path);
|
|
27
|
+
const groupValue = group === undefined ? undefined : stringValue(group);
|
|
28
|
+
if (methodValue === undefined || pathValue === undefined || groupValue === undefined) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
method: methodValue.toUpperCase(),
|
|
34
|
+
path: pathValue,
|
|
35
|
+
group: groupValue,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
exports.getOvokPublicApiEndpoints = getOvokPublicApiEndpoints;
|
package/dist/index.d.ts
CHANGED
|
@@ -924,6 +924,7 @@ declare const Medplum: {
|
|
|
924
924
|
export { Medplum };
|
|
925
925
|
export { OvokClient } from "./client/ovok-client";
|
|
926
926
|
export * from "./client/errors/rate-limit-error";
|
|
927
|
+
export { MAX_CONDITIONAL_OBSERVATION_ENTRIES, MAX_OBSERVATION_BUNDLE_BYTES, } from "./client/ovok-client";
|
|
927
928
|
export type { SaveObservationsResult } from "./client/ovok-client";
|
|
928
929
|
export * from "./client/offline";
|
|
929
930
|
export * from "./hooks";
|
|
@@ -932,3 +933,5 @@ export * from "./client/observation/services";
|
|
|
932
933
|
export * from "./client/observation/methods";
|
|
933
934
|
export { saveMeasurement } from "./client/observation/methods/saveMeasurement";
|
|
934
935
|
export * from "./conformance/capability-requirements";
|
|
936
|
+
export * from "./conformance/api-requirements";
|
|
937
|
+
export * from "./conformance/capability";
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
36
36
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.saveMeasurement = exports.OvokClient = exports.Medplum = void 0;
|
|
39
|
+
exports.saveMeasurement = exports.MAX_OBSERVATION_BUNDLE_BYTES = exports.MAX_CONDITIONAL_OBSERVATION_ENTRIES = exports.OvokClient = exports.Medplum = void 0;
|
|
40
40
|
const MedplumCore = __importStar(require("@medplum/core"));
|
|
41
41
|
__exportStar(require("./types"), exports);
|
|
42
42
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -45,6 +45,9 @@ 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
47
|
__exportStar(require("./client/errors/rate-limit-error"), exports);
|
|
48
|
+
var ovok_client_2 = require("./client/ovok-client");
|
|
49
|
+
Object.defineProperty(exports, "MAX_CONDITIONAL_OBSERVATION_ENTRIES", { enumerable: true, get: function () { return ovok_client_2.MAX_CONDITIONAL_OBSERVATION_ENTRIES; } });
|
|
50
|
+
Object.defineProperty(exports, "MAX_OBSERVATION_BUNDLE_BYTES", { enumerable: true, get: function () { return ovok_client_2.MAX_OBSERVATION_BUNDLE_BYTES; } });
|
|
48
51
|
__exportStar(require("./client/offline"), exports);
|
|
49
52
|
__exportStar(require("./hooks"), exports);
|
|
50
53
|
__exportStar(require("./utils"), exports);
|
|
@@ -53,3 +56,5 @@ __exportStar(require("./client/observation/methods"), exports);
|
|
|
53
56
|
var saveMeasurement_1 = require("./client/observation/methods/saveMeasurement");
|
|
54
57
|
Object.defineProperty(exports, "saveMeasurement", { enumerable: true, get: function () { return saveMeasurement_1.saveMeasurement; } });
|
|
55
58
|
__exportStar(require("./conformance/capability-requirements"), exports);
|
|
59
|
+
__exportStar(require("./conformance/api-requirements"), exports);
|
|
60
|
+
__exportStar(require("./conformance/capability"), exports);
|