cloesce 0.0.5-unstable.2 → 0.0.5-unstable.3

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.
@@ -2,246 +2,262 @@ import { mapSql, loadOrmWasm } from "./wasm.js";
2
2
  import { proxyCrud } from "./crud.js";
3
3
  import { HttpResult, Orm } from "../ui/backend.js";
4
4
  import { Either } from "../ui/common.js";
5
- import { NO_DATA_SOURCE, MediaType, } from "../ast.js";
5
+ import { NO_DATA_SOURCE, MediaType } from "../ast.js";
6
6
  import { RuntimeValidator } from "./validator.js";
7
7
  /**
8
8
  * Singleton instance containing the cidl, constructor registry, and wasm binary.
9
9
  * These values are guaranteed to never change throughout a workers lifetime.
10
10
  */
11
11
  export class RuntimeContainer {
12
- ast;
13
- constructorRegistry;
14
- wasm;
15
- static instance;
16
- constructor(ast, constructorRegistry, wasm) {
17
- this.ast = ast;
18
- this.constructorRegistry = constructorRegistry;
19
- this.wasm = wasm;
20
- }
21
- static async init(ast, constructorRegistry, wasm) {
22
- if (this.instance)
23
- return;
24
- const wasmAbi = await loadOrmWasm(ast, wasm);
25
- this.instance = new RuntimeContainer(ast, constructorRegistry, wasmAbi);
26
- }
27
- static get() {
28
- return this.instance;
29
- }
30
- /**
31
- * Disposes the singleton instance. For testing purposes only.
32
- */
33
- static dispose() {
34
- this.instance = undefined;
35
- }
12
+ ast;
13
+ constructorRegistry;
14
+ wasm;
15
+ static instance;
16
+ constructor(ast, constructorRegistry, wasm) {
17
+ this.ast = ast;
18
+ this.constructorRegistry = constructorRegistry;
19
+ this.wasm = wasm;
20
+ }
21
+ static async init(ast, constructorRegistry, wasm) {
22
+ if (this.instance) return;
23
+ const wasmAbi = await loadOrmWasm(ast, wasm);
24
+ this.instance = new RuntimeContainer(ast, constructorRegistry, wasmAbi);
25
+ }
26
+ static get() {
27
+ return this.instance;
28
+ }
29
+ /**
30
+ * Disposes the singleton instance. For testing purposes only.
31
+ */
32
+ static dispose() {
33
+ this.instance = undefined;
34
+ }
36
35
  }
37
36
  /**
38
37
  * Expected states in which the router may exit.
39
38
  */
40
39
  export var RouterError;
41
40
  (function (RouterError) {
42
- RouterError[RouterError["UnknownPrefix"] = 0] = "UnknownPrefix";
43
- RouterError[RouterError["UnknownRoute"] = 1] = "UnknownRoute";
44
- RouterError[RouterError["UnmatchedHttpVerb"] = 2] = "UnmatchedHttpVerb";
45
- RouterError[RouterError["InstantiatedMethodMissingId"] = 3] = "InstantiatedMethodMissingId";
46
- RouterError[RouterError["RequestMissingBody"] = 4] = "RequestMissingBody";
47
- RouterError[RouterError["RequestBodyMissingParameters"] = 5] = "RequestBodyMissingParameters";
48
- RouterError[RouterError["RequestBodyInvalidParameter"] = 6] = "RequestBodyInvalidParameter";
49
- RouterError[RouterError["InstantiatedMethodMissingDataSource"] = 7] = "InstantiatedMethodMissingDataSource";
50
- RouterError[RouterError["MissingDependency"] = 8] = "MissingDependency";
51
- RouterError[RouterError["InvalidDatabaseQuery"] = 9] = "InvalidDatabaseQuery";
52
- RouterError[RouterError["ModelNotFound"] = 10] = "ModelNotFound";
53
- RouterError[RouterError["UncaughtException"] = 11] = "UncaughtException";
41
+ RouterError[(RouterError["UnknownPrefix"] = 0)] = "UnknownPrefix";
42
+ RouterError[(RouterError["UnknownRoute"] = 1)] = "UnknownRoute";
43
+ RouterError[(RouterError["UnmatchedHttpVerb"] = 2)] = "UnmatchedHttpVerb";
44
+ RouterError[(RouterError["InstantiatedMethodMissingId"] = 3)] =
45
+ "InstantiatedMethodMissingId";
46
+ RouterError[(RouterError["RequestMissingBody"] = 4)] = "RequestMissingBody";
47
+ RouterError[(RouterError["RequestBodyMissingParameters"] = 5)] =
48
+ "RequestBodyMissingParameters";
49
+ RouterError[(RouterError["RequestBodyInvalidParameter"] = 6)] =
50
+ "RequestBodyInvalidParameter";
51
+ RouterError[(RouterError["InstantiatedMethodMissingDataSource"] = 7)] =
52
+ "InstantiatedMethodMissingDataSource";
53
+ RouterError[(RouterError["MissingDependency"] = 8)] = "MissingDependency";
54
+ RouterError[(RouterError["InvalidDatabaseQuery"] = 9)] =
55
+ "InvalidDatabaseQuery";
56
+ RouterError[(RouterError["ModelNotFound"] = 10)] = "ModelNotFound";
57
+ RouterError[(RouterError["UncaughtException"] = 11)] = "UncaughtException";
54
58
  })(RouterError || (RouterError = {}));
55
59
  export class CloesceApp {
56
- routePrefix = "api";
57
- globalMiddleware = [];
58
- /**
59
- * Registers global middleware which runs before any route matching.
60
- *
61
- * TODO: Middleware may violate the API contract and return unexpected types
62
- *
63
- * @param m - The middleware function to register.
64
- */
65
- onRequest(m) {
66
- this.globalMiddleware.push(m);
60
+ routePrefix = "api";
61
+ globalMiddleware = [];
62
+ /**
63
+ * Registers global middleware which runs before any route matching.
64
+ *
65
+ * TODO: Middleware may violate the API contract and return unexpected types
66
+ *
67
+ * @param m - The middleware function to register.
68
+ */
69
+ onRequest(m) {
70
+ this.globalMiddleware.push(m);
71
+ }
72
+ resultMiddleware = [];
73
+ /**
74
+ * Registers middleware which runs after the response is generated, but before
75
+ * it is returned to the client.
76
+ *
77
+ * Optionally, return a new HttpResult to short-circuit the response.
78
+ *
79
+ * Errors thrown in response middleware are caught and returned as a 500 response.
80
+ *
81
+ * TODO: Middleware may violate the API contract and return unexpected types
82
+ *
83
+ * @param m - The middleware function to register.
84
+ */
85
+ onResult(m) {
86
+ this.resultMiddleware.push(m);
87
+ }
88
+ namespaceMiddleware = new Map();
89
+ /**
90
+ * Registers middleware for a specific namespace (being, a model or service)
91
+ *
92
+ * Runs before request validation and method middleware.
93
+ *
94
+ * TODO: Middleware may violate the API contract and return unexpected types
95
+ *
96
+ * @typeParam T - The namespace type
97
+ * @param ctor - The namespace's constructor (used to derive its name).
98
+ * @param m - The middleware function to register.
99
+ */
100
+ onNamespace(ctor, m) {
101
+ if (this.namespaceMiddleware.has(ctor.name)) {
102
+ this.namespaceMiddleware.get(ctor.name).push(m);
103
+ } else {
104
+ this.namespaceMiddleware.set(ctor.name, [m]);
67
105
  }
68
- resultMiddleware = [];
69
- /**
70
- * Registers middleware which runs after the response is generated, but before
71
- * it is returned to the client.
72
- *
73
- * Optionally, return a new HttpResult to short-circuit the response.
74
- *
75
- * Errors thrown in response middleware are caught and returned as a 500 response.
76
- *
77
- * TODO: Middleware may violate the API contract and return unexpected types
78
- *
79
- * @param m - The middleware function to register.
80
- */
81
- onResult(m) {
82
- this.resultMiddleware.push(m);
106
+ }
107
+ methodMiddleware = new Map();
108
+ /**
109
+ * Registers middleware for a specific method on a namespace
110
+ *
111
+ * Runs after namespace middleware and request validation.
112
+ *
113
+ * TODO: Middleware may violate the API contract and return unexpected types
114
+ *
115
+ * @typeParam T - The namespace type
116
+ * @param ctor - The namespace constructor
117
+ * @param method - The method name on the namespace.
118
+ * @param m - The middleware function to register.
119
+ */
120
+ onMethod(ctor, method, m) {
121
+ if (!this.methodMiddleware.has(ctor.name)) {
122
+ this.methodMiddleware.set(ctor.name, new Map());
83
123
  }
84
- namespaceMiddleware = new Map();
85
- /**
86
- * Registers middleware for a specific namespace (being, a model or service)
87
- *
88
- * Runs before request validation and method middleware.
89
- *
90
- * TODO: Middleware may violate the API contract and return unexpected types
91
- *
92
- * @typeParam T - The namespace type
93
- * @param ctor - The namespace's constructor (used to derive its name).
94
- * @param m - The middleware function to register.
95
- */
96
- onNamespace(ctor, m) {
97
- if (this.namespaceMiddleware.has(ctor.name)) {
98
- this.namespaceMiddleware.get(ctor.name).push(m);
99
- }
100
- else {
101
- this.namespaceMiddleware.set(ctor.name, [m]);
102
- }
124
+ const methods = this.methodMiddleware.get(ctor.name);
125
+ if (!methods.has(method)) {
126
+ methods.set(method, []);
103
127
  }
104
- methodMiddleware = new Map();
105
- /**
106
- * Registers middleware for a specific method on a namespace
107
- *
108
- * Runs after namespace middleware and request validation.
109
- *
110
- * TODO: Middleware may violate the API contract and return unexpected types
111
- *
112
- * @typeParam T - The namespace type
113
- * @param ctor - The namespace constructor
114
- * @param method - The method name on the namespace.
115
- * @param m - The middleware function to register.
116
- */
117
- onMethod(ctor, method, m) {
118
- if (!this.methodMiddleware.has(ctor.name)) {
119
- this.methodMiddleware.set(ctor.name, new Map());
120
- }
121
- const methods = this.methodMiddleware.get(ctor.name);
122
- if (!methods.has(method)) {
123
- methods.set(method, []);
124
- }
125
- methods.get(method).push(m);
128
+ methods.get(method).push(m);
129
+ }
130
+ async router(request, env, ast, ctorReg, di) {
131
+ // Global middleware
132
+ for (const m of this.globalMiddleware) {
133
+ const res = await m(di);
134
+ if (res) {
135
+ return res;
136
+ }
126
137
  }
127
- async router(request, env, ast, ctorReg, di) {
128
- // Global middleware
129
- for (const m of this.globalMiddleware) {
130
- const res = await m(di);
131
- if (res) {
132
- return res;
133
- }
134
- }
135
- // Route match
136
- const routeRes = matchRoute(request, ast, this.routePrefix);
137
- if (routeRes.isLeft()) {
138
- return routeRes.value;
139
- }
140
- const route = routeRes.unwrap();
141
- // Model middleware
142
- for (const m of this.namespaceMiddleware.get(route.namespace) ?? []) {
143
- const res = await m(di);
144
- if (res) {
145
- return res;
146
- }
147
- }
148
- // Request validation
149
- const validation = await validateRequest(request, ast, ctorReg, route);
150
- if (validation.isLeft()) {
151
- return validation.value;
152
- }
153
- const { params, dataSource } = validation.unwrap();
154
- // Method middleware
155
- for (const m of this.methodMiddleware
156
- .get(route.namespace)
157
- ?.get(route.method.name) ?? []) {
158
- const res = await m(di);
159
- if (res) {
160
- return res;
161
- }
162
- }
163
- // Hydration
164
- const hydrated = await (async () => {
165
- if (route.kind == "model") {
166
- // TODO: Support multiple D1 bindings
167
- // It's been verified by the compiler that wrangler_env exists if a D1 model is present
168
- const d1 = env[ast.wrangler_env.db_binding];
169
- // Proxy CRUD
170
- if (route.method.is_static) {
171
- return Either.right(proxyCrud(ctorReg[route.namespace], ctorReg[route.namespace], d1));
172
- }
173
- return await hydrateModelD1(ctorReg, d1, route.model, route.id, dataSource);
174
- }
175
- // Services
176
- if (route.method.is_static) {
177
- return Either.right(ctorReg[route.namespace]);
178
- }
179
- return Either.right(di.get(route.namespace));
180
- })();
181
- if (hydrated.isLeft()) {
182
- return hydrated.value;
183
- }
184
- // Method dispatch
185
- return await methodDispatch(hydrated.unwrap(), di, route, params);
138
+ // Route match
139
+ const routeRes = matchRoute(request, ast, this.routePrefix);
140
+ if (routeRes.isLeft()) {
141
+ return routeRes.value;
186
142
  }
187
- /**
188
- * Runs the Cloesce app. Intended to be called from the generated workers code.
189
- */
190
- async run(request, env, ast, ctorReg) {
191
- await RuntimeContainer.init(ast, ctorReg);
192
- const di = new Map();
193
- if (ast.wrangler_env) {
194
- di.set(ast.wrangler_env.name, env);
195
- }
196
- di.set("Request", request);
197
- // Note: Services are in topological order
198
- for (const name in ast.services) {
199
- const service = ast.services[name];
200
- for (const attr of service.attributes) {
201
- const injected = di.get(attr.injected);
202
- if (!injected) {
203
- return exit(500, RouterError.MissingDependency, `An injected parameter was missing from the instance registry: ${JSON.stringify(attr.injected)}`)
204
- .unwrapLeft()
205
- .toResponse();
206
- }
207
- service[attr.var_name] = injected;
208
- }
209
- // Inject services
210
- di.set(name, Object.assign(new ctorReg[name](), service));
143
+ const route = routeRes.unwrap();
144
+ // Model middleware
145
+ for (const m of this.namespaceMiddleware.get(route.namespace) ?? []) {
146
+ const res = await m(di);
147
+ if (res) {
148
+ return res;
149
+ }
150
+ }
151
+ // Request validation
152
+ const validation = await validateRequest(request, ast, ctorReg, route);
153
+ if (validation.isLeft()) {
154
+ return validation.value;
155
+ }
156
+ const { params, dataSource } = validation.unwrap();
157
+ // Method middleware
158
+ for (const m of this.methodMiddleware
159
+ .get(route.namespace)
160
+ ?.get(route.method.name) ?? []) {
161
+ const res = await m(di);
162
+ if (res) {
163
+ return res;
164
+ }
165
+ }
166
+ // Hydration
167
+ const hydrated = await (async () => {
168
+ if (route.kind == "model") {
169
+ // TODO: Support multiple D1 bindings
170
+ // It's been verified by the compiler that wrangler_env exists if a D1 model is present
171
+ const d1 = env[ast.wrangler_env.db_binding];
172
+ // Proxy CRUD
173
+ if (route.method.is_static) {
174
+ return Either.right(
175
+ proxyCrud(ctorReg[route.namespace], ctorReg[route.namespace], d1),
176
+ );
211
177
  }
212
- try {
213
- let httpResult = await this.router(request, env, ast, ctorReg, di);
214
- // Response middleware
215
- for (const m of this.resultMiddleware) {
216
- const res = await m(di, httpResult);
217
- if (res) {
218
- return res.toResponse();
219
- }
220
- }
221
- return httpResult.toResponse();
178
+ return await hydrateModelD1(
179
+ ctorReg,
180
+ d1,
181
+ route.model,
182
+ route.id,
183
+ dataSource,
184
+ );
185
+ }
186
+ // Services
187
+ if (route.method.is_static) {
188
+ return Either.right(ctorReg[route.namespace]);
189
+ }
190
+ return Either.right(di.get(route.namespace));
191
+ })();
192
+ if (hydrated.isLeft()) {
193
+ return hydrated.value;
194
+ }
195
+ // Method dispatch
196
+ return await methodDispatch(hydrated.unwrap(), di, route, params);
197
+ }
198
+ /**
199
+ * Runs the Cloesce app. Intended to be called from the generated workers code.
200
+ */
201
+ async run(request, env, ast, ctorReg) {
202
+ await RuntimeContainer.init(ast, ctorReg);
203
+ const di = new Map();
204
+ if (ast.wrangler_env) {
205
+ di.set(ast.wrangler_env.name, env);
206
+ }
207
+ di.set("Request", request);
208
+ // Note: Services are in topological order
209
+ for (const name in ast.services) {
210
+ const service = ast.services[name];
211
+ for (const attr of service.attributes) {
212
+ const injected = di.get(attr.injected);
213
+ if (!injected) {
214
+ return exit(
215
+ 500,
216
+ RouterError.MissingDependency,
217
+ `An injected parameter was missing from the instance registry: ${JSON.stringify(attr.injected)}`,
218
+ )
219
+ .unwrapLeft()
220
+ .toResponse();
222
221
  }
223
- catch (e) {
224
- let debug;
225
- if (e instanceof Error) {
226
- debug = {
227
- name: e.name,
228
- message: e.message,
229
- stack: e.stack,
230
- cause: e.cause,
231
- };
232
- }
233
- else {
234
- debug = {
235
- name: "NonErrorThrown",
236
- message: typeof e === "string" ? e : JSON.stringify(e),
237
- stack: undefined,
238
- };
239
- }
240
- const res = HttpResult.fail(500, JSON.stringify(debug));
241
- console.error("An uncaught error occurred in the Cloesce Router: ", debug);
242
- return res.toResponse();
222
+ service[attr.var_name] = injected;
223
+ }
224
+ // Inject services
225
+ di.set(name, Object.assign(new ctorReg[name](), service));
226
+ }
227
+ try {
228
+ let httpResult = await this.router(request, env, ast, ctorReg, di);
229
+ // Response middleware
230
+ for (const m of this.resultMiddleware) {
231
+ const res = await m(di, httpResult);
232
+ if (res) {
233
+ return res.toResponse();
243
234
  }
235
+ }
236
+ return httpResult.toResponse();
237
+ } catch (e) {
238
+ let debug;
239
+ if (e instanceof Error) {
240
+ debug = {
241
+ name: e.name,
242
+ message: e.message,
243
+ stack: e.stack,
244
+ cause: e.cause,
245
+ };
246
+ } else {
247
+ debug = {
248
+ name: "NonErrorThrown",
249
+ message: typeof e === "string" ? e : JSON.stringify(e),
250
+ stack: undefined,
251
+ };
252
+ }
253
+ const res = HttpResult.fail(500, JSON.stringify(debug));
254
+ console.error(
255
+ "An uncaught error occurred in the Cloesce Router: ",
256
+ debug,
257
+ );
258
+ return res.toResponse();
244
259
  }
260
+ }
245
261
  }
246
262
  /**
247
263
  * Matches a request to an ApiInvocation
@@ -249,57 +265,54 @@ export class CloesceApp {
249
265
  * @returns 404 or a matched route.
250
266
  */
251
267
  function matchRoute(request, ast, routePrefix) {
252
- const url = new URL(request.url);
253
- const parts = url.pathname.split("/").filter(Boolean);
254
- const prefix = routePrefix.split("/").filter(Boolean);
255
- // Error state: We expect an exact request format, and expect that the model
256
- // and are apart of the CIDL
257
- const notFound = (c) => exit(404, c, "Unknown route");
258
- for (const p of prefix) {
259
- if (parts.shift() !== p)
260
- return notFound(RouterError.UnknownPrefix);
261
- }
262
- if (parts.length < 2) {
263
- return notFound(RouterError.UnknownPrefix);
268
+ const url = new URL(request.url);
269
+ const parts = url.pathname.split("/").filter(Boolean);
270
+ const prefix = routePrefix.split("/").filter(Boolean);
271
+ // Error state: We expect an exact request format, and expect that the model
272
+ // and are apart of the CIDL
273
+ const notFound = (c) => exit(404, c, "Unknown route");
274
+ for (const p of prefix) {
275
+ if (parts.shift() !== p) return notFound(RouterError.UnknownPrefix);
276
+ }
277
+ if (parts.length < 2) {
278
+ return notFound(RouterError.UnknownPrefix);
279
+ }
280
+ // Extract pattern
281
+ const namespace = parts[0];
282
+ const methodName = parts[parts.length - 1];
283
+ const id = parts.length === 3 ? parts[1] : null;
284
+ const model = ast.models[namespace];
285
+ if (model) {
286
+ const method = model.methods[methodName];
287
+ if (!method) return notFound(RouterError.UnknownRoute);
288
+ if (request.method !== method.http_verb) {
289
+ return notFound(RouterError.UnmatchedHttpVerb);
264
290
  }
265
- // Extract pattern
266
- const namespace = parts[0];
267
- const methodName = parts[parts.length - 1];
268
- const id = parts.length === 3 ? parts[1] : null;
269
- const model = ast.models[namespace];
270
- if (model) {
271
- const method = model.methods[methodName];
272
- if (!method)
273
- return notFound(RouterError.UnknownRoute);
274
- if (request.method !== method.http_verb) {
275
- return notFound(RouterError.UnmatchedHttpVerb);
276
- }
277
- return Either.right({
278
- kind: "model",
279
- namespace,
280
- method,
281
- model,
282
- id,
283
- });
284
- }
285
- const service = ast.services[namespace];
286
- if (service) {
287
- const method = service.methods[methodName];
288
- // Services do not have IDs.
289
- if (!method || id)
290
- return notFound(RouterError.UnknownRoute);
291
- if (request.method !== method.http_verb) {
292
- return notFound(RouterError.UnmatchedHttpVerb);
293
- }
294
- return Either.right({
295
- kind: "service",
296
- namespace,
297
- method,
298
- service,
299
- id: null,
300
- });
291
+ return Either.right({
292
+ kind: "model",
293
+ namespace,
294
+ method,
295
+ model,
296
+ id,
297
+ });
298
+ }
299
+ const service = ast.services[namespace];
300
+ if (service) {
301
+ const method = service.methods[methodName];
302
+ // Services do not have IDs.
303
+ if (!method || id) return notFound(RouterError.UnknownRoute);
304
+ if (request.method !== method.http_verb) {
305
+ return notFound(RouterError.UnmatchedHttpVerb);
301
306
  }
302
- return notFound(RouterError.UnknownRoute);
307
+ return Either.right({
308
+ kind: "service",
309
+ namespace,
310
+ method,
311
+ service,
312
+ id: null,
313
+ });
314
+ }
315
+ return notFound(RouterError.UnknownRoute);
303
316
  }
304
317
  /**
305
318
  * Validates the request's body/search params against a ModelMethod
@@ -307,65 +320,76 @@ function matchRoute(request, ast, routePrefix) {
307
320
  * a data source
308
321
  */
309
322
  async function validateRequest(request, ast, ctorReg, route) {
310
- // Error state: any missing parameter, body, or malformed input will exit with 400.
311
- const invalidRequest = (c) => exit(400, c, "Invalid Request Body");
312
- // Models must have an ID on instantiated methods.
313
- if (route.kind === "model" && !route.method.is_static && route.id == null) {
314
- return invalidRequest(RouterError.InstantiatedMethodMissingId);
315
- }
316
- // Filter out injected parameters
317
- const requiredParams = route.method.parameters.filter((p) => !(typeof p.cidl_type === "object" && "Inject" in p.cidl_type));
318
- const url = new URL(request.url);
319
- let params = Object.fromEntries(url.searchParams.entries());
320
- // Extract all method parameters from the body
321
- if (route.method.http_verb !== "GET") {
322
- try {
323
- switch (route.method.parameters_media) {
324
- case MediaType.Json: {
325
- const body = await request.json();
326
- params = { ...params, ...body };
327
- break;
328
- }
329
- case MediaType.Octet: {
330
- // Octet streams are verified by the Cloesce compiler to have
331
- // one Stream type
332
- const streamParam = requiredParams.find((p) => typeof p.cidl_type === "string" && p.cidl_type === "Stream");
333
- params[streamParam.name] = request.body;
334
- break;
335
- }
336
- default: {
337
- throw new Error("not implemented");
338
- }
339
- }
323
+ // Error state: any missing parameter, body, or malformed input will exit with 400.
324
+ const invalidRequest = (c) => exit(400, c, "Invalid Request Body");
325
+ // Models must have an ID on instantiated methods.
326
+ if (route.kind === "model" && !route.method.is_static && route.id == null) {
327
+ return invalidRequest(RouterError.InstantiatedMethodMissingId);
328
+ }
329
+ // Filter out injected parameters
330
+ const requiredParams = route.method.parameters.filter(
331
+ (p) => !(typeof p.cidl_type === "object" && "Inject" in p.cidl_type),
332
+ );
333
+ const url = new URL(request.url);
334
+ let params = Object.fromEntries(url.searchParams.entries());
335
+ // Extract all method parameters from the body
336
+ if (route.method.http_verb !== "GET") {
337
+ try {
338
+ switch (route.method.parameters_media) {
339
+ case MediaType.Json: {
340
+ const body = await request.json();
341
+ params = { ...params, ...body };
342
+ break;
340
343
  }
341
- catch {
342
- return invalidRequest(RouterError.RequestMissingBody);
344
+ case MediaType.Octet: {
345
+ // Octet streams are verified by the Cloesce compiler to have
346
+ // one Stream type
347
+ const streamParam = requiredParams.find(
348
+ (p) => typeof p.cidl_type === "string" && p.cidl_type === "Stream",
349
+ );
350
+ params[streamParam.name] = request.body;
351
+ break;
343
352
  }
344
- }
345
- if (!requiredParams.every((p) => p.name in params)) {
346
- return invalidRequest(RouterError.RequestBodyMissingParameters);
347
- }
348
- // Validate all parameters type
349
- // Octet streams can be passed as is
350
- if (route.method.parameters_media !== MediaType.Octet) {
351
- for (const p of requiredParams) {
352
- const res = RuntimeValidator.validate(params[p.name], p.cidl_type, ast, ctorReg);
353
- if (res.isLeft()) {
354
- return invalidRequest(RouterError.RequestBodyInvalidParameter);
355
- }
356
- params[p.name] = res.unwrap();
353
+ default: {
354
+ throw new Error("not implemented");
357
355
  }
356
+ }
357
+ } catch {
358
+ return invalidRequest(RouterError.RequestMissingBody);
358
359
  }
359
- // A data source is required for instantiated model methods
360
- const dataSource = requiredParams
361
- .filter((p) => typeof p.cidl_type === "object" &&
362
- "DataSource" in p.cidl_type &&
363
- p.cidl_type.DataSource === route.namespace)
364
- .map((p) => params[p.name])[0];
365
- if (route.kind === "model" && !route.method.is_static && !dataSource) {
366
- return invalidRequest(RouterError.InstantiatedMethodMissingDataSource);
360
+ }
361
+ if (!requiredParams.every((p) => p.name in params)) {
362
+ return invalidRequest(RouterError.RequestBodyMissingParameters);
363
+ }
364
+ // Validate all parameters type
365
+ // Octet streams can be passed as is
366
+ if (route.method.parameters_media !== MediaType.Octet) {
367
+ for (const p of requiredParams) {
368
+ const res = RuntimeValidator.validate(
369
+ params[p.name],
370
+ p.cidl_type,
371
+ ast,
372
+ ctorReg,
373
+ );
374
+ if (res.isLeft()) {
375
+ return invalidRequest(RouterError.RequestBodyInvalidParameter);
376
+ }
377
+ params[p.name] = res.unwrap();
367
378
  }
368
- return Either.right({ params, dataSource: dataSource ?? null });
379
+ }
380
+ // A data source is required for instantiated model methods
381
+ const dataSource = requiredParams
382
+ .filter(
383
+ (p) =>
384
+ typeof p.cidl_type === "object" &&
385
+ "DataSource" in p.cidl_type &&
386
+ p.cidl_type.DataSource === route.namespace,
387
+ )
388
+ .map((p) => params[p.name])[0];
389
+ if (route.kind === "model" && !route.method.is_static && !dataSource) {
390
+ return invalidRequest(RouterError.InstantiatedMethodMissingDataSource);
391
+ }
392
+ return Either.right({ params, dataSource: dataSource ?? null });
369
393
  }
370
394
  /**
371
395
  * Queries D1 for a particular model's ID, then transforms the SQL column output into
@@ -375,82 +399,101 @@ async function validateRequest(request, ast, ctorReg, route) {
375
399
  * @returns The instantiated model on success
376
400
  */
377
401
  async function hydrateModelD1(constructorRegistry, d1, model, id, dataSource) {
378
- // Error state: If some outside force tweaked the database schema, the query may fail.
379
- // Otherwise, this indicates a bug in the compiler or runtime.
380
- const malformedQuery = (e) => exit(500, RouterError.InvalidDatabaseQuery, `Error in hydration query, is the database out of sync with the backend?: ${e instanceof Error ? e.message : String(e)}`);
381
- // Query DB
382
- let records;
383
- try {
384
- let includeTree = dataSource === NO_DATA_SOURCE
385
- ? null
386
- : constructorRegistry[model.name][dataSource];
387
- records = await d1
388
- .prepare(Orm.getQuery(constructorRegistry[model.name], includeTree))
389
- .bind(id)
390
- .run();
391
- // Error state: If no record is found for the id, return a 404
392
- if (!records?.results) {
393
- return exit(404, RouterError.ModelNotFound, "Record not found");
394
- }
395
- if (records.error) {
396
- return malformedQuery(records.error);
397
- }
398
- }
399
- catch (e) {
400
- return malformedQuery(e);
402
+ // Error state: If some outside force tweaked the database schema, the query may fail.
403
+ // Otherwise, this indicates a bug in the compiler or runtime.
404
+ const malformedQuery = (e) =>
405
+ exit(
406
+ 500,
407
+ RouterError.InvalidDatabaseQuery,
408
+ `Error in hydration query, is the database out of sync with the backend?: ${e instanceof Error ? e.message : String(e)}`,
409
+ );
410
+ // Query DB
411
+ let records;
412
+ try {
413
+ let includeTree =
414
+ dataSource === NO_DATA_SOURCE
415
+ ? null
416
+ : constructorRegistry[model.name][dataSource];
417
+ records = await d1
418
+ .prepare(Orm.getQuery(constructorRegistry[model.name], includeTree))
419
+ .bind(id)
420
+ .run();
421
+ // Error state: If no record is found for the id, return a 404
422
+ if (!records?.results) {
423
+ return exit(404, RouterError.ModelNotFound, "Record not found");
401
424
  }
402
- // Hydrate
403
- const models = mapSql(constructorRegistry[model.name], records.results, model.data_sources[dataSource]?.tree ?? {});
404
- if (models.isLeft()) {
405
- return malformedQuery(models.value);
425
+ if (records.error) {
426
+ return malformedQuery(records.error);
406
427
  }
407
- return Either.right(models.unwrap()[0]);
428
+ } catch (e) {
429
+ return malformedQuery(e);
430
+ }
431
+ // Hydrate
432
+ const models = mapSql(
433
+ constructorRegistry[model.name],
434
+ records.results,
435
+ model.data_sources[dataSource]?.tree ?? {},
436
+ );
437
+ if (models.isLeft()) {
438
+ return malformedQuery(models.value);
439
+ }
440
+ return Either.right(models.unwrap()[0]);
408
441
  }
409
442
  /**
410
443
  * Calls a method on a model given a list of parameters.
411
444
  * @returns 500 on an uncaught client error, 200 with a result body on success
412
445
  */
413
446
  async function methodDispatch(obj, di, route, params) {
414
- const paramArray = [];
415
- for (const param of route.method.parameters) {
416
- if (param.name in params) {
417
- paramArray.push(params[param.name]);
418
- continue;
419
- }
420
- // Injected type
421
- const injected = di.get(param.cidl_type["Inject"]);
422
- if (!injected) {
423
- // Error state: Injected parameters cannot be found at compile time, only at runtime.
424
- // If a injected reference does not exist, throw a 500.
425
- return exit(500, RouterError.MissingDependency, `An injected parameter was missing from the instance registry: ${JSON.stringify(param.cidl_type)}`).unwrapLeft();
426
- }
427
- paramArray.push(injected);
428
- }
429
- const wrapResult = (res) => {
430
- const rt = route.method.return_type;
431
- const httpResult = typeof rt === "object" && rt !== null && "HttpResult" in rt
432
- ? res
433
- : HttpResult.ok(200, res);
434
- return httpResult.setMediaType(route.method.return_media);
435
- };
436
- try {
437
- const res = await obj[route.method.name](...paramArray);
438
- return wrapResult(res);
447
+ const paramArray = [];
448
+ for (const param of route.method.parameters) {
449
+ if (param.name in params) {
450
+ paramArray.push(params[param.name]);
451
+ continue;
439
452
  }
440
- catch (e) {
441
- // Error state: Client code threw an uncaught exception.
442
- return exit(500, RouterError.UncaughtException, `Uncaught exception in method dispatch: ${e instanceof Error ? e.message : String(e)}`).unwrapLeft();
453
+ // Injected type
454
+ const injected = di.get(param.cidl_type["Inject"]);
455
+ if (!injected) {
456
+ // Error state: Injected parameters cannot be found at compile time, only at runtime.
457
+ // If a injected reference does not exist, throw a 500.
458
+ return exit(
459
+ 500,
460
+ RouterError.MissingDependency,
461
+ `An injected parameter was missing from the instance registry: ${JSON.stringify(param.cidl_type)}`,
462
+ ).unwrapLeft();
443
463
  }
464
+ paramArray.push(injected);
465
+ }
466
+ const wrapResult = (res) => {
467
+ const rt = route.method.return_type;
468
+ const httpResult =
469
+ typeof rt === "object" && rt !== null && "HttpResult" in rt
470
+ ? res
471
+ : HttpResult.ok(200, res);
472
+ return httpResult.setMediaType(route.method.return_media);
473
+ };
474
+ try {
475
+ const res = await obj[route.method.name](...paramArray);
476
+ return wrapResult(res);
477
+ } catch (e) {
478
+ // Error state: Client code threw an uncaught exception.
479
+ return exit(
480
+ 500,
481
+ RouterError.UncaughtException,
482
+ `Uncaught exception in method dispatch: ${e instanceof Error ? e.message : String(e)}`,
483
+ ).unwrapLeft();
484
+ }
444
485
  }
445
486
  function exit(status, state, message, debugMessage = "") {
446
- return Either.left(HttpResult.fail(status, `${message} (ErrorCode: ${state}${debugMessage})`));
487
+ return Either.left(
488
+ HttpResult.fail(status, `${message} (ErrorCode: ${state}${debugMessage})`),
489
+ );
447
490
  }
448
491
  /**
449
492
  * For testing purposes
450
493
  */
451
494
  export const _cloesceInternal = {
452
- matchRoute,
453
- validateRequest,
454
- methodDispatch,
455
- RuntimeContainer,
495
+ matchRoute,
496
+ validateRequest,
497
+ methodDispatch,
498
+ RuntimeContainer,
456
499
  };