@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 CHANGED
@@ -7,15 +7,20 @@
7
7
  </div>
8
8
  <br>
9
9
 
10
- The official TypeScript SDK for the OpenEDC Health Platform — a modular, standards-compliant platform for medical research.
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** metadata, clinical data, administrative data, and more with filtering, sorting, and cross-project support
15
- - **Mutate** study data with a single `.commit()` call, including cascading saves across related records
16
- - **Stream live updates** via reactive queries that automatically reflect changes in real time
17
- - **Track changes** with a built-in audit trail for all relevant study data
18
- - **Manage files** with resumable uploads and signed downloads, supporting files up to 5 GB
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
+ ![API key creation](https://cappie.studio/images/74d309b5-ee64-4302-9a5c-3b33df49ede4.jpg)
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
- .asc().all();
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
- // Instead of specifying the project for each query or commit, it can also be scoped globally for
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
- .asc().all();
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: Record<string, ExtendedKey>): Collection<T>;
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>): this & 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
- private startDateUTC;
811
+ startDateUTC: string;
805
812
  private startTimeUTC?;
806
- private endDateUTC;
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 (Array.isArray(s)) {
9839
- const o = s.map((u) => ke.fromString(u)).filter((u) => !!u), a = i.lazy ? o : await Promise.all(o.map((u) => u.data));
9840
- Reflect.set(r, n, a.filter((u) => u));
9841
- } else {
9842
- const o = ke.fromString(s), a = i.lazy ? o : await (o == null ? void 0 : o.data);
9843
- Reflect.set(r, n, a);
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.translationGlob = {}, kt.defaults = {}, kt.overwrites = {}, kt.localizedElements = /* @__PURE__ */ new Map(), kt.defaultLocale = new Intl.Locale("en"), kt.currentLocale = kt.defaultLocale;
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(t + "/api/status");
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(t + "/api/auth/api-key", {
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 Ps.supabaseClient = Yn, V.adapter = Ps, f;
20796
+ return f;
20792
20797
  }, TD = async () => {
20793
20798
  await Yn.auth.signOut();
20794
20799
  }, FD = (t) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openedc/sdk",
3
- "version": "3.8.2-next.0",
3
+ "version": "3.8.2-next.2",
4
4
  "description": "TypeScript SDK for the OpenEDC Health Platform",
5
5
  "type": "module",
6
6
  "main": "./openedc-sdk.js",