@openedc/sdk 3.8.2-next.0 → 3.8.2-next.2
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 +25 -14
- package/openedc-sdk.d.ts +12 -5
- package/openedc-sdk.js +18 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,15 +7,20 @@
|
|
|
7
7
|
</div>
|
|
8
8
|
<br>
|
|
9
9
|
|
|
10
|
-
The official TypeScript SDK for the OpenEDC Health Platform
|
|
10
|
+
The official TypeScript SDK for the OpenEDC Health Platform.
|
|
11
11
|
|
|
12
12
|
Fully typed end-to-end, it runs on any modern JavaScript runtime (Node, Deno, Bun) and gives you a clean, expressive API to work with all aspects of a clinical study:
|
|
13
13
|
|
|
14
|
-
- **Query
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
14
|
+
- **Data access:** Query study metadata, clinical data, administrative data, and more, with flexible filtering, sorting, and cross-project support
|
|
15
|
+
- **Data editing:** Create or modify study data reliably with a single commit call, with all changes automatically recorded in the audit trail
|
|
16
|
+
- **Audit trail:** Query the complete history of all study data changes, supporting data integrity and GCP compliance
|
|
17
|
+
- **Live updates:** Subscribe to real-time changes via live queries that automatically reflect updates as they happen
|
|
18
|
+
- **File management:** Upload and download files securely with resumable transfers, supporting files up to 5 GB
|
|
19
|
+
|
|
20
|
+
The OpenEDC Health platform and the SDK are built around PostgreSQL Row Level Security (RLS) Policies and Triggers. This ensures that:
|
|
21
|
+
|
|
22
|
+
- **Scoped access:** Only the projects and data the authenticated user has been granted access to can be read or modified through the SDK
|
|
23
|
+
- **Automated records:** All data changes are automatically recorded in the audit trail, attributed to the user with a server-side timestamp
|
|
19
24
|
|
|
20
25
|
## Getting started
|
|
21
26
|
|
|
@@ -34,6 +39,12 @@ deno add npm:@openedc/sdk
|
|
|
34
39
|
|
|
35
40
|
### Initialization
|
|
36
41
|
|
|
42
|
+
Before you can authenticate within the SDK, please create an API key in the OpenEDC Health application. Click on your name in the bottom-left corner, select *Account*, and then create an API key at the bottom of the dialog.
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
The API key can then be used as shown below.
|
|
47
|
+
|
|
37
48
|
```ts
|
|
38
49
|
import { login, logout } from "@openedc/sdk";
|
|
39
50
|
|
|
@@ -69,8 +80,8 @@ const location = await Location
|
|
|
69
80
|
const subjects = await SubjectData
|
|
70
81
|
.where({ location })
|
|
71
82
|
.project(project)
|
|
72
|
-
.sort("createdDate")
|
|
73
|
-
.
|
|
83
|
+
.sort("createdDate").asc()
|
|
84
|
+
.all();
|
|
74
85
|
|
|
75
86
|
// Create a new subject within a location
|
|
76
87
|
const subject = await new SubjectData("Patient-001", location.reference)
|
|
@@ -86,12 +97,12 @@ await subject.set({ subjectKey: "Patient-003" }).commit();
|
|
|
86
97
|
|
|
87
98
|
#### Global project scope
|
|
88
99
|
|
|
100
|
+
Instead of specifying the project per call, a global scope can be set for all upcoming statements. This works well for linear, sequential flows but should be avoided in parallel or callback-based code that handles multiple projects concurrently. Assumed for the rest of the examples.
|
|
101
|
+
|
|
89
102
|
```ts
|
|
90
|
-
import { Location, SubjectData } from "@openedc/sdk";
|
|
103
|
+
import { scope, Location, SubjectData } from "@openedc/sdk";
|
|
91
104
|
|
|
92
|
-
//
|
|
93
|
-
// all upcoming statements. This can be changed at any time but shouldn't be used when handling
|
|
94
|
-
// multiple projects at the same time. Will be assumed to be set for the rest of the examples.
|
|
105
|
+
// Scope subsequent queries and commits
|
|
95
106
|
scope(project);
|
|
96
107
|
|
|
97
108
|
const sameLocation = await Location
|
|
@@ -100,8 +111,8 @@ const sameLocation = await Location
|
|
|
100
111
|
|
|
101
112
|
const sameSubjects = await SubjectData
|
|
102
113
|
.where({ location })
|
|
103
|
-
.sort("createdDate")
|
|
104
|
-
.
|
|
114
|
+
.sort("createdDate").asc()
|
|
115
|
+
.all();
|
|
105
116
|
|
|
106
117
|
// Create a new subject (note that Patient-003 would result in a unique constraint violation)
|
|
107
118
|
const newSubject = await new SubjectData("Patient-004").commit();
|
package/openedc-sdk.d.ts
CHANGED
|
@@ -196,6 +196,13 @@ export type FieldDefinition = {
|
|
|
196
196
|
relation?: typeof DataBlock | typeof DataBlock[];
|
|
197
197
|
delete?: "unset" | "cascade";
|
|
198
198
|
};
|
|
199
|
+
export type FieldsOf<T> = {
|
|
200
|
+
[K in keyof T as T[K] extends Function ? never : K]: T[K];
|
|
201
|
+
};
|
|
202
|
+
export type WhereValue<T> = T extends DataBlock ? T | BlockReference<T> : T extends BlockReference<infer U> ? T | U : T extends (infer E)[] ? WhereValue<E>[] : T;
|
|
203
|
+
export type WhereFilter<T> = {
|
|
204
|
+
[K in keyof FieldsOf<T>]?: WhereValue<T[K]>;
|
|
205
|
+
};
|
|
199
206
|
declare class DataBlock {
|
|
200
207
|
static adapter: PersistenceAdapter;
|
|
201
208
|
static fields: Record<string, FieldDefinition>;
|
|
@@ -215,8 +222,8 @@ declare class DataBlock {
|
|
|
215
222
|
get static(): typeof DataBlock;
|
|
216
223
|
protected get children(): DataBlock[];
|
|
217
224
|
static where<T extends typeof DataBlock>(this: T): Collection<T>;
|
|
218
|
-
static where<T extends typeof DataBlock>(this: T, value: string): Where<T>;
|
|
219
|
-
static where<T extends typeof DataBlock>(this: T, value:
|
|
225
|
+
static where<T extends typeof DataBlock>(this: T, value: keyof WhereFilter<InstanceType<T>> & string): Where<T>;
|
|
226
|
+
static where<T extends typeof DataBlock>(this: T, value: WhereFilter<InstanceType<T>>): Collection<T>;
|
|
220
227
|
commit(options?: {
|
|
221
228
|
cascade?: boolean;
|
|
222
229
|
project?: Project | string;
|
|
@@ -243,7 +250,7 @@ declare class DataBlock {
|
|
|
243
250
|
}, visited?: Record<string, DataBlock>): this;
|
|
244
251
|
find<T extends typeof DataBlock>(block: T, key?: string, value?: unknown, all?: boolean, visited?: Set<string>): InstanceType<T> | undefined;
|
|
245
252
|
find<T extends typeof DataBlock>(block: T, key: string | undefined, value: unknown | undefined, all: true, visited?: Set<string>): InstanceType<T>[] | undefined;
|
|
246
|
-
set(values: Partial<this
|
|
253
|
+
set(values: Partial<FieldsOf<this>>): this & Partial<FieldsOf<this>>;
|
|
247
254
|
get uncommitted(): boolean;
|
|
248
255
|
get reference(): BlockReference<this>;
|
|
249
256
|
private static withCache;
|
|
@@ -801,9 +808,9 @@ export declare class CalendarEvent extends DataBlock {
|
|
|
801
808
|
studyEventInstance?: number;
|
|
802
809
|
name?: string;
|
|
803
810
|
description?: string;
|
|
804
|
-
|
|
811
|
+
startDateUTC: string;
|
|
805
812
|
private startTimeUTC?;
|
|
806
|
-
|
|
813
|
+
endDateUTC: string;
|
|
807
814
|
private endTimeUTC?;
|
|
808
815
|
shared?: boolean;
|
|
809
816
|
constructor(owner: BlockReference<User>, startDate: string);
|
package/openedc-sdk.js
CHANGED
|
@@ -9833,15 +9833,17 @@ const Gs = (Ve = class {
|
|
|
9833
9833
|
static async rebuildBlock(e) {
|
|
9834
9834
|
const r = Object.assign(new this(), e);
|
|
9835
9835
|
for (const [n, i] of Object.entries(this.fields)) {
|
|
9836
|
-
if (!i.relation) continue;
|
|
9837
9836
|
const s = Reflect.get(r, n);
|
|
9838
|
-
if (
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9837
|
+
if (i.type === Date && s)
|
|
9838
|
+
Reflect.set(r, n, new Date(s));
|
|
9839
|
+
else if (i.relation)
|
|
9840
|
+
if (Array.isArray(s)) {
|
|
9841
|
+
const o = s.map((u) => ke.fromString(u)).filter((u) => !!u), a = i.lazy ? o : await Promise.all(o.map((u) => u.data));
|
|
9842
|
+
Reflect.set(r, n, a.filter(Boolean));
|
|
9843
|
+
} else {
|
|
9844
|
+
const o = ke.fromString(s), a = i.lazy ? o : await (o == null ? void 0 : o.data);
|
|
9845
|
+
Reflect.set(r, n, a);
|
|
9846
|
+
}
|
|
9845
9847
|
}
|
|
9846
9848
|
return r.internal.lastState = structuredClone(e), r;
|
|
9847
9849
|
}
|
|
@@ -10636,11 +10638,13 @@ const kt = class kt {
|
|
|
10636
10638
|
const [o, ...a] = s.split("."), u = a.at(0) === "ref" ? this.getLocalization(o, { locale: n, interpolation: r }) : r == null ? void 0 : r[o];
|
|
10637
10639
|
return String(a.reduce(
|
|
10638
10640
|
(c, h) => this.transform(c, h, n),
|
|
10639
|
-
u
|
|
10641
|
+
u
|
|
10640
10642
|
));
|
|
10641
10643
|
});
|
|
10642
10644
|
}
|
|
10643
10645
|
static transform(e, r, n) {
|
|
10646
|
+
if (e == null || e === "")
|
|
10647
|
+
return "—";
|
|
10644
10648
|
switch (r) {
|
|
10645
10649
|
case "upper":
|
|
10646
10650
|
return String(e).toUpperCase();
|
|
@@ -10663,7 +10667,7 @@ const kt = class kt {
|
|
|
10663
10667
|
}
|
|
10664
10668
|
}
|
|
10665
10669
|
};
|
|
10666
|
-
kt.SELECTED_LANGUAGE = "SELECTED_LANGUAGE", kt.
|
|
10670
|
+
kt.SELECTED_LANGUAGE = "SELECTED_LANGUAGE", kt.defaults = {}, kt.overwrites = {}, kt.defaultLocale = new Intl.Locale("en"), kt.currentLocale = kt.defaultLocale, kt.translationGlob = {}, kt.localizedElements = /* @__PURE__ */ new Map();
|
|
10667
10671
|
let ye = kt;
|
|
10668
10672
|
var _p = Object.defineProperty, Ep = Object.getOwnPropertyDescriptor, Ur = (t, e, r, n) => {
|
|
10669
10673
|
for (var i = n > 1 ? void 0 : n ? Ep(e, r) : e, s = t.length - 1, o; s >= 0; s--)
|
|
@@ -20766,7 +20770,7 @@ Ze = Et([
|
|
|
20766
20770
|
let Yn;
|
|
20767
20771
|
const xD = async ({ serverUrl: t, apiKey: e }) => {
|
|
20768
20772
|
var d;
|
|
20769
|
-
const r = await fetch(
|
|
20773
|
+
const r = await fetch(new URL("/api/status", t));
|
|
20770
20774
|
if (!r.ok) throw new Error("Server not available");
|
|
20771
20775
|
const { supabaseUrl: n, supabaseKey: i } = await r.json();
|
|
20772
20776
|
Yn = ip(n, i, {
|
|
@@ -20777,7 +20781,7 @@ const xD = async ({ serverUrl: t, apiKey: e }) => {
|
|
|
20777
20781
|
}
|
|
20778
20782
|
}
|
|
20779
20783
|
});
|
|
20780
|
-
const s = await fetch(
|
|
20784
|
+
const s = await fetch(new URL("/api/auth/token", t), {
|
|
20781
20785
|
method: "POST",
|
|
20782
20786
|
headers: {
|
|
20783
20787
|
Authorization: `Bearer ${e}`
|
|
@@ -20786,9 +20790,10 @@ const xD = async ({ serverUrl: t, apiKey: e }) => {
|
|
|
20786
20790
|
if (!s.ok) throw new Error("Invalid API key");
|
|
20787
20791
|
const { email: o, token: a, type: u } = await s.json(), { data: c, error: h } = await Yn.auth.verifyOtp({ email: o, token: a, type: u });
|
|
20788
20792
|
if (!c.user) throw h ?? new Error("Error during OTP verification");
|
|
20793
|
+
Ps.supabaseClient = Yn, V.adapter = Ps;
|
|
20789
20794
|
const f = await ((d = ke.fromString(c.user.app_metadata.user)) == null ? void 0 : d.data);
|
|
20790
20795
|
if (!f) throw new Error("User not found");
|
|
20791
|
-
return
|
|
20796
|
+
return f;
|
|
20792
20797
|
}, TD = async () => {
|
|
20793
20798
|
await Yn.auth.signOut();
|
|
20794
20799
|
}, FD = (t) => {
|