cloesce 0.0.3-fix.6 → 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.
@@ -1,38 +1,11 @@
1
- import { left, right, isNullableType, getNavigationPropertyCidlType, } from "../common.js";
2
- // Requires the rust runtime binary to have been built
3
- import mod from "../runtime.wasm";
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
4
  /**
5
- * RAII for wasm memory
5
+ * Singleton instance containing the cidl, constructor registry, and wasm binary.
6
+ * These values are guaranteed to never change throughout a workers lifetime.
6
7
  */
7
- class WasmResource {
8
- wasm;
9
- ptr;
10
- len;
11
- constructor(wasm, ptr, len) {
12
- this.wasm = wasm;
13
- this.ptr = ptr;
14
- this.len = len;
15
- }
16
- free() {
17
- this.wasm.dealloc(this.ptr, this.len);
18
- }
19
- /**
20
- * Copies a value from TS memory to WASM memory. A subsequent `free` is necessary.
21
- */
22
- static fromString(str, wasm) {
23
- const encoder = new TextEncoder();
24
- const bytes = encoder.encode(str);
25
- const ptr = wasm.alloc(bytes.length);
26
- const mem = new Uint8Array(wasm.memory.buffer, ptr, bytes.length);
27
- mem.set(bytes);
28
- return new this(wasm, ptr, bytes.length);
29
- }
30
- }
31
- /**
32
- * Singleton instances of the cidl, constructor registry, and wasm binary.
33
- * These values are guaranteed to never change throughout a program lifetime.
34
- */
35
- class RuntimeContainer {
8
+ export class RuntimeContainer {
36
9
  ast;
37
10
  constructorRegistry;
38
11
  wasm;
@@ -43,19 +16,10 @@ class RuntimeContainer {
43
16
  this.wasm = wasm;
44
17
  }
45
18
  static async init(ast, constructorRegistry, wasm) {
46
- if (this.instance) {
19
+ if (this.instance)
47
20
  return;
48
- }
49
- // Load WASM
50
- const wasmInstance = (wasm ??
51
- (await WebAssembly.instantiate(mod)));
52
- const modelMeta = WasmResource.fromString(JSON.stringify(ast.models), wasmInstance.exports);
53
- if (wasmInstance.exports.set_meta_ptr(modelMeta.ptr, modelMeta.len) != 0) {
54
- modelMeta.free();
55
- throw Error("The WASM Module failed to load due to an invalid CIDL");
56
- }
57
- // Intentionally leak `modelMeta`, it should exist for the programs lifetime.
58
- this.instance = new RuntimeContainer(ast, constructorRegistry, wasmInstance.exports);
21
+ const wasmAbi = await loadOrmWasm(ast, wasm);
22
+ this.instance = new RuntimeContainer(ast, constructorRegistry, wasmAbi);
59
23
  }
60
24
  static get() {
61
25
  return this.instance;
@@ -67,48 +31,82 @@ class RuntimeContainer {
67
31
  *
68
32
  * @returns A Response with an `HttpResult` JSON body.
69
33
  */
70
- export async function cloesce(request, ast, constructorRegistry, instanceRegistry, envMeta, api_route) {
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);
71
39
  await RuntimeContainer.init(ast, constructorRegistry);
72
- const d1 = instanceRegistry.get(envMeta.envName)[envMeta.dbName];
73
- // Match the route to a model method
74
- const route = matchRoute(request, ast, api_route);
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);
75
52
  if (!route.ok) {
76
53
  return toResponse(route.value);
77
54
  }
78
55
  const { method, model, id } = route.value;
79
- // Validate request body to the model method
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
80
66
  const validation = await validateRequest(request, ast, model, method, id);
81
67
  if (!validation.ok) {
82
68
  return toResponse(validation.value);
83
69
  }
84
70
  const { params, dataSource } = validation.value;
85
- // Instantatiate the model
86
- let instance;
87
- if (method.is_static) {
88
- instance = constructorRegistry[model.name];
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
+ }
89
78
  }
90
- else {
91
- const hydratedModel = await hydrateModel(constructorRegistry, d1, model, id, dataSource);
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);
92
87
  if (!hydratedModel.ok) {
93
- return toResponse(hydratedModel.value);
88
+ return hydratedModel;
94
89
  }
95
- instance = hydratedModel.value;
90
+ return right(CrudContext.fromInstance(d1, hydratedModel.value, constructorRegistry[model.name]));
91
+ })();
92
+ if (!crudCtx.ok) {
93
+ return toResponse(crudCtx.value);
96
94
  }
97
- // Dispatch a method on the model and return the result
98
- return toResponse(await methodDispatch(instance, instanceRegistry, envMeta, method, params));
95
+ //#endregion
96
+ return toResponse(await methodDispatch(crudCtx.value, ir, method, params));
99
97
  }
100
98
  /**
101
99
  * Matches a request to a method on a model.
102
- * @param api_route The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
100
+ * @param apiRoute The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
103
101
  * @returns 404 or a `MatchedRoute`
104
102
  */
105
- function matchRoute(request, ast, api_route) {
103
+ function matchRoute(request, ast, apiRoute) {
106
104
  const url = new URL(request.url);
107
105
  // Error state: We expect an exact request format, and expect that the model
108
106
  // and are apart of the CIDL
109
107
  const notFound = (e) => left(errorState(404, `Path not found: ${e} ${url.pathname}`));
110
108
  const routeParts = url.pathname
111
- .slice(api_route.length)
109
+ .slice(apiRoute.length)
112
110
  .split("/")
113
111
  .filter(Boolean);
114
112
  if (routeParts.length < 2) {
@@ -148,14 +146,10 @@ async function validateRequest(request, ast, model, method, id) {
148
146
  }
149
147
  // Filter out any injected parameters that will not be passed
150
148
  // by the query.
151
- const requiredParams = method.parameters.filter((p) => !(typeof p.cidl_type === "object" &&
152
- p.cidl_type !== null &&
153
- "Inject" in p.cidl_type));
154
- // Extract data source
155
- const url = new URL(request.url);
156
- let dataSource = url.searchParams.get("dataSource");
149
+ const requiredParams = method.parameters.filter((p) => !(typeof p.cidl_type === "object" && "Inject" in p.cidl_type));
157
150
  // Extract url or body parameters
158
- let params;
151
+ const url = new URL(request.url);
152
+ let params = {};
159
153
  if (method.http_verb === "GET") {
160
154
  params = Object.fromEntries(url.searchParams.entries());
161
155
  }
@@ -167,10 +161,6 @@ async function validateRequest(request, ast, model, method, id) {
167
161
  return invalidRequest("Could not retrieve JSON body.");
168
162
  }
169
163
  }
170
- // Validate data source if exists
171
- if (dataSource && !(dataSource in model.data_sources)) {
172
- return invalidRequest(`Unknown data source ${dataSource}`);
173
- }
174
164
  // Ensure all required params exist
175
165
  if (!requiredParams.every((p) => p.name in params)) {
176
166
  return invalidRequest(`Missing parameters.`);
@@ -178,10 +168,21 @@ async function validateRequest(request, ast, model, method, id) {
178
168
  // Validate all parameters type
179
169
  for (const p of requiredParams) {
180
170
  const value = params[p.name];
181
- if (!validateCidlType(ast, value, p.cidl_type)) {
171
+ const isPartial = typeof p.cidl_type !== "string" && "Partial" in p.cidl_type;
172
+ if (!validateCidlType(ast, value, p.cidl_type, isPartial)) {
182
173
  return invalidRequest("Invalid parameters.");
183
174
  }
184
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
+ }
185
186
  return right({ params, dataSource });
186
187
  }
187
188
  /**
@@ -198,14 +199,14 @@ async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
198
199
  // Error state: If no record is found for the id, return a 404
199
200
  const missingRecord = left(errorState(404, "Record not found"));
200
201
  const pk = model.primary_key.name;
201
- const query = dataSource !== null
202
+ const query = dataSource !== NO_DATA_SOURCE
202
203
  ? `SELECT * FROM "${model.name}.${dataSource}" WHERE "${model.name}.${pk}" = ?`
203
204
  : `SELECT * FROM "${model.name}" WHERE "${pk}" = ?`;
204
205
  // Query DB
205
206
  let records;
206
207
  try {
207
208
  records = await d1.prepare(query).bind(id).run();
208
- if (!records) {
209
+ if (!records?.results) {
209
210
  return missingRecord;
210
211
  }
211
212
  if (records.error) {
@@ -215,24 +216,32 @@ async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
215
216
  catch (e) {
216
217
  return malformedQuery(e);
217
218
  }
218
- // Get include tree
219
- const includeTree = dataSource !== null ? model.data_sources[dataSource].tree : {};
220
219
  // Hydrate
221
- const models = modelsFromSql(constructorRegistry[model.name], records.results, includeTree);
220
+ const models = fromSql(constructorRegistry[model.name], records.results, model.data_sources[dataSource]?.tree ?? {}).value;
222
221
  return right(models[0]);
223
222
  }
224
223
  /**
225
224
  * Calls a method on a model given a list of parameters.
226
225
  * @returns 500 on an uncaught client error, 200 with a result body on success
227
226
  */
228
- async function methodDispatch(instance, instanceRegistry, envMeta, method, params) {
227
+ async function methodDispatch(crudCtx, instanceRegistry, method, params) {
229
228
  // Error state: Client code ran into an uncaught exception.
230
229
  const uncaughtException = (e) => errorState(500, `Uncaught exception in method dispatch: ${e instanceof Error ? e.message : String(e)}`);
231
- // For now, the only injected dependency is the wrangler env,
232
- // so we will assume that is what this is
233
- const paramArray = method.parameters.map((p) => params[p.name] == undefined
234
- ? instanceRegistry.get(envMeta.envName)
235
- : params[p.name]);
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
+ }
236
245
  // Ensure the result is always some HttpResult
237
246
  const resultWrapper = (res) => {
238
247
  const rt = method.return_type;
@@ -245,17 +254,18 @@ async function methodDispatch(instance, instanceRegistry, envMeta, method, param
245
254
  return { ok: true, status: 200, data: res };
246
255
  };
247
256
  try {
248
- return resultWrapper(await instance[method.name](...paramArray));
257
+ const res = await crudCtx.interceptCrud(method.name)(...paramArray);
258
+ return resultWrapper(res);
249
259
  }
250
260
  catch (e) {
251
261
  return uncaughtException(e);
252
262
  }
253
263
  }
254
- function validateCidlType(ast, value, cidlType) {
264
+ function validateCidlType(ast, value, cidlType, isPartial) {
255
265
  if (value === undefined)
256
- return false;
266
+ return isPartial;
257
267
  // TODO: consequences of null checking like this? 'null' is passed in
258
- // as a string for GET requests...
268
+ // as a string for GET requests
259
269
  const nullable = isNullableType(cidlType);
260
270
  if (value == null || value === "null")
261
271
  return nullable;
@@ -277,109 +287,57 @@ function validateCidlType(ast, value, cidlType) {
277
287
  return false;
278
288
  }
279
289
  }
290
+ // Handle Data Sources
291
+ if ("DataSource" in cidlType) {
292
+ return typeof value === "string";
293
+ }
280
294
  // Handle Models
281
- if ("Object" in cidlType && ast.models[cidlType.Object]) {
282
- const model = ast.models[cidlType.Object];
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];
283
302
  if (!model || typeof value !== "object")
284
303
  return false;
285
304
  const valueObj = value;
286
305
  // Validate attributes
287
- if (!model.attributes.every((attr) => validateCidlType(ast, valueObj[attr.value.name], attr.value.cidl_type))) {
306
+ if (!model.attributes.every((attr) => validateCidlType(ast, valueObj[attr.value.name], attr.value.cidl_type, isPartial))) {
288
307
  return false;
289
308
  }
290
309
  // Validate navigation properties
291
310
  return model.navigation_properties.every((nav) => {
292
311
  const navValue = valueObj[nav.var_name];
293
312
  return (navValue == null ||
294
- validateCidlType(ast, navValue, getNavigationPropertyCidlType(nav)));
313
+ validateCidlType(ast, navValue, getNavigationPropertyCidlType(nav), isPartial));
295
314
  });
296
315
  }
297
316
  // Handle Plain Old Objects
298
- if ("Object" in cidlType && ast.poos[cidlType.Object]) {
299
- const poo = ast.poos[cidlType.Object];
317
+ if (cidlTypeAccessor && ast.poos[cidlTypeAccessor]) {
318
+ const poo = ast.poos[cidlTypeAccessor];
300
319
  if (!poo || typeof value !== "object")
301
320
  return false;
302
321
  const valueObj = value;
303
322
  // Validate attributes
304
- if (!poo.attributes.every((attr) => validateCidlType(ast, valueObj[attr.name], attr.cidl_type))) {
323
+ if (!poo.attributes.every((attr) => validateCidlType(ast, valueObj[attr.name], attr.cidl_type, isPartial))) {
305
324
  return false;
306
325
  }
307
326
  }
308
327
  if ("Array" in cidlType) {
309
328
  const arr = cidlType.Array;
310
- return (Array.isArray(value) && value.every((v) => validateCidlType(ast, v, arr)));
329
+ return (Array.isArray(value) &&
330
+ value.every((v) => validateCidlType(ast, v, arr, isPartial)));
311
331
  }
312
332
  if ("HttpResult" in cidlType) {
313
333
  if (value === null)
314
334
  return cidlType.HttpResult === null;
315
335
  if (cidlType.HttpResult === null)
316
336
  return false;
317
- return validateCidlType(ast, value, cidlType.HttpResult);
337
+ return validateCidlType(ast, value, cidlType.HttpResult, isPartial);
318
338
  }
319
339
  return false;
320
340
  }
321
- /**
322
- * Creates model instances given a properly formatted SQL record, being either:
323
- *
324
- * 1. Flat, relationship-less (ex: id, name, location, ...)
325
- * 2. `DataSource` formatted (ex: Horse.id, Horse.name, Horse.rider, ...)
326
- *
327
- * @param ctor The type of the model
328
- * @param records SQL records
329
- * @param includeTree The include tree to use when parsing the records
330
- * @returns An instantiated array of `T`, containing one or more objects.
331
- */
332
- export function modelsFromSql(ctor, records, includeTree) {
333
- const { ast, constructorRegistry, wasm } = RuntimeContainer.get();
334
- const modelName = WasmResource.fromString(ctor.name, wasm);
335
- const rows = WasmResource.fromString(JSON.stringify(records), wasm);
336
- const includeTreeJson = WasmResource.fromString(JSON.stringify(includeTree), wasm);
337
- // Invoke the ORM
338
- const jsonResults = (() => {
339
- let resPtr;
340
- let resLen;
341
- try {
342
- resPtr = wasm.object_relational_mapping(modelName.ptr, modelName.len, rows.ptr, rows.len, includeTreeJson.ptr, includeTreeJson.len);
343
- resLen = wasm.get_return_len();
344
- // Parse the results as JSON
345
- return JSON.parse(new TextDecoder().decode(new Uint8Array(wasm.memory.buffer, resPtr, resLen)));
346
- }
347
- finally {
348
- modelName.free();
349
- rows.free();
350
- includeTreeJson.free();
351
- // Could resPtr some how be set but not resLen? Kind of a flaw
352
- // in how WASM works.
353
- if (resPtr && resLen)
354
- wasm.dealloc(resPtr, resLen);
355
- }
356
- })();
357
- return jsonResults.map((obj) => instantiateDfs(obj, ast.models[ctor.name], includeTree));
358
- // The result that comes back is just raw JSON, run a DFS on each navigation property
359
- // in the include tree provided, instantiating each object via constructor registry.
360
- function instantiateDfs(m, meta, includeTree) {
361
- m = Object.assign(new constructorRegistry[meta.name](), m);
362
- if (!includeTree) {
363
- return m;
364
- }
365
- for (const navProp of meta.navigation_properties) {
366
- const nestedIncludeTree = includeTree[navProp.var_name];
367
- if (!nestedIncludeTree)
368
- continue;
369
- const nestedMeta = ast.models[navProp.model_name];
370
- const value = m[navProp.var_name];
371
- // One to Many, Many to Many
372
- if (Array.isArray(value)) {
373
- m[navProp.var_name] = value.map((child) => instantiateDfs(child, nestedMeta, nestedIncludeTree));
374
- }
375
- // One to one
376
- else if (value) {
377
- m[navProp.var_name] = instantiateDfs(value, nestedMeta, nestedIncludeTree);
378
- }
379
- }
380
- return m;
381
- }
382
- }
383
341
  function errorState(status, message) {
384
342
  return { ok: false, status, message };
385
343
  }
@@ -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
+ }