cloesce 0.0.3 → 0.0.4-unstable.0

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.
Files changed (44) hide show
  1. package/dist/README.md +487 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +221 -254
  4. package/dist/common.d.ts +69 -1
  5. package/dist/common.d.ts.map +1 -1
  6. package/dist/common.js +72 -11
  7. package/dist/{extract.d.ts → extractor/extract.d.ts} +5 -2
  8. package/dist/extractor/extract.d.ts.map +1 -0
  9. package/dist/{extract.js → extractor/extract.js} +242 -43
  10. package/dist/generator.wasm +0 -0
  11. package/dist/orm.wasm +0 -0
  12. package/dist/router/crud.d.ts +22 -0
  13. package/dist/router/crud.d.ts.map +1 -0
  14. package/dist/router/crud.js +65 -0
  15. package/dist/router/router.d.ts +77 -0
  16. package/dist/router/router.d.ts.map +1 -0
  17. package/dist/router/router.js +358 -0
  18. package/dist/router/wasm.d.ts +37 -0
  19. package/dist/router/wasm.d.ts.map +1 -0
  20. package/dist/router/wasm.js +98 -0
  21. package/dist/ui/backend.d.ts +124 -0
  22. package/dist/ui/backend.d.ts.map +1 -0
  23. package/dist/ui/backend.js +201 -0
  24. package/dist/ui/client.d.ts +5 -0
  25. package/dist/ui/client.d.ts.map +1 -0
  26. package/dist/ui/client.js +7 -0
  27. package/package.json +70 -58
  28. package/LICENSE +0 -201
  29. package/README.md +0 -23
  30. package/dist/cli.wasm +0 -0
  31. package/dist/cloesce.d.ts +0 -108
  32. package/dist/cloesce.d.ts.map +0 -1
  33. package/dist/cloesce.js +0 -453
  34. package/dist/decorators.d.ts +0 -13
  35. package/dist/decorators.d.ts.map +0 -1
  36. package/dist/decorators.js +0 -13
  37. package/dist/dog.cloesce.js +0 -111
  38. package/dist/extract.d.ts.map +0 -1
  39. package/dist/index.d.ts +0 -24
  40. package/dist/index.d.ts.map +0 -1
  41. package/dist/index.js +0 -24
  42. package/dist/types.d.ts +0 -4
  43. package/dist/types.d.ts.map +0 -1
  44. package/dist/types.js +0 -1
@@ -0,0 +1,77 @@
1
+ import { HttpResult, Either, ModelMethod, CloesceAst, Model, CloesceApp, InstanceRegistry } from "../common.js";
2
+ import { OrmWasmExports } from "./wasm.js";
3
+ import { CrudContext } from "./crud.js";
4
+ /**
5
+ * Map of model names to their respective constructor.
6
+ *
7
+ * The value accepted into the `cloesce` function is generated by the Cloesce compiler, and
8
+ * is guaranteed to contain all model definitions.
9
+ */
10
+ 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
+ * Meta information on the wrangler env and db bindings
18
+ */
19
+ interface MetaWranglerEnv {
20
+ envName: string;
21
+ dbName: string;
22
+ }
23
+ /**
24
+ * Singleton instance containing the cidl, constructor registry, and wasm binary.
25
+ * These values are guaranteed to never change throughout a workers lifetime.
26
+ */
27
+ export declare class RuntimeContainer {
28
+ readonly ast: CloesceAst;
29
+ readonly constructorRegistry: ModelConstructorRegistry;
30
+ readonly wasm: OrmWasmExports;
31
+ private static instance;
32
+ private constructor();
33
+ static init(ast: CloesceAst, constructorRegistry: ModelConstructorRegistry, wasm?: WebAssembly.Instance): Promise<void>;
34
+ static get(): RuntimeContainer;
35
+ }
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
+ /**
44
+ * Matches a request to a method on a model.
45
+ * @param apiRoute The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
46
+ * @returns 404 or a `MatchedRoute`
47
+ */
48
+ declare function matchRoute(request: Request, ast: CloesceAst, apiRoute: string): Either<HttpResult, {
49
+ model: Model;
50
+ method: ModelMethod;
51
+ id: string | null;
52
+ }>;
53
+ /**
54
+ * Validates the request's body/search params against a ModelMethod
55
+ * @returns 400 or a `RequestParamMap` consisting of each parameters name mapped to its value, and
56
+ * a data source
57
+ */
58
+ declare function validateRequest(request: Request, ast: CloesceAst, model: Model, method: ModelMethod, id: string | null): Promise<Either<HttpResult, {
59
+ params: RequestParamMap;
60
+ dataSource: string | null;
61
+ }>>;
62
+ /**
63
+ * Calls a method on a model given a list of parameters.
64
+ * @returns 500 on an uncaught client error, 200 with a result body on success
65
+ */
66
+ declare function methodDispatch(crudCtx: CrudContext, instanceRegistry: InstanceRegistry, method: ModelMethod, params: Record<string, unknown>): Promise<HttpResult<unknown>>;
67
+ /**
68
+ * For testing purposes
69
+ */
70
+ export declare const _cloesceInternal: {
71
+ matchRoute: typeof matchRoute;
72
+ validateRequest: typeof validateRequest;
73
+ methodDispatch: typeof methodDispatch;
74
+ RuntimeContainer: typeof RuntimeContainer;
75
+ };
76
+ export {};
77
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,358 @@
1
+ import { left, right, isNullableType, getNavigationPropertyCidlType, NO_DATA_SOURCE, } from "../common.js";
2
+ import { fromSql, loadOrmWasm } from "./wasm.js";
3
+ import { CrudContext } from "./crud.js";
4
+ /**
5
+ * Singleton instance containing the cidl, constructor registry, and wasm binary.
6
+ * These values are guaranteed to never change throughout a workers lifetime.
7
+ */
8
+ export class RuntimeContainer {
9
+ ast;
10
+ constructorRegistry;
11
+ wasm;
12
+ static instance;
13
+ constructor(ast, constructorRegistry, wasm) {
14
+ this.ast = ast;
15
+ this.constructorRegistry = constructorRegistry;
16
+ this.wasm = wasm;
17
+ }
18
+ static async init(ast, constructorRegistry, wasm) {
19
+ if (this.instance)
20
+ return;
21
+ const wasmAbi = await loadOrmWasm(ast, wasm);
22
+ this.instance = new RuntimeContainer(ast, constructorRegistry, wasmAbi);
23
+ }
24
+ static get() {
25
+ return this.instance;
26
+ }
27
+ }
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
+ /**
99
+ * Matches a request to a method on a model.
100
+ * @param apiRoute The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
101
+ * @returns 404 or a `MatchedRoute`
102
+ */
103
+ function matchRoute(request, ast, apiRoute) {
104
+ const url = new URL(request.url);
105
+ // Error state: We expect an exact request format, and expect that the model
106
+ // 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) {
113
+ return notFound("Expected /model/method or /model/:id/method");
114
+ }
115
+ // 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;
119
+ const model = ast.models[modelName];
120
+ if (!model) {
121
+ return notFound(`Unknown model ${modelName}`);
122
+ }
123
+ const method = model.methods[methodName];
124
+ if (!method) {
125
+ return notFound(`Unknown method ${modelName}.${methodName}`);
126
+ }
127
+ if (request.method !== method.http_verb) {
128
+ return notFound("Unmatched HTTP method");
129
+ }
130
+ return right({
131
+ model,
132
+ method,
133
+ id,
134
+ });
135
+ }
136
+ /**
137
+ * Validates the request's body/search params against a ModelMethod
138
+ * @returns 400 or a `RequestParamMap` consisting of each parameters name mapped to its value, and
139
+ * a data source
140
+ */
141
+ async function validateRequest(request, ast, model, method, id) {
142
+ // Error state: any missing parameter, body, or malformed input will exit with 400.
143
+ const invalidRequest = (e) => left(errorState(400, `Invalid Request Body: ${e}`));
144
+ if (!method.is_static && id == null) {
145
+ return invalidRequest("Id's are required for instantiated methods.");
146
+ }
147
+ // Filter out any injected parameters that will not be passed
148
+ // by the query.
149
+ const requiredParams = method.parameters.filter((p) => !(typeof p.cidl_type === "object" && "Inject" in p.cidl_type));
150
+ // Extract url or body parameters
151
+ const url = new URL(request.url);
152
+ let params = {};
153
+ if (method.http_verb === "GET") {
154
+ params = Object.fromEntries(url.searchParams.entries());
155
+ }
156
+ else {
157
+ try {
158
+ params = await request.json();
159
+ }
160
+ catch {
161
+ return invalidRequest("Could not retrieve JSON body.");
162
+ }
163
+ }
164
+ // Ensure all required params exist
165
+ if (!requiredParams.every((p) => p.name in params)) {
166
+ return invalidRequest(`Missing parameters.`);
167
+ }
168
+ // Validate all parameters type
169
+ for (const p of requiredParams) {
170
+ const value = params[p.name];
171
+ const isPartial = typeof p.cidl_type !== "string" && "Partial" in p.cidl_type;
172
+ if (!validateCidlType(ast, value, p.cidl_type, isPartial)) {
173
+ return invalidRequest("Invalid parameters.");
174
+ }
175
+ }
176
+ // Validate data source if exists
177
+ const dataSourceParam = requiredParams.find((p) => typeof p.cidl_type === "object" && "DataSource" in p.cidl_type);
178
+ const dataSource = dataSourceParam
179
+ ? params[dataSourceParam.name]
180
+ : null;
181
+ if (dataSource &&
182
+ dataSource !== NO_DATA_SOURCE &&
183
+ !(dataSource in model.data_sources)) {
184
+ return invalidRequest(`Unknown data source ${dataSource}`);
185
+ }
186
+ return right({ params, dataSource });
187
+ }
188
+ /**
189
+ * Queries D1 for a particular model's ID, then transforms the SQL column output into
190
+ * an instance of a model using the provided include tree and metadata as a guide.
191
+ * @returns 404 if no record was found for the provided ID
192
+ * @returns 500 if the D1 database is not synced with Cloesce and yields an error
193
+ * @returns The instantiated model on success
194
+ */
195
+ async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
196
+ // Error state: If the D1 database has been tweaked outside of Cloesce
197
+ // 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)}`));
199
+ // 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}" = ?`;
205
+ // Query DB
206
+ let records;
207
+ try {
208
+ records = await d1.prepare(query).bind(id).run();
209
+ if (!records?.results) {
210
+ return missingRecord;
211
+ }
212
+ if (records.error) {
213
+ return malformedQuery(records.error);
214
+ }
215
+ }
216
+ catch (e) {
217
+ return malformedQuery(e);
218
+ }
219
+ // Hydrate
220
+ const models = fromSql(constructorRegistry[model.name], records.results, model.data_sources[dataSource]?.tree ?? {}).value;
221
+ return right(models[0]);
222
+ }
223
+ /**
224
+ * Calls a method on a model given a list of parameters.
225
+ * @returns 500 on an uncaught client error, 200 with a result body on success
226
+ */
227
+ async function methodDispatch(crudCtx, instanceRegistry, method, params) {
228
+ // 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)}`);
230
+ const paramArray = [];
231
+ for (const param of method.parameters) {
232
+ if (params[param.name]) {
233
+ paramArray.push(params[param.name]);
234
+ continue;
235
+ }
236
+ // Injected type
237
+ const injected = instanceRegistry.get(param.cidl_type["Inject"]);
238
+ if (!injected) {
239
+ // Error state: Injected parameters cannot be found at compile time, only at runtime.
240
+ // 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)}`);
242
+ }
243
+ paramArray.push(injected);
244
+ }
245
+ // Ensure the result is always some HttpResult
246
+ const resultWrapper = (res) => {
247
+ const rt = method.return_type;
248
+ if (rt === null) {
249
+ return { ok: true, status: 200 };
250
+ }
251
+ if (typeof rt === "object" && rt !== null && "HttpResult" in rt) {
252
+ return res;
253
+ }
254
+ return { ok: true, status: 200, data: res };
255
+ };
256
+ try {
257
+ const res = await crudCtx.interceptCrud(method.name)(...paramArray);
258
+ return resultWrapper(res);
259
+ }
260
+ catch (e) {
261
+ return uncaughtException(e);
262
+ }
263
+ }
264
+ function validateCidlType(ast, value, cidlType, isPartial) {
265
+ if (value === undefined)
266
+ return isPartial;
267
+ // TODO: consequences of null checking like this? 'null' is passed in
268
+ // as a string for GET requests
269
+ const nullable = isNullableType(cidlType);
270
+ if (value == null || value === "null")
271
+ return nullable;
272
+ if (nullable) {
273
+ cidlType = cidlType.Nullable; // Unwrap the nullable type
274
+ }
275
+ // Handle primitive string types with switch
276
+ if (typeof cidlType === "string") {
277
+ switch (cidlType) {
278
+ case "Integer":
279
+ return Number.isInteger(Number(value));
280
+ case "Real":
281
+ return !Number.isNaN(Number(value));
282
+ case "Text":
283
+ return typeof value === "string";
284
+ case "Blob":
285
+ return value instanceof Blob || value instanceof ArrayBuffer;
286
+ default:
287
+ return false;
288
+ }
289
+ }
290
+ // Handle Data Sources
291
+ if ("DataSource" in cidlType) {
292
+ return typeof value === "string";
293
+ }
294
+ // Handle Models
295
+ let cidlTypeAccessor = "Partial" in cidlType
296
+ ? cidlType.Partial
297
+ : "Object" in cidlType
298
+ ? cidlType.Object
299
+ : undefined;
300
+ if (cidlTypeAccessor && ast.models[cidlTypeAccessor]) {
301
+ const model = ast.models[cidlTypeAccessor];
302
+ if (!model || typeof value !== "object")
303
+ return false;
304
+ const valueObj = value;
305
+ // Validate attributes
306
+ if (!model.attributes.every((attr) => validateCidlType(ast, valueObj[attr.value.name], attr.value.cidl_type, isPartial))) {
307
+ return false;
308
+ }
309
+ // Validate navigation properties
310
+ return model.navigation_properties.every((nav) => {
311
+ const navValue = valueObj[nav.var_name];
312
+ return (navValue == null ||
313
+ validateCidlType(ast, navValue, getNavigationPropertyCidlType(nav), isPartial));
314
+ });
315
+ }
316
+ // Handle Plain Old Objects
317
+ if (cidlTypeAccessor && ast.poos[cidlTypeAccessor]) {
318
+ const poo = ast.poos[cidlTypeAccessor];
319
+ if (!poo || typeof value !== "object")
320
+ return false;
321
+ const valueObj = value;
322
+ // Validate attributes
323
+ if (!poo.attributes.every((attr) => validateCidlType(ast, valueObj[attr.name], attr.cidl_type, isPartial))) {
324
+ return false;
325
+ }
326
+ }
327
+ if ("Array" in cidlType) {
328
+ const arr = cidlType.Array;
329
+ return (Array.isArray(value) &&
330
+ value.every((v) => validateCidlType(ast, v, arr, isPartial)));
331
+ }
332
+ if ("HttpResult" in cidlType) {
333
+ if (value === null)
334
+ return cidlType.HttpResult === null;
335
+ if (cidlType.HttpResult === null)
336
+ return false;
337
+ return validateCidlType(ast, value, cidlType.HttpResult, isPartial);
338
+ }
339
+ return false;
340
+ }
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
+ /**
351
+ * For testing purposes
352
+ */
353
+ export const _cloesceInternal = {
354
+ matchRoute,
355
+ validateRequest,
356
+ methodDispatch,
357
+ RuntimeContainer,
358
+ };
@@ -0,0 +1,37 @@
1
+ import { CidlIncludeTree, CloesceAst, Either } from "../common.js";
2
+ import { IncludeTree } from "../ui/backend.js";
3
+ /**
4
+ * WASM ABI
5
+ */
6
+ export interface OrmWasmExports {
7
+ memory: WebAssembly.Memory;
8
+ get_return_len(): number;
9
+ get_return_ptr(): number;
10
+ set_meta_ptr(ptr: number, len: number): number;
11
+ alloc(len: number): number;
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;
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
+ }
16
+ /**
17
+ * RAII for wasm memory
18
+ */
19
+ export declare class WasmResource {
20
+ private wasm;
21
+ ptr: number;
22
+ len: number;
23
+ constructor(wasm: OrmWasmExports, ptr: number, len: number);
24
+ free(): void;
25
+ /**
26
+ * Copies a value from TS memory to WASM memory. A subsequent `free` is necessary.
27
+ */
28
+ static fromString(str: string, wasm: OrmWasmExports): WasmResource;
29
+ }
30
+ 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
+ * Calls `object_relational_mapping` to turn a row of SQL records into
34
+ * an instantiated object.
35
+ */
36
+ export declare function fromSql<T extends object>(ctor: new () => T, records: Record<string, any>[], includeTree: IncludeTree<T> | CidlIncludeTree | null): Either<string, T[]>;
37
+ //# sourceMappingURL=wasm.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,98 @@
1
+ import { left, right, } from "../common.js";
2
+ import { RuntimeContainer } from "./router.js";
3
+ // Requires the ORM binary to have been built
4
+ import mod from "../orm.wasm";
5
+ /**
6
+ * RAII for wasm memory
7
+ */
8
+ export class WasmResource {
9
+ wasm;
10
+ ptr;
11
+ len;
12
+ constructor(wasm, ptr, len) {
13
+ this.wasm = wasm;
14
+ this.ptr = ptr;
15
+ this.len = len;
16
+ }
17
+ free() {
18
+ this.wasm.dealloc(this.ptr, this.len);
19
+ }
20
+ /**
21
+ * Copies a value from TS memory to WASM memory. A subsequent `free` is necessary.
22
+ */
23
+ static fromString(str, wasm) {
24
+ const encoder = new TextEncoder();
25
+ const bytes = encoder.encode(str);
26
+ const ptr = wasm.alloc(bytes.length);
27
+ const mem = new Uint8Array(wasm.memory.buffer, ptr, bytes.length);
28
+ mem.set(bytes);
29
+ return new this(wasm, ptr, bytes.length);
30
+ }
31
+ }
32
+ export async function loadOrmWasm(ast, wasm) {
33
+ // Load WASM
34
+ const wasmInstance = (wasm ??
35
+ (await WebAssembly.instantiate(mod)));
36
+ const modelMeta = WasmResource.fromString(JSON.stringify(ast.models), wasmInstance.exports);
37
+ if (wasmInstance.exports.set_meta_ptr(modelMeta.ptr, modelMeta.len) != 0) {
38
+ modelMeta.free();
39
+ throw Error("The WASM Module failed to load due to an invalid CIDL");
40
+ }
41
+ // Intentionally leak `modelMeta`, it should exist for the programs lifetime.
42
+ return wasmInstance.exports;
43
+ }
44
+ export function invokeOrmWasm(fn, args, wasm) {
45
+ let resPtr;
46
+ let resLen;
47
+ try {
48
+ const failed = fn(...args.flatMap((a) => [a.ptr, a.len]));
49
+ resPtr = wasm.get_return_ptr();
50
+ resLen = wasm.get_return_len();
51
+ const result = new TextDecoder().decode(new Uint8Array(wasm.memory.buffer, resPtr, resLen));
52
+ return failed ? left(result) : right(result);
53
+ }
54
+ finally {
55
+ args.forEach((a) => a.free());
56
+ if (resPtr && resLen)
57
+ wasm.dealloc(resPtr, resLen);
58
+ }
59
+ }
60
+ /**
61
+ * Calls `object_relational_mapping` to turn a row of SQL records into
62
+ * an instantiated object.
63
+ */
64
+ export function fromSql(ctor, records, includeTree) {
65
+ const { ast, constructorRegistry, wasm } = RuntimeContainer.get();
66
+ const args = [
67
+ WasmResource.fromString(ctor.name, wasm),
68
+ WasmResource.fromString(JSON.stringify(records), wasm),
69
+ WasmResource.fromString(JSON.stringify(includeTree), wasm),
70
+ ];
71
+ const jsonResults = invokeOrmWasm(wasm.object_relational_mapping, args, wasm);
72
+ if (!jsonResults.ok)
73
+ return jsonResults;
74
+ const parsed = JSON.parse(jsonResults.value);
75
+ return right(parsed.map((obj) => instantiateDepthFirst(obj, ast.models[ctor.name], includeTree)));
76
+ function instantiateDepthFirst(m, meta, includeTree) {
77
+ m = Object.assign(new constructorRegistry[meta.name](), m);
78
+ if (!includeTree) {
79
+ return m;
80
+ }
81
+ for (const navProp of meta.navigation_properties) {
82
+ const nestedIncludeTree = includeTree[navProp.var_name];
83
+ if (!nestedIncludeTree)
84
+ continue;
85
+ const nestedMeta = ast.models[navProp.model_name];
86
+ const value = m[navProp.var_name];
87
+ // One to Many, Many to Many
88
+ if (Array.isArray(value)) {
89
+ m[navProp.var_name] = value.map((child) => instantiateDepthFirst(child, nestedMeta, nestedIncludeTree));
90
+ }
91
+ // One to one
92
+ else if (value) {
93
+ m[navProp.var_name] = instantiateDepthFirst(value, nestedMeta, nestedIncludeTree);
94
+ }
95
+ }
96
+ return m;
97
+ }
98
+ }