cloesce 0.0.5-unstable.4 → 0.0.5-unstable.6

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.
Binary file
package/dist/orm.wasm CHANGED
Binary file
@@ -1,7 +1,6 @@
1
- import { D1Database } from "@cloudflare/workers-types/experimental/index.js";
2
1
  /**
3
2
  * Wraps an object in a Proxy that will intercept non-overriden CRUD methods,
4
3
  * calling a default implementation.
5
4
  */
6
- export declare function proxyCrud(obj: any, ctor: any, d1: D1Database): any;
5
+ export declare function proxyCrud(obj: any, ctor: any, env: any): any;
7
6
  //# sourceMappingURL=crud.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../../src/router/crud.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iDAAiD,CAAC;AAK7E;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,UAAU,OAyB5D"}
1
+ {"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../../src/router/crud.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,OAyBtD"}
@@ -1,11 +1,11 @@
1
- import { Orm } from "../ui/backend.js";
2
- import { HttpResult } from "../ui/common.js";
1
+ import { Orm, HttpResult } from "../ui/backend.js";
3
2
  import { NO_DATA_SOURCE } from "../ast.js";
3
+ import { RuntimeContainer } from "./router.js";
4
4
  /**
5
5
  * Wraps an object in a Proxy that will intercept non-overriden CRUD methods,
6
6
  * calling a default implementation.
7
7
  */
8
- export function proxyCrud(obj, ctor, d1) {
8
+ export function proxyCrud(obj, ctor, env) {
9
9
  return new Proxy(obj, {
10
10
  get(target, method) {
11
11
  // If the instance defines the method, always use it (override allowed)
@@ -15,44 +15,53 @@ export function proxyCrud(obj, ctor, d1) {
15
15
  }
16
16
  // Fallback to CRUD methods
17
17
  if (method === "save") {
18
- return (body, ds) => upsert(ctor, body, ds, d1);
18
+ return (body, ds) => upsert(ctor, body, ds, env);
19
19
  }
20
20
  if (method === "list") {
21
- return (ds) => list(ctor, ds, d1);
21
+ return (ds) => list(ctor, ds, env);
22
22
  }
23
23
  if (method === "get") {
24
- return (id, ds) => _get(ctor, id, ds, d1);
24
+ return (...args) => _get(ctor, args, env);
25
25
  }
26
26
  return value;
27
27
  },
28
28
  });
29
29
  }
30
- async function upsert(ctor, body, dataSource, d1) {
30
+ async function upsert(ctor, body, dataSource, env) {
31
31
  const includeTree = findIncludeTree(dataSource, ctor);
32
- const orm = Orm.fromD1(d1);
32
+ const orm = Orm.fromEnv(env);
33
+ // Upsert
33
34
  const result = await orm.upsert(ctor, body, includeTree);
34
- if (result.isLeft())
35
- return HttpResult.fail(500, result.value);
36
- const getRes = await orm.get(ctor, result.value, includeTree);
37
- return getRes.isRight()
38
- ? HttpResult.ok(200, getRes.value)
39
- : HttpResult.fail(500, getRes.value);
35
+ return !result ? HttpResult.fail(404) : HttpResult.ok(200, result);
40
36
  }
41
- async function _get(ctor, id, dataSource, d1) {
42
- const includeTree = findIncludeTree(dataSource, ctor);
43
- const orm = Orm.fromD1(d1);
44
- const res = await orm.get(ctor, id, includeTree);
45
- return res.isRight()
46
- ? HttpResult.ok(200, res.value)
47
- : HttpResult.fail(500, res.value);
37
+ async function _get(ctor, args, env) {
38
+ const { ast } = RuntimeContainer.get();
39
+ const model = ast.models[ctor.name];
40
+ const getArgs = {};
41
+ let argIndex = 0;
42
+ if (model.primary_key) {
43
+ // If there is a primary key, the first argument is the primary key.
44
+ getArgs.id = args[argIndex++];
45
+ }
46
+ if (model.key_params.length > 0) {
47
+ // All key params come after the primary key.
48
+ // Order is guaranteed by the compiler.
49
+ getArgs.keyParams = {};
50
+ for (const keyParam of model.key_params) {
51
+ getArgs.keyParams[keyParam] = args[argIndex++];
52
+ }
53
+ }
54
+ // The last argument is always the data source.
55
+ getArgs.includeTree = findIncludeTree(args[argIndex], ctor);
56
+ const orm = Orm.fromEnv(env);
57
+ const result = await orm.get(ctor, getArgs);
58
+ return !result ? HttpResult.fail(404) : HttpResult.ok(200, result);
48
59
  }
49
- async function list(ctor, dataSource, d1) {
60
+ async function list(ctor, dataSource, env) {
50
61
  const includeTree = findIncludeTree(dataSource, ctor);
51
- const orm = Orm.fromD1(d1);
52
- const res = await orm.list(ctor, { includeTree });
53
- return res.isRight()
54
- ? HttpResult.ok(200, res.value)
55
- : HttpResult.fail(500, res.value);
62
+ const orm = Orm.fromEnv(env);
63
+ const result = await orm.list(ctor, includeTree);
64
+ return HttpResult.ok(200, result);
56
65
  }
57
66
  function findIncludeTree(dataSource, ctor) {
58
67
  const normalizedDs = dataSource === NO_DATA_SOURCE ? null : dataSource;
@@ -0,0 +1,66 @@
1
+ import type { D1Result } from "@cloudflare/workers-types";
2
+ import { IncludeTree, DeepPartial } from "../ui/backend.js";
3
+ export declare class Orm {
4
+ private env;
5
+ private constructor();
6
+ /**
7
+ * Creates an instance of an `Orm`
8
+ * @param env The Wrangler environment containing Cloudflare bindings.
9
+ */
10
+ static fromEnv(env: unknown): Orm;
11
+ private get db();
12
+ /**
13
+ * Maps D1 results into model instances. Capable of mapping a flat result set
14
+ * (ie, SELECT * FROM Model) or a joined result granted it is aliased as `select_model` would produce.
15
+ *
16
+ * Does not hydrate into an instance of the model; for that, use `hydrate` after mapping.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const d1Result = await db.prepare("SELECT * FROM User").all();
21
+ * const users: User[] = Orm.map(User, d1Result.results);
22
+ * ```
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const d1Result = await db.prepare(`
27
+ * ${Orm.select(User, null, { posts: {} })}
28
+ * WHERE User.id = ?
29
+ * `).bind(1).all();
30
+ *
31
+ * const users: User[] = Orm.map(User, d1Result.results, { posts: {} });
32
+ * ```
33
+ *
34
+ * @param ctor Constructor of the model to map to
35
+ * @param d1Results Results from a D1 query
36
+ * @param includeTree Include tree specifying which navigation properties to include
37
+ * @returns Array of mapped model instances
38
+ */
39
+ static map<T extends object>(ctor: new () => T, d1Results: D1Result, includeTree?: IncludeTree<T> | null): T[];
40
+ static select<T extends object>(ctor: new () => T, args?: {
41
+ from?: string | null;
42
+ includeTree?: IncludeTree<T> | null;
43
+ }): string;
44
+ hydrate<T extends object>(ctor: new () => T, args?: {
45
+ base?: any;
46
+ keyParams?: Record<string, string>;
47
+ includeTree?: IncludeTree<T> | null;
48
+ }): Promise<T>;
49
+ upsert<T extends object>(ctor: new () => T, newModel: DeepPartial<T>, includeTree?: IncludeTree<T> | null): Promise<T | null>;
50
+ list<T extends object>(ctor: new () => T, includeTree?: IncludeTree<T> | null): Promise<T[]>;
51
+ /**
52
+ * Fetches a model by its primary key ID or key parameters.
53
+ * * If the model does not have a primary key, key parameters must be provided.
54
+ * * If the model has a primary key, the ID must be provided.
55
+ *
56
+ * @param ctor Constructor of the model to retrieve
57
+ * @param args Arguments for retrieval
58
+ * @returns The retrieved model instance, or `null` if not found
59
+ */
60
+ get<T extends object>(ctor: new () => T, args?: {
61
+ id?: any;
62
+ keyParams?: Record<string, string>;
63
+ includeTree?: IncludeTree<T> | null;
64
+ }): Promise<T | null>;
65
+ }
66
+ //# sourceMappingURL=orm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orm.d.ts","sourceRoot":"","sources":["../../src/router/orm.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,QAAQ,EACT,MAAM,2BAA2B,CAAC;AAMnC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAU,MAAM,kBAAkB,CAAC;AAEpE,qBAAa,GAAG;IACM,OAAO,CAAC,GAAG;IAA/B,OAAO;IAEP;;;OAGG;IACH,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG;IAOjC,OAAO,KAAK,EAAE,GAGb;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,MAAM,EACzB,IAAI,EAAE,UAAU,CAAC,EACjB,SAAS,EAAE,QAAQ,EACnB,WAAW,GAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAW,GACxC,CAAC,EAAE;IAyBN,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,MAAM,EAC5B,IAAI,EAAE,UAAU,CAAC,EACjB,IAAI,GAAE;QACJ,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,WAAW,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;KAIrC,GACA,MAAM;IA0BH,OAAO,CAAC,CAAC,SAAS,MAAM,EAC5B,IAAI,EAAE,UAAU,CAAC,EACjB,IAAI,GAAE;QACJ,IAAI,CAAC,EAAE,GAAG,CAAC;QACX,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACnC,WAAW,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;KAKrC,GACA,OAAO,CAAC,CAAC,CAAC;IAkNP,MAAM,CAAC,CAAC,SAAS,MAAM,EAC3B,IAAI,EAAE,UAAU,CAAC,EACjB,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EACxB,WAAW,GAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAW,GACxC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAqLd,IAAI,CAAC,CAAC,SAAS,MAAM,EACzB,IAAI,EAAE,UAAU,CAAC,EACjB,WAAW,GAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAW,GACxC,OAAO,CAAC,CAAC,EAAE,CAAC;IAqCf;;;;;;;;OAQG;IACG,GAAG,CAAC,CAAC,SAAS,MAAM,EACxB,IAAI,EAAE,UAAU,CAAC,EACjB,IAAI,GAAE;QACJ,EAAE,CAAC,EAAE,GAAG,CAAC;QACT,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACnC,WAAW,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;KAKrC,GACA,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CA2CrB"}
@@ -0,0 +1,447 @@
1
+ import { RuntimeContainer } from "../router/router.js";
2
+ import { WasmResource, invokeOrmWasm } from "../router/wasm.js";
3
+ import { InternalError, u8ToB64 } from "../common.js";
4
+ import { KValue } from "../ui/backend.js";
5
+ export class Orm {
6
+ env;
7
+ constructor(env) {
8
+ this.env = env;
9
+ }
10
+ /**
11
+ * Creates an instance of an `Orm`
12
+ * @param env The Wrangler environment containing Cloudflare bindings.
13
+ */
14
+ static fromEnv(env) {
15
+ // TODO: We could validate that `env` is of the correct type defined by the `@WranglerEnv` class
16
+ // by putting the class definition in the Constructor Registry at compile time.
17
+ return new Orm(env);
18
+ }
19
+ // TODO: support multiple D1 bindings
20
+ get db() {
21
+ const { ast } = RuntimeContainer.get();
22
+ return this.env[ast.wrangler_env.d1_binding];
23
+ }
24
+ /**
25
+ * Maps D1 results into model instances. Capable of mapping a flat result set
26
+ * (ie, SELECT * FROM Model) or a joined result granted it is aliased as `select_model` would produce.
27
+ *
28
+ * Does not hydrate into an instance of the model; for that, use `hydrate` after mapping.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const d1Result = await db.prepare("SELECT * FROM User").all();
33
+ * const users: User[] = Orm.map(User, d1Result.results);
34
+ * ```
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const d1Result = await db.prepare(`
39
+ * ${Orm.select(User, null, { posts: {} })}
40
+ * WHERE User.id = ?
41
+ * `).bind(1).all();
42
+ *
43
+ * const users: User[] = Orm.map(User, d1Result.results, { posts: {} });
44
+ * ```
45
+ *
46
+ * @param ctor Constructor of the model to map to
47
+ * @param d1Results Results from a D1 query
48
+ * @param includeTree Include tree specifying which navigation properties to include
49
+ * @returns Array of mapped model instances
50
+ */
51
+ static map(ctor, d1Results, includeTree = null) {
52
+ const { wasm } = RuntimeContainer.get();
53
+ const d1ResultsRes = WasmResource.fromString(JSON.stringify(d1Results.results), wasm);
54
+ const includeTreeRes = WasmResource.fromString(JSON.stringify(includeTree), wasm);
55
+ const mapQueryRes = invokeOrmWasm(wasm.map, [WasmResource.fromString(ctor.name, wasm), d1ResultsRes, includeTreeRes], wasm);
56
+ if (mapQueryRes.isLeft()) {
57
+ throw new InternalError(`Mapping failed: ${mapQueryRes.value}`);
58
+ }
59
+ return JSON.parse(mapQueryRes.unwrap());
60
+ }
61
+ // Given a model, generates a sequence of joins to select it with its includes.
62
+ static select(ctor, args = {
63
+ from: null,
64
+ includeTree: null,
65
+ }) {
66
+ const { wasm } = RuntimeContainer.get();
67
+ const fromRes = WasmResource.fromString(JSON.stringify(args.from ?? null), wasm);
68
+ const includeTreeRes = WasmResource.fromString(JSON.stringify(args.includeTree ?? null), wasm);
69
+ const selectQueryRes = invokeOrmWasm(wasm.select_model, [WasmResource.fromString(ctor.name, wasm), fromRes, includeTreeRes], wasm);
70
+ if (selectQueryRes.isLeft()) {
71
+ throw new InternalError(`Select generation failed: ${selectQueryRes.value}`);
72
+ }
73
+ return selectQueryRes.unwrap();
74
+ }
75
+ async hydrate(ctor, args = {
76
+ base: {},
77
+ keyParams: {},
78
+ includeTree: null,
79
+ }) {
80
+ const { ast, constructorRegistry } = RuntimeContainer.get();
81
+ const model = ast.models[ctor.name];
82
+ if (!model) {
83
+ return args.base ?? {};
84
+ }
85
+ const env = this.env;
86
+ const instance = Object.assign(new constructorRegistry[model.name](), args.base);
87
+ const promises = [];
88
+ recurse(instance, model, args.includeTree ?? {});
89
+ await Promise.all(promises);
90
+ return instance;
91
+ async function hydrateKVList(namespace, key, kv, current) {
92
+ const res = await namespace.list({ prefix: key });
93
+ if (kv.value.cidl_type === "Stream") {
94
+ current[kv.value.name] = await Promise.all(res.keys.map(async (k) => {
95
+ const stream = await namespace.get(k.name, { type: "stream" });
96
+ return Object.assign(new KValue(), {
97
+ key: k.name,
98
+ raw: stream,
99
+ metadata: null,
100
+ });
101
+ }));
102
+ }
103
+ else {
104
+ current[kv.value.name] = await Promise.all(res.keys.map(async (k) => {
105
+ const kvRes = await namespace.getWithMetadata(k.name, {
106
+ type: "json",
107
+ });
108
+ return Object.assign(new KValue(), {
109
+ key: k.name,
110
+ raw: kvRes.value,
111
+ metadata: kvRes.metadata,
112
+ });
113
+ }));
114
+ }
115
+ }
116
+ async function hydrateKVSingle(namespace, key, kv, current) {
117
+ if (kv.value.cidl_type === "Stream") {
118
+ const res = await namespace.get(key, { type: "stream" });
119
+ current[kv.value.name] = Object.assign(new KValue(), {
120
+ key,
121
+ raw: res,
122
+ metadata: null,
123
+ });
124
+ }
125
+ else {
126
+ const res = await namespace.getWithMetadata(key, { type: "json" });
127
+ current[kv.value.name] = Object.assign(new KValue(), {
128
+ key,
129
+ raw: res.value,
130
+ metadata: res.metadata,
131
+ });
132
+ }
133
+ }
134
+ function recurse(current, meta, includeTree) {
135
+ // Hydrate navigation properties
136
+ for (const navProp of meta.navigation_properties) {
137
+ const nestedTree = includeTree[navProp.var_name];
138
+ if (!nestedTree)
139
+ continue;
140
+ const nestedMeta = ast.models[navProp.model_reference];
141
+ const value = current[navProp.var_name];
142
+ if (Array.isArray(value)) {
143
+ const ctor = constructorRegistry[nestedMeta.name];
144
+ current[navProp.var_name] = value.map((child) => {
145
+ const instance = Object.assign(new ctor(), child);
146
+ recurse(instance, nestedMeta, nestedTree);
147
+ return instance;
148
+ });
149
+ }
150
+ else if (value) {
151
+ current[navProp.var_name] = Object.assign(new constructorRegistry[nestedMeta.name](), value);
152
+ recurse(current[navProp.var_name], nestedMeta, nestedTree);
153
+ }
154
+ }
155
+ // Hydrate columns
156
+ for (const col of meta.columns) {
157
+ if (current[col.value.name] === undefined) {
158
+ continue;
159
+ }
160
+ switch (col.value.cidl_type) {
161
+ case "DateIso": {
162
+ current[col.value.name] = new Date(current[col.value.name]);
163
+ break;
164
+ }
165
+ case "Blob": {
166
+ const arr = current[col.value.name];
167
+ current[col.value.name] = new Uint8Array(arr);
168
+ }
169
+ default: {
170
+ break;
171
+ }
172
+ }
173
+ }
174
+ // Hydrate key params
175
+ if (args.keyParams) {
176
+ for (const keyParam of meta.key_params) {
177
+ current[keyParam] = args.keyParams[keyParam];
178
+ }
179
+ }
180
+ // Hydrate KV objects
181
+ for (const kv of meta.kv_objects) {
182
+ // Include check
183
+ if (includeTree[kv.value.name] === undefined) {
184
+ if (kv.list_prefix) {
185
+ current[kv.value.name] = [];
186
+ }
187
+ continue;
188
+ }
189
+ const key = resolveKey(kv.format, current, args.keyParams ?? {});
190
+ if (!key) {
191
+ if (kv.list_prefix) {
192
+ current[kv.value.name] = [];
193
+ }
194
+ // All key params must be resolvable.
195
+ // Fail silently by skipping hydration.
196
+ continue;
197
+ }
198
+ const namespace = env[kv.namespace_binding];
199
+ if (kv.list_prefix) {
200
+ promises.push(hydrateKVList(namespace, key, kv, current));
201
+ }
202
+ else {
203
+ promises.push(hydrateKVSingle(namespace, key, kv, current));
204
+ }
205
+ }
206
+ // Hydrate R2 objects
207
+ for (const r2 of meta.r2_objects) {
208
+ if (includeTree[r2.var_name] === undefined) {
209
+ if (r2.list_prefix) {
210
+ current[r2.var_name] = [];
211
+ }
212
+ continue;
213
+ }
214
+ const key = resolveKey(r2.format, current, args.keyParams ?? {});
215
+ if (!key) {
216
+ if (r2.list_prefix) {
217
+ current[r2.var_name] = [];
218
+ }
219
+ // All key params must be resolvable.
220
+ // Fail silently by skipping hydration.
221
+ continue;
222
+ }
223
+ const bucket = env[r2.bucket_binding];
224
+ if (r2.list_prefix) {
225
+ promises.push((async () => {
226
+ const list = await bucket.list({ prefix: key });
227
+ current[r2.var_name] = await Promise.all(list.objects.map(async (obj) => {
228
+ const fullObj = await bucket.get(obj.key);
229
+ return fullObj;
230
+ }));
231
+ })());
232
+ }
233
+ else {
234
+ promises.push((async () => {
235
+ const obj = await bucket.get(key);
236
+ current[r2.var_name] = obj;
237
+ })());
238
+ }
239
+ }
240
+ }
241
+ }
242
+ async upsert(ctor, newModel, includeTree = null) {
243
+ const { wasm, ast } = RuntimeContainer.get();
244
+ const meta = ast.models[ctor.name];
245
+ const upsertQueryRes = invokeOrmWasm(wasm.upsert_model, [
246
+ WasmResource.fromString(ctor.name, wasm),
247
+ WasmResource.fromString(
248
+ // TODO: Stringify only objects in the include tree?
249
+ // Could try to track `this` in the reviver function
250
+ JSON.stringify(newModel, (_, v) =>
251
+ // To serialize a Uint8Array s.t. WASM can read it, we convert it to a base64 string.
252
+ v instanceof Uint8Array ? u8ToB64(v) : v), wasm),
253
+ WasmResource.fromString(JSON.stringify(includeTree), wasm),
254
+ ], wasm);
255
+ if (upsertQueryRes.isLeft()) {
256
+ throw new InternalError(`Upsert failed: ${upsertQueryRes.value}`);
257
+ }
258
+ const res = JSON.parse(upsertQueryRes.unwrap());
259
+ const kvUploadPromises = res.kv_uploads.map(async (upload) => {
260
+ const namespace = this.env[upload.namespace_binding];
261
+ if (!namespace) {
262
+ throw new InternalError(`KV Namespace binding "${upload.namespace_binding}" not found for upsert.`);
263
+ }
264
+ await namespace.put(upload.key, JSON.stringify(upload.value), {
265
+ metadata: upload.metadata,
266
+ });
267
+ });
268
+ const queries = res.sql.map((s) => this.db.prepare(s.query).bind(...s.values));
269
+ // Concurrently execute SQL with KV uploads.
270
+ const [batchRes] = await Promise.all([
271
+ queries.length > 0 ? this.db.batch(queries) : Promise.resolve([]),
272
+ ...kvUploadPromises,
273
+ ]);
274
+ let base = {};
275
+ if (queries.length > 0) {
276
+ const failed = batchRes.find((r) => !r.success);
277
+ if (failed) {
278
+ // An error in the upsert should not be possible unless the AST is invalid.
279
+ throw new InternalError(`Upsert failed during execution: ${failed.error}`);
280
+ }
281
+ // A SELECT statement towards the end will call `select_model` to retrieve the upserted model.
282
+ let selectIndex;
283
+ for (let i = res.sql.length - 1; i >= 0; i--) {
284
+ if (/^SELECT/i.test(res.sql[i].query)) {
285
+ selectIndex = i;
286
+ break;
287
+ }
288
+ }
289
+ base = Orm.map(ctor, batchRes[selectIndex], includeTree)[0];
290
+ }
291
+ // Base needs to include all of the key params from newModel and its includes.
292
+ const q = [
293
+ { model: base, meta, tree: includeTree ?? {} },
294
+ ];
295
+ while (q.length > 0) {
296
+ const { model: currentModel, meta: currentMeta, tree: currentTree, } = q.shift();
297
+ // Key params
298
+ for (const keyParam of currentMeta.key_params) {
299
+ if (currentModel[keyParam] === undefined) {
300
+ currentModel[keyParam] = newModel[keyParam];
301
+ }
302
+ }
303
+ // Navigation properties
304
+ for (const navProp of currentMeta.navigation_properties) {
305
+ const nestedTree = currentTree[navProp.var_name];
306
+ if (!nestedTree)
307
+ continue;
308
+ const nestedMeta = ast.models[navProp.model_reference];
309
+ const value = currentModel[navProp.var_name];
310
+ if (Array.isArray(value)) {
311
+ for (let i = 0; i < value.length; i++) {
312
+ q.push({
313
+ model: value[i],
314
+ meta: nestedMeta,
315
+ tree: nestedTree,
316
+ });
317
+ }
318
+ }
319
+ else if (value) {
320
+ q.push({
321
+ model: value,
322
+ meta: nestedMeta,
323
+ tree: nestedTree,
324
+ });
325
+ }
326
+ }
327
+ }
328
+ // Upload all delayed KV uploads
329
+ await Promise.all(res.kv_delayed_uploads.map(async (upload) => {
330
+ let current = base;
331
+ for (const pathPart of upload.path) {
332
+ current = current[pathPart];
333
+ if (current === undefined) {
334
+ throw new InternalError(`Failed to resolve path ${upload.path.join(".")} for delayed KV upload.`);
335
+ }
336
+ }
337
+ const namespace = this.env[upload.namespace_binding];
338
+ if (!namespace) {
339
+ throw new InternalError(`KV Namespace binding "${upload.namespace_binding}" not found for upsert.`);
340
+ }
341
+ const key = resolveKey(upload.key, current, {});
342
+ if (!key) {
343
+ throw new InternalError(`Failed to resolve key format "${upload.key}" for delayed KV upload.`);
344
+ }
345
+ await namespace.put(key, JSON.stringify(upload.value), {
346
+ metadata: upload.metadata,
347
+ });
348
+ }));
349
+ // Hydrate and return the upserted model
350
+ return await this.hydrate(ctor, {
351
+ base: base,
352
+ includeTree,
353
+ });
354
+ }
355
+ async list(ctor, includeTree = null) {
356
+ const { ast } = RuntimeContainer.get();
357
+ const model = ast.models[ctor.name];
358
+ if (!model) {
359
+ return [];
360
+ }
361
+ if (model.primary_key === null) {
362
+ // Listing is not supported for models without primary keys (i.e., KV or R2 only).
363
+ return [];
364
+ }
365
+ const query = Orm.select(ctor, {
366
+ includeTree,
367
+ });
368
+ const rows = await this.db.prepare(query).all();
369
+ if (rows.error) {
370
+ // An error in the query should not be possible unless the AST is invalid.
371
+ throw new InternalError(`Failed to list models for ${ctor.name}: ${rows.error}`);
372
+ }
373
+ // Map and hydrate
374
+ const results = Orm.map(ctor, rows, includeTree);
375
+ await Promise.all(results.map(async (modelJson, index) => {
376
+ results[index] = await this.hydrate(ctor, {
377
+ base: modelJson,
378
+ includeTree,
379
+ });
380
+ }));
381
+ return results;
382
+ }
383
+ /**
384
+ * Fetches a model by its primary key ID or key parameters.
385
+ * * If the model does not have a primary key, key parameters must be provided.
386
+ * * If the model has a primary key, the ID must be provided.
387
+ *
388
+ * @param ctor Constructor of the model to retrieve
389
+ * @param args Arguments for retrieval
390
+ * @returns The retrieved model instance, or `null` if not found
391
+ */
392
+ async get(ctor, args = {
393
+ id: undefined,
394
+ keyParams: {},
395
+ includeTree: null,
396
+ }) {
397
+ const { ast } = RuntimeContainer.get();
398
+ const model = ast.models[ctor.name];
399
+ if (!model) {
400
+ return null;
401
+ }
402
+ // KV or R2 only
403
+ if (model.primary_key === null) {
404
+ return await this.hydrate(ctor, {
405
+ keyParams: args.keyParams,
406
+ includeTree: args.includeTree ?? null,
407
+ });
408
+ }
409
+ // D1 retrieval
410
+ const pkName = model.primary_key.name;
411
+ const query = `
412
+ ${Orm.select(ctor, { includeTree: args.includeTree })}
413
+ WHERE "${model.name}"."${pkName}" = ?
414
+ `;
415
+ const rows = await this.db.prepare(query).bind(args.id).run();
416
+ if (rows.error) {
417
+ // An error in the query should not be possible unless the AST is invalid.
418
+ throw new InternalError(`Failed to retrieve model ${ctor.name} with ${pkName}=${args.id}: ${rows.error}`);
419
+ }
420
+ if (rows.results.length < 1) {
421
+ return null;
422
+ }
423
+ // Map and hydrate
424
+ const results = Orm.map(ctor, rows, args.includeTree ?? null);
425
+ return await this.hydrate(ctor, {
426
+ base: results.at(0),
427
+ keyParams: args.keyParams,
428
+ includeTree: args.includeTree ?? null,
429
+ });
430
+ }
431
+ }
432
+ /**
433
+ * @returns null if any parameter could not be resolved
434
+ */
435
+ function resolveKey(format, current, keyParams) {
436
+ try {
437
+ return format.replace(/\{([^}]+)\}/g, (_, paramName) => {
438
+ const paramValue = keyParams[paramName] ?? current[paramName];
439
+ if (!paramValue)
440
+ throw null;
441
+ return String(paramValue);
442
+ });
443
+ }
444
+ catch {
445
+ return null;
446
+ }
447
+ }