cloesce 0.0.4-unstable.0 → 0.0.4-unstable.10

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.
@@ -1,4 +1,4 @@
1
- import { NO_DATA_SOURCE } from "../common.js";
1
+ import { HttpResult, NO_DATA_SOURCE } from "../common.js";
2
2
  import { Orm } from "../ui/backend.js";
3
3
  /**
4
4
  * A wrapper for Model Instances, containing definitions for built-in CRUD methods.
@@ -24,7 +24,7 @@ export class CrudContext {
24
24
  */
25
25
  interceptCrud(methodName) {
26
26
  const map = {
27
- post: this.upsert.bind(this),
27
+ save: this.upsert.bind(this),
28
28
  get: this.get.bind(this),
29
29
  list: this.list.bind(this),
30
30
  };
@@ -32,34 +32,39 @@ export class CrudContext {
32
32
  return fn ? fn.bind(this.instance) : map[methodName];
33
33
  }
34
34
  async upsert(obj, dataSource) {
35
- const normalizedDs = dataSource === NO_DATA_SOURCE ? null : dataSource;
36
- const includeTree = normalizedDs ? this.ctor[normalizedDs] : null;
35
+ const includeTree = findIncludeTree(dataSource, this.ctor);
37
36
  // Upsert
38
37
  const orm = Orm.fromD1(this.d1);
39
38
  const upsert = await orm.upsert(this.ctor, obj, includeTree);
40
- if (!upsert.ok) {
41
- return { ok: false, status: 500, data: upsert.value }; // TODO: better status code?
39
+ if (upsert.isLeft()) {
40
+ return HttpResult.fail(500, upsert.value); // TODO: better status code?
42
41
  }
43
42
  // Get
44
- const get = await orm.get(this.ctor, upsert.value, normalizedDs);
45
- return get.ok
46
- ? { ok: true, status: 200, data: get.value }
47
- : { ok: false, status: 500, data: get.value };
43
+ const get = await orm.get(this.ctor, upsert.value, includeTree);
44
+ return get.isRight()
45
+ ? HttpResult.ok(200, get.value)
46
+ : HttpResult.fail(500, get.value);
48
47
  }
49
48
  async get(id, dataSource) {
50
- const normalizedDs = dataSource === NO_DATA_SOURCE ? null : dataSource;
49
+ const includeTree = findIncludeTree(dataSource, this.ctor);
51
50
  const orm = Orm.fromD1(this.d1);
52
- const res = await orm.get(this.ctor, id, normalizedDs);
53
- return res.ok
54
- ? { ok: true, status: 200, data: res.value }
55
- : { ok: false, status: 500, data: res.value };
51
+ const res = await orm.get(this.ctor, id, includeTree);
52
+ return res.isRight()
53
+ ? HttpResult.ok(200, res.value)
54
+ : HttpResult.fail(500, res.value);
56
55
  }
57
56
  async list(dataSource) {
58
- const normalizedDs = dataSource === NO_DATA_SOURCE ? null : dataSource;
57
+ const includeTree = findIncludeTree(dataSource, this.ctor);
59
58
  const orm = Orm.fromD1(this.d1);
60
- const res = await orm.list(this.ctor, normalizedDs);
61
- return res.ok
62
- ? { ok: true, status: 200, data: res.value }
63
- : { ok: false, status: 500, data: res.value };
59
+ const res = await orm.list(this.ctor, {
60
+ includeTree,
61
+ });
62
+ return res.isRight()
63
+ ? HttpResult.ok(200, res.value)
64
+ : HttpResult.fail(500, res.value);
64
65
  }
65
66
  }
67
+ function findIncludeTree(dataSource, ctor) {
68
+ const normalizedDs = dataSource === NO_DATA_SOURCE ? null : dataSource;
69
+ return normalizedDs ? ctor[normalizedDs] : null;
70
+ }
@@ -1,6 +1,12 @@
1
- import { HttpResult, Either, ModelMethod, CloesceAst, Model, CloesceApp, InstanceRegistry } from "../common.js";
1
+ import { HttpResult, Either, ModelMethod, CloesceAst, Model, KeysOfType } from "../common.js";
2
2
  import { OrmWasmExports } from "./wasm.js";
3
3
  import { CrudContext } from "./crud.js";
4
+ /**
5
+ * Dependency injection container, mapping an object type name to an instance of that object.
6
+ *
7
+ * Comes with the WranglerEnv and Request by default.
8
+ */
9
+ export type DependencyInjector = Map<string, any>;
4
10
  /**
5
11
  * Map of model names to their respective constructor.
6
12
  *
@@ -8,11 +14,6 @@ import { CrudContext } from "./crud.js";
8
14
  * is guaranteed to contain all model definitions.
9
15
  */
10
16
  type ModelConstructorRegistry = Record<string, new () => any>;
11
- /**
12
- * Given a request, this represents a map of each body / url param name to
13
- * its actual value. Unknown, as the a request can be anything.
14
- */
15
- type RequestParamMap = Record<string, unknown>;
16
17
  /**
17
18
  * Meta information on the wrangler env and db bindings
18
19
  */
@@ -20,6 +21,68 @@ interface MetaWranglerEnv {
20
21
  envName: string;
21
22
  dbName: string;
22
23
  }
24
+ export type MiddlewareFn = (request: Request, env: any, di: DependencyInjector) => Promise<HttpResult | void>;
25
+ export type ResultMiddlewareFn = (request: Request, env: any, di: DependencyInjector, result: HttpResult) => Promise<HttpResult | void>;
26
+ export declare class CloesceApp {
27
+ private globalMiddleware;
28
+ private modelMiddleware;
29
+ private methodMiddleware;
30
+ private resultMiddleware;
31
+ routePrefix: string;
32
+ /**
33
+ * Registers global middleware which runs before any route matching.
34
+ *
35
+ * @param m - The middleware function to register.
36
+ */
37
+ onRequest(m: MiddlewareFn): void;
38
+ /**
39
+ * Registers middleware which runs after the response is generated, but before
40
+ * it is returned to the client.
41
+ *
42
+ * Optionally, return a new HttpResult to short-circuit the response.
43
+ *
44
+ * Errors thrown in response middleware are caught and returned as a 500 response.
45
+ *
46
+ * Errors thrown in earlier middleware or route processing are not caught here.
47
+ *
48
+ * @param m - The middleware function to register.
49
+ */
50
+ onResult(m: ResultMiddlewareFn): void;
51
+ /**
52
+ * Registers middleware for a specific model type.
53
+ *
54
+ * Runs before request validation and method middleware.
55
+ *
56
+ * @typeParam T - The model type.
57
+ * @param ctor - The model constructor (used to derive its name).
58
+ * @param m - The middleware function to register.
59
+ */
60
+ onModel<T>(ctor: new () => T, m: MiddlewareFn): void;
61
+ /**
62
+ * Registers middleware for a specific method on a model.
63
+ *
64
+ * Runs after model middleware and request validation.
65
+ *
66
+ * @typeParam T - The model type.
67
+ * @param ctor - The model constructor (used to derive its name).
68
+ * @param method - The method name on the model.
69
+ * @param m - The middleware function to register.
70
+ */
71
+ onMethod<T>(ctor: new () => T, method: KeysOfType<T, (...args: any) => any>, m: MiddlewareFn): void;
72
+ /**
73
+ * Router entry point. Undergoes route matching, request validation, hydration, and method dispatch.
74
+ */
75
+ private cloesce;
76
+ /**
77
+ * Runs the Cloesce app. Intended to be called from the generated workers code.
78
+ */
79
+ run(request: Request, env: any, ast: CloesceAst, constructorRegistry: ModelConstructorRegistry, envMeta: MetaWranglerEnv): Promise<Response>;
80
+ }
81
+ /**
82
+ * Given a request, this represents a map of each body / url param name to
83
+ * its actual value. Unknown, as the a request can be anything.
84
+ */
85
+ export type RequestParamMap = Record<string, unknown>;
23
86
  /**
24
87
  * Singleton instance containing the cidl, constructor registry, and wasm binary.
25
88
  * These values are guaranteed to never change throughout a workers lifetime.
@@ -33,19 +96,12 @@ export declare class RuntimeContainer {
33
96
  static init(ast: CloesceAst, constructorRegistry: ModelConstructorRegistry, wasm?: WebAssembly.Instance): Promise<void>;
34
97
  static get(): RuntimeContainer;
35
98
  }
36
- /**
37
- * Runtime entry point. Given a request, undergoes: routing, validating,
38
- * hydrating, and method dispatch.
39
- *
40
- * @returns A Response with an `HttpResult` JSON body.
41
- */
42
- export declare function cloesce(request: Request, env: any, ast: CloesceAst, app: CloesceApp, constructorRegistry: ModelConstructorRegistry, envMeta: MetaWranglerEnv, apiRoute: string): Promise<Response>;
43
99
  /**
44
100
  * Matches a request to a method on a model.
45
101
  * @param apiRoute The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
46
102
  * @returns 404 or a `MatchedRoute`
47
103
  */
48
- declare function matchRoute(request: Request, ast: CloesceAst, apiRoute: string): Either<HttpResult, {
104
+ declare function matchRoute(request: Request, ast: CloesceAst, routePrefix: string): Either<HttpResult, {
49
105
  model: Model;
50
106
  method: ModelMethod;
51
107
  id: string | null;
@@ -63,7 +119,7 @@ declare function validateRequest(request: Request, ast: CloesceAst, model: Model
63
119
  * Calls a method on a model given a list of parameters.
64
120
  * @returns 500 on an uncaught client error, 200 with a result body on success
65
121
  */
66
- declare function methodDispatch(crudCtx: CrudContext, instanceRegistry: InstanceRegistry, method: ModelMethod, params: Record<string, unknown>): Promise<HttpResult<unknown>>;
122
+ declare function methodDispatch(crudCtx: CrudContext, instanceRegistry: DependencyInjector, method: ModelMethod, params: Record<string, unknown>): Promise<HttpResult<unknown>>;
67
123
  /**
68
124
  * For testing purposes
69
125
  */
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/router/router.ts"],"names":[],"mappings":"AACA,OAAO,EACL,UAAU,EACV,MAAM,EACN,WAAW,EAIX,UAAU,EAEV,KAAK,EAGL,UAAU,EACV,gBAAgB,EACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAwB,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC;;;;;GAKG;AACH,KAAK,wBAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC;AAE9D;;;GAGG;AACH,KAAK,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE/C;;GAEG;AACH,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,qBAAa,gBAAgB;aAGT,GAAG,EAAE,UAAU;aACf,mBAAmB,EAAE,wBAAwB;aAC7C,IAAI,EAAE,cAAc;IAJtC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA+B;IACtD,OAAO;WAMM,IAAI,CACf,GAAG,EAAE,UAAU,EACf,mBAAmB,EAAE,wBAAwB,EAC7C,IAAI,CAAC,EAAE,WAAW,CAAC,QAAQ;IAO7B,MAAM,CAAC,GAAG,IAAI,gBAAgB;CAG/B;AAED;;;;;GAKG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,UAAU,EACf,mBAAmB,EAAE,wBAAwB,EAC7C,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CAqFnB;AAED;;;;GAIG;AACH,iBAAS,UAAU,CACjB,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CACP,UAAU,EACV;IACE,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CACnB,CACF,CAyCA;AAED;;;;GAIG;AACH,iBAAe,eAAe,CAC5B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,WAAW,EACnB,EAAE,EAAE,MAAM,GAAG,IAAI,GAChB,OAAO,CACR,MAAM,CAAC,UAAU,EAAE;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAC3E,CA4DA;AA2DD;;;GAGG;AACH,iBAAe,cAAc,CAC3B,OAAO,EAAE,WAAW,EACpB,gBAAgB,EAAE,gBAAgB,EAClC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAkD9B;AA+HD;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;CAK5B,CAAC"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/router/router.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,153 @@
1
- import { left, right, isNullableType, getNavigationPropertyCidlType, NO_DATA_SOURCE, } from "../common.js";
2
- import { fromSql, loadOrmWasm } from "./wasm.js";
1
+ import { HttpResult, Either, isNullableType, getNavigationPropertyCidlType, NO_DATA_SOURCE, } from "../common.js";
2
+ import { mapSql, loadOrmWasm } from "./wasm.js";
3
3
  import { CrudContext } from "./crud.js";
4
+ import { Orm } from "../ui/backend.js";
5
+ export class CloesceApp {
6
+ globalMiddleware = [];
7
+ modelMiddleware = new Map();
8
+ methodMiddleware = new Map();
9
+ resultMiddleware = [];
10
+ routePrefix = "api";
11
+ /**
12
+ * Registers global middleware which runs before any route matching.
13
+ *
14
+ * @param m - The middleware function to register.
15
+ */
16
+ onRequest(m) {
17
+ this.globalMiddleware.push(m);
18
+ }
19
+ /**
20
+ * Registers middleware which runs after the response is generated, but before
21
+ * it is returned to the client.
22
+ *
23
+ * Optionally, return a new HttpResult to short-circuit the response.
24
+ *
25
+ * Errors thrown in response middleware are caught and returned as a 500 response.
26
+ *
27
+ * Errors thrown in earlier middleware or route processing are not caught here.
28
+ *
29
+ * @param m - The middleware function to register.
30
+ */
31
+ onResult(m) {
32
+ this.resultMiddleware.push(m);
33
+ }
34
+ /**
35
+ * Registers middleware for a specific model type.
36
+ *
37
+ * Runs before request validation and method middleware.
38
+ *
39
+ * @typeParam T - The model type.
40
+ * @param ctor - The model constructor (used to derive its name).
41
+ * @param m - The middleware function to register.
42
+ */
43
+ onModel(ctor, m) {
44
+ if (this.modelMiddleware.has(ctor.name)) {
45
+ this.modelMiddleware.get(ctor.name).push(m);
46
+ }
47
+ else {
48
+ this.modelMiddleware.set(ctor.name, [m]);
49
+ }
50
+ }
51
+ /**
52
+ * Registers middleware for a specific method on a model.
53
+ *
54
+ * Runs after model middleware and request validation.
55
+ *
56
+ * @typeParam T - The model type.
57
+ * @param ctor - The model constructor (used to derive its name).
58
+ * @param method - The method name on the model.
59
+ * @param m - The middleware function to register.
60
+ */
61
+ onMethod(ctor, method, m) {
62
+ if (!this.methodMiddleware.has(ctor.name)) {
63
+ this.methodMiddleware.set(ctor.name, new Map());
64
+ }
65
+ const methods = this.methodMiddleware.get(ctor.name);
66
+ if (!methods.has(method)) {
67
+ methods.set(method, []);
68
+ }
69
+ methods.get(method).push(m);
70
+ }
71
+ /**
72
+ * Router entry point. Undergoes route matching, request validation, hydration, and method dispatch.
73
+ */
74
+ async cloesce(request, env, ast, constructorRegistry, di, d1) {
75
+ // Global middleware
76
+ for (const m of this.globalMiddleware) {
77
+ const res = await m(request, env, di);
78
+ if (res) {
79
+ return res;
80
+ }
81
+ }
82
+ // Route match
83
+ const route = matchRoute(request, ast, this.routePrefix);
84
+ if (route.isLeft()) {
85
+ return route.value;
86
+ }
87
+ const { method, model, id } = route.unwrap();
88
+ // Model middleware
89
+ for (const m of this.modelMiddleware.get(model.name) ?? []) {
90
+ const res = await m(request, env, di);
91
+ if (res) {
92
+ return res;
93
+ }
94
+ }
95
+ // Request validation
96
+ const validation = await validateRequest(request, ast, model, method, id);
97
+ if (validation.isLeft()) {
98
+ return validation.value;
99
+ }
100
+ const { params, dataSource } = validation.unwrap();
101
+ // Method middleware
102
+ for (const m of this.methodMiddleware.get(model.name)?.get(method.name) ??
103
+ []) {
104
+ const res = await m(request, env, di);
105
+ if (res) {
106
+ return res;
107
+ }
108
+ }
109
+ // Hydration
110
+ const crudCtx = await (async () => {
111
+ if (method.is_static) {
112
+ return Either.right(CrudContext.fromCtor(d1, constructorRegistry[model.name]));
113
+ }
114
+ const hydratedModel = await hydrateModel(constructorRegistry, d1, model, id, // id must exist after matchRoute
115
+ dataSource);
116
+ return hydratedModel.map((_) => CrudContext.fromInstance(d1, hydratedModel.value, constructorRegistry[model.name]));
117
+ })();
118
+ if (crudCtx.isLeft()) {
119
+ return crudCtx.value;
120
+ }
121
+ // Method dispatch
122
+ return await methodDispatch(crudCtx.unwrap(), di, method, params);
123
+ }
124
+ /**
125
+ * Runs the Cloesce app. Intended to be called from the generated workers code.
126
+ */
127
+ async run(request, env, ast, constructorRegistry, envMeta) {
128
+ const di = new Map();
129
+ di.set(envMeta.envName, env);
130
+ di.set("Request", request);
131
+ await RuntimeContainer.init(ast, constructorRegistry);
132
+ const d1 = env[envMeta.dbName]; // TODO: multiple dbs
133
+ try {
134
+ // Core cloesce processing
135
+ const response = await this.cloesce(request, env, ast, constructorRegistry, di, d1);
136
+ // Response middleware
137
+ for (const m of this.resultMiddleware) {
138
+ const res = await m(request, env, di, response);
139
+ if (res) {
140
+ return res.toResponse();
141
+ }
142
+ }
143
+ return response.toResponse();
144
+ }
145
+ catch (e) {
146
+ console.error(JSON.stringify(e));
147
+ return HttpResult.fail(500, e.toString()).toResponse();
148
+ }
149
+ }
150
+ }
4
151
  /**
5
152
  * Singleton instance containing the cidl, constructor registry, and wasm binary.
6
153
  * These values are guaranteed to never change throughout a workers lifetime.
@@ -25,97 +172,29 @@ export class RuntimeContainer {
25
172
  return this.instance;
26
173
  }
27
174
  }
28
- /**
29
- * Runtime entry point. Given a request, undergoes: routing, validating,
30
- * hydrating, and method dispatch.
31
- *
32
- * @returns A Response with an `HttpResult` JSON body.
33
- */
34
- export async function cloesce(request, env, ast, app, constructorRegistry, envMeta, apiRoute) {
35
- //#region Initialization
36
- const ir = new Map();
37
- ir.set(envMeta.envName, env);
38
- ir.set("Request", request);
39
- await RuntimeContainer.init(ast, constructorRegistry);
40
- const d1 = env[envMeta.dbName]; // TODO: multiple dbs
41
- //#endregion
42
- //#region Global Middleware
43
- for (const m of app.global) {
44
- const res = await m(request, env, ir);
45
- if (res) {
46
- return toResponse(res);
47
- }
48
- }
49
- //#endregion
50
- //#region Match the route to a model method
51
- const route = matchRoute(request, ast, apiRoute);
52
- if (!route.ok) {
53
- return toResponse(route.value);
54
- }
55
- const { method, model, id } = route.value;
56
- //#endregion
57
- //#region Model Middleware
58
- for (const m of app.model.get(model.name) ?? []) {
59
- const res = await m(request, env, ir);
60
- if (res) {
61
- return toResponse(res);
62
- }
63
- }
64
- //#endregion
65
- //#region Validate request body to the model method
66
- const validation = await validateRequest(request, ast, model, method, id);
67
- if (!validation.ok) {
68
- return toResponse(validation.value);
69
- }
70
- const { params, dataSource } = validation.value;
71
- //#endregion
72
- //#region Method Middleware
73
- for (const m of app.method.get(model.name)?.get(method.name) ?? []) {
74
- const res = await m(request, env, ir);
75
- if (res) {
76
- return toResponse(res);
77
- }
78
- }
79
- //#endregion
80
- //#region Instantatiate the model
81
- const crudCtx = await (async () => {
82
- if (method.is_static) {
83
- return right(CrudContext.fromCtor(d1, constructorRegistry[model.name]));
84
- }
85
- const hydratedModel = await hydrateModel(constructorRegistry, d1, model, id, // id must exist after matchRoute
86
- dataSource);
87
- if (!hydratedModel.ok) {
88
- return hydratedModel;
89
- }
90
- return right(CrudContext.fromInstance(d1, hydratedModel.value, constructorRegistry[model.name]));
91
- })();
92
- if (!crudCtx.ok) {
93
- return toResponse(crudCtx.value);
94
- }
95
- //#endregion
96
- return toResponse(await methodDispatch(crudCtx.value, ir, method, params));
97
- }
98
175
  /**
99
176
  * Matches a request to a method on a model.
100
177
  * @param apiRoute The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
101
178
  * @returns 404 or a `MatchedRoute`
102
179
  */
103
- function matchRoute(request, ast, apiRoute) {
180
+ function matchRoute(request, ast, routePrefix) {
104
181
  const url = new URL(request.url);
182
+ const parts = url.pathname.split("/").filter(Boolean);
183
+ const prefix = routePrefix.split("/").filter(Boolean);
105
184
  // Error state: We expect an exact request format, and expect that the model
106
185
  // and are apart of the CIDL
107
- const notFound = (e) => left(errorState(404, `Path not found: ${e} ${url.pathname}`));
108
- const routeParts = url.pathname
109
- .slice(apiRoute.length)
110
- .split("/")
111
- .filter(Boolean);
112
- if (routeParts.length < 2) {
186
+ const notFound = (e) => Either.left(HttpResult.fail(404, `Path not found: ${e} ${url.pathname}`));
187
+ for (const p of prefix) {
188
+ if (parts.shift() !== p)
189
+ return notFound(`Missing prefix segment "${p}"`);
190
+ }
191
+ if (parts.length < 2) {
113
192
  return notFound("Expected /model/method or /model/:id/method");
114
193
  }
115
194
  // Attempt to extract from routeParts
116
- const modelName = routeParts[0];
117
- const methodName = routeParts[routeParts.length - 1];
118
- const id = routeParts.length === 3 ? routeParts[1] : null;
195
+ const modelName = parts[0];
196
+ const methodName = parts[parts.length - 1];
197
+ const id = parts.length === 3 ? parts[1] : null;
119
198
  const model = ast.models[modelName];
120
199
  if (!model) {
121
200
  return notFound(`Unknown model ${modelName}`);
@@ -127,7 +206,7 @@ function matchRoute(request, ast, apiRoute) {
127
206
  if (request.method !== method.http_verb) {
128
207
  return notFound("Unmatched HTTP method");
129
208
  }
130
- return right({
209
+ return Either.right({
131
210
  model,
132
211
  method,
133
212
  id,
@@ -140,7 +219,7 @@ function matchRoute(request, ast, apiRoute) {
140
219
  */
141
220
  async function validateRequest(request, ast, model, method, id) {
142
221
  // Error state: any missing parameter, body, or malformed input will exit with 400.
143
- const invalidRequest = (e) => left(errorState(400, `Invalid Request Body: ${e}`));
222
+ const invalidRequest = (e) => Either.left(HttpResult.fail(400, `Invalid Request Body: ${e}`));
144
223
  if (!method.is_static && id == null) {
145
224
  return invalidRequest("Id's are required for instantiated methods.");
146
225
  }
@@ -183,7 +262,7 @@ async function validateRequest(request, ast, model, method, id) {
183
262
  !(dataSource in model.data_sources)) {
184
263
  return invalidRequest(`Unknown data source ${dataSource}`);
185
264
  }
186
- return right({ params, dataSource });
265
+ return Either.right({ params, dataSource });
187
266
  }
188
267
  /**
189
268
  * Queries D1 for a particular model's ID, then transforms the SQL column output into
@@ -195,17 +274,19 @@ async function validateRequest(request, ast, model, method, id) {
195
274
  async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
196
275
  // Error state: If the D1 database has been tweaked outside of Cloesce
197
276
  // resulting in a malformed query, exit with a 500.
198
- const malformedQuery = (e) => left(errorState(500, `Error in hydration query, is the database out of sync with the backend?: ${e instanceof Error ? e.message : String(e)}`));
277
+ const malformedQuery = (e) => Either.left(HttpResult.fail(500, `Error in hydration query, is the database out of sync with the backend?: ${e instanceof Error ? e.message : String(e)}`));
199
278
  // Error state: If no record is found for the id, return a 404
200
- const missingRecord = left(errorState(404, "Record not found"));
201
- const pk = model.primary_key.name;
202
- const query = dataSource !== NO_DATA_SOURCE
203
- ? `SELECT * FROM "${model.name}.${dataSource}" WHERE "${model.name}.${pk}" = ?`
204
- : `SELECT * FROM "${model.name}" WHERE "${pk}" = ?`;
279
+ const missingRecord = Either.left(HttpResult.fail(404, "Record not found"));
205
280
  // Query DB
206
281
  let records;
207
282
  try {
208
- records = await d1.prepare(query).bind(id).run();
283
+ let includeTree = dataSource === NO_DATA_SOURCE
284
+ ? null
285
+ : constructorRegistry[model.name][dataSource];
286
+ records = await d1
287
+ .prepare(Orm.getQuery(constructorRegistry[model.name], includeTree))
288
+ .bind(id)
289
+ .run();
209
290
  if (!records?.results) {
210
291
  return missingRecord;
211
292
  }
@@ -217,8 +298,8 @@ async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
217
298
  return malformedQuery(e);
218
299
  }
219
300
  // Hydrate
220
- const models = fromSql(constructorRegistry[model.name], records.results, model.data_sources[dataSource]?.tree ?? {}).value;
221
- return right(models[0]);
301
+ const models = mapSql(constructorRegistry[model.name], records.results, model.data_sources[dataSource]?.tree ?? {}).value;
302
+ return Either.right(models[0]);
222
303
  }
223
304
  /**
224
305
  * Calls a method on a model given a list of parameters.
@@ -226,7 +307,7 @@ async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
226
307
  */
227
308
  async function methodDispatch(crudCtx, instanceRegistry, method, params) {
228
309
  // Error state: Client code ran into an uncaught exception.
229
- const uncaughtException = (e) => errorState(500, `Uncaught exception in method dispatch: ${e instanceof Error ? e.message : String(e)}`);
310
+ const uncaughtException = (e) => HttpResult.fail(500, `Uncaught exception in method dispatch: ${e instanceof Error ? e.message : String(e)}`);
230
311
  const paramArray = [];
231
312
  for (const param of method.parameters) {
232
313
  if (params[param.name]) {
@@ -238,7 +319,7 @@ async function methodDispatch(crudCtx, instanceRegistry, method, params) {
238
319
  if (!injected) {
239
320
  // Error state: Injected parameters cannot be found at compile time, only at runtime.
240
321
  // If a injected reference does not exist, throw a 500.
241
- return errorState(500, `An injected parameter was missing from the instance registry: ${JSON.stringify(param.cidl_type)}`);
322
+ return HttpResult.fail(500, `An injected parameter was missing from the instance registry: ${JSON.stringify(param.cidl_type)}`);
242
323
  }
243
324
  paramArray.push(injected);
244
325
  }
@@ -246,12 +327,12 @@ async function methodDispatch(crudCtx, instanceRegistry, method, params) {
246
327
  const resultWrapper = (res) => {
247
328
  const rt = method.return_type;
248
329
  if (rt === null) {
249
- return { ok: true, status: 200 };
330
+ return HttpResult.ok(200);
250
331
  }
251
332
  if (typeof rt === "object" && rt !== null && "HttpResult" in rt) {
252
333
  return res;
253
334
  }
254
- return { ok: true, status: 200, data: res };
335
+ return HttpResult.ok(200, res);
255
336
  };
256
337
  try {
257
338
  const res = await crudCtx.interceptCrud(method.name)(...paramArray);
@@ -261,6 +342,11 @@ async function methodDispatch(crudCtx, instanceRegistry, method, params) {
261
342
  return uncaughtException(e);
262
343
  }
263
344
  }
345
+ /**
346
+ * Runtime type validation for CIDL types.
347
+ *
348
+ * Returns true if the value matches the CIDL type, false otherwise.
349
+ */
264
350
  function validateCidlType(ast, value, cidlType, isPartial) {
265
351
  if (value === undefined)
266
352
  return isPartial;
@@ -281,8 +367,11 @@ function validateCidlType(ast, value, cidlType, isPartial) {
281
367
  return !Number.isNaN(Number(value));
282
368
  case "Text":
283
369
  return typeof value === "string";
284
- case "Blob":
285
- return value instanceof Blob || value instanceof ArrayBuffer;
370
+ case "Boolean":
371
+ return typeof value === "boolean";
372
+ case "DateIso":
373
+ const date = new Date(value);
374
+ return !isNaN(date.getTime());
286
375
  default:
287
376
  return false;
288
377
  }
@@ -323,6 +412,7 @@ function validateCidlType(ast, value, cidlType, isPartial) {
323
412
  if (!poo.attributes.every((attr) => validateCidlType(ast, valueObj[attr.name], attr.cidl_type, isPartial))) {
324
413
  return false;
325
414
  }
415
+ return true;
326
416
  }
327
417
  if ("Array" in cidlType) {
328
418
  const arr = cidlType.Array;
@@ -338,15 +428,6 @@ function validateCidlType(ast, value, cidlType, isPartial) {
338
428
  }
339
429
  return false;
340
430
  }
341
- function errorState(status, message) {
342
- return { ok: false, status, message };
343
- }
344
- function toResponse(r) {
345
- return new Response(JSON.stringify(r), {
346
- status: r.status,
347
- headers: { "Content-Type": "application/json" },
348
- });
349
- }
350
431
  /**
351
432
  * For testing purposes
352
433
  */
@@ -10,8 +10,9 @@ export interface OrmWasmExports {
10
10
  set_meta_ptr(ptr: number, len: number): number;
11
11
  alloc(len: number): number;
12
12
  dealloc(ptr: number, len: number): void;
13
- object_relational_mapping(model_name_ptr: number, model_name_len: number, sql_rows_ptr: number, sql_rows_len: number, include_tree_ptr: number, include_tree_len: number): boolean;
13
+ map_sql(model_name_ptr: number, model_name_len: number, sql_rows_ptr: number, sql_rows_len: number, include_tree_ptr: number, include_tree_len: number): boolean;
14
14
  upsert_model(model_name_ptr: number, model_name_len: number, new_model_ptr: number, new_model_len: number, include_tree_ptr: number, include_tree_len: number): boolean;
15
+ list_models(model_name_ptr: number, model_name_len: number, include_tree_ptr: number, include_tree_len: number, tag_cte_ptr: number, tag_cte_len: number, custom_from_ptr: number, custom_from_len: number): boolean;
15
16
  }
16
17
  /**
17
18
  * RAII for wasm memory
@@ -28,10 +29,16 @@ export declare class WasmResource {
28
29
  static fromString(str: string, wasm: OrmWasmExports): WasmResource;
29
30
  }
30
31
  export declare function loadOrmWasm(ast: CloesceAst, wasm?: WebAssembly.Instance): Promise<OrmWasmExports>;
31
- export declare function invokeOrmWasm<T>(fn: (...args: number[]) => boolean, args: WasmResource[], wasm: OrmWasmExports): Either<string, T>;
32
+ /**
33
+ * Invokes a WASM ORM function with the provided arguments, handling memory
34
+ * allocation and deallocation.
35
+ *
36
+ * Returns an Either where Left is an error message and Right the raw string result.
37
+ */
38
+ export declare function invokeOrmWasm(fn: (...args: number[]) => boolean, args: WasmResource[], wasm: OrmWasmExports): Either<string, string>;
32
39
  /**
33
40
  * Calls `object_relational_mapping` to turn a row of SQL records into
34
41
  * an instantiated object.
35
42
  */
36
- export declare function fromSql<T extends object>(ctor: new () => T, records: Record<string, any>[], includeTree: IncludeTree<T> | CidlIncludeTree | null): Either<string, T[]>;
43
+ export declare function mapSql<T extends object>(ctor: new () => T, records: Record<string, any>[], includeTree: IncludeTree<T> | CidlIncludeTree | null): Either<string, T[]>;
37
44
  //# sourceMappingURL=wasm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wasm.d.ts","sourceRoot":"","sources":["../../src/router/wasm.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,UAAU,EACV,MAAM,EAIP,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAM/C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;IAC3B,cAAc,IAAI,MAAM,CAAC;IACzB,cAAc,IAAI,MAAM,CAAC;IACzB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAExC,yBAAyB,CACvB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC;IAEX,YAAY,CACV,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,EACxB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC;CACZ;AAED;;GAEG;AACH,qBAAa,YAAY;IAErB,OAAO,CAAC,IAAI;IACL,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;gBAFV,IAAI,EAAE,cAAc,EACrB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM;IAEpB,IAAI;IAIJ;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,YAAY;CAQnE;AAED,wBAAsB,WAAW,CAC/B,GAAG,EAAE,UAAU,EACf,IAAI,CAAC,EAAE,WAAW,CAAC,QAAQ,GAC1B,OAAO,CAAC,cAAc,CAAC,CAmBzB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAC7B,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,EAClC,IAAI,EAAE,YAAY,EAAE,EACpB,IAAI,EAAE,cAAc,GACnB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAkBnB;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EACtC,IAAI,EAAE,UAAU,CAAC,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EAC9B,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,eAAe,GAAG,IAAI,GACnD,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CA0DrB"}
1
+ {"version":3,"file":"wasm.d.ts","sourceRoot":"","sources":["../../src/router/wasm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAS,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAM/C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;IAC3B,cAAc,IAAI,MAAM,CAAC;IACzB,cAAc,IAAI,MAAM,CAAC;IACzB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAExC,OAAO,CACL,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC;IAEX,YAAY,CACV,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,EACxB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC;IAEX,WAAW,CACT,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,EACtB,gBAAgB,EAAE,MAAM,EACxB,gBAAgB,EAAE,MAAM,EACxB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC;CACZ;AAED;;GAEG;AACH,qBAAa,YAAY;IAErB,OAAO,CAAC,IAAI;IACL,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;gBAFV,IAAI,EAAE,cAAc,EACrB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM;IAEpB,IAAI;IAIJ;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,YAAY;CAQnE;AAED,wBAAsB,WAAW,CAC/B,GAAG,EAAE,UAAU,EACf,IAAI,CAAC,EAAE,WAAW,CAAC,QAAQ,GAC1B,OAAO,CAAC,cAAc,CAAC,CAmBzB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,EAClC,IAAI,EAAE,YAAY,EAAE,EACpB,IAAI,EAAE,cAAc,GACnB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkBxB;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,EACrC,IAAI,EAAE,UAAU,CAAC,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EAC9B,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,eAAe,GAAG,IAAI,GACnD,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAsDrB"}