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.
- package/dist/ast.d.ts +28 -18
- package/dist/ast.d.ts.map +1 -1
- package/dist/ast.js +3 -3
- package/dist/cli.js +4 -5
- package/dist/common.d.ts +23 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +78 -0
- package/dist/extractor/err.d.ts +12 -13
- package/dist/extractor/err.d.ts.map +1 -1
- package/dist/extractor/err.js +41 -46
- package/dist/extractor/extract.d.ts +21 -14
- package/dist/extractor/extract.d.ts.map +1 -1
- package/dist/extractor/extract.js +575 -348
- package/dist/generator.wasm +0 -0
- package/dist/orm.wasm +0 -0
- package/dist/router/crud.d.ts +1 -2
- package/dist/router/crud.d.ts.map +1 -1
- package/dist/router/crud.js +36 -27
- package/dist/router/orm.d.ts +66 -0
- package/dist/router/orm.d.ts.map +1 -0
- package/dist/router/orm.js +447 -0
- package/dist/router/router.d.ts +21 -30
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +116 -116
- package/dist/router/validator.d.ts +1 -1
- package/dist/router/validator.d.ts.map +1 -1
- package/dist/router/validator.js +48 -4
- package/dist/router/wasm.d.ts +9 -16
- package/dist/router/wasm.d.ts.map +1 -1
- package/dist/router/wasm.js +10 -49
- package/dist/ui/backend.d.ts +95 -341
- package/dist/ui/backend.d.ts.map +1 -1
- package/dist/ui/backend.js +135 -409
- package/package.json +4 -10
- package/dist/ui/client.d.ts +0 -7
- package/dist/ui/client.d.ts.map +0 -1
- package/dist/ui/client.js +0 -2
- package/dist/ui/common.d.ts +0 -103
- package/dist/ui/common.d.ts.map +0 -1
- package/dist/ui/common.js +0 -191
package/dist/generator.wasm
CHANGED
|
Binary file
|
package/dist/orm.wasm
CHANGED
|
Binary file
|
package/dist/router/crud.d.ts
CHANGED
|
@@ -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,
|
|
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":"
|
|
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"}
|
package/dist/router/crud.js
CHANGED
|
@@ -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,
|
|
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,
|
|
18
|
+
return (body, ds) => upsert(ctor, body, ds, env);
|
|
19
19
|
}
|
|
20
20
|
if (method === "list") {
|
|
21
|
-
return (ds) => list(ctor, ds,
|
|
21
|
+
return (ds) => list(ctor, ds, env);
|
|
22
22
|
}
|
|
23
23
|
if (method === "get") {
|
|
24
|
-
return (
|
|
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,
|
|
30
|
+
async function upsert(ctor, body, dataSource, env) {
|
|
31
31
|
const includeTree = findIncludeTree(dataSource, ctor);
|
|
32
|
-
const orm = Orm.
|
|
32
|
+
const orm = Orm.fromEnv(env);
|
|
33
|
+
// Upsert
|
|
33
34
|
const result = await orm.upsert(ctor, body, includeTree);
|
|
34
|
-
|
|
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,
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,
|
|
60
|
+
async function list(ctor, dataSource, env) {
|
|
50
61
|
const includeTree = findIncludeTree(dataSource, ctor);
|
|
51
|
-
const orm = Orm.
|
|
52
|
-
const
|
|
53
|
-
return
|
|
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
|
+
}
|