cloesce 0.0.3-fix.1 → 0.0.3-fix.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.
@@ -1,50 +1,74 @@
1
- import { left, right, isNullableType, getNavigationPropertyCidlType, } from "./common.js";
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";
2
4
  /**
3
- * Singleton instances of the MetaCidl and Constructor Registry.
4
- * These values are guaranteed to never change throughout a workers lifetime.
5
+ * RAII for wasm memory
5
6
  */
6
- class MetaContainer {
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 {
7
36
  ast;
8
37
  constructorRegistry;
38
+ wasm;
9
39
  static instance;
10
- constructor(ast, constructorRegistry) {
40
+ constructor(ast, constructorRegistry, wasm) {
11
41
  this.ast = ast;
12
42
  this.constructorRegistry = constructorRegistry;
43
+ this.wasm = wasm;
13
44
  }
14
- static init(ast, constructorRegistry) {
15
- if (!this.instance) {
16
- this.instance = new MetaContainer(ast, constructorRegistry);
45
+ static async init(ast, constructorRegistry, wasm) {
46
+ if (this.instance) {
47
+ 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");
17
56
  }
57
+ // Intentionally leak `modelMeta`, it should exist for the programs lifetime.
58
+ this.instance = new RuntimeContainer(ast, constructorRegistry, wasmInstance.exports);
18
59
  }
19
60
  static get() {
20
61
  return this.instance;
21
62
  }
22
63
  }
23
64
  /**
24
- * Creates model instances given a properly formatted SQL record
25
- * (either a foreign-key-less model or derived from a Cloesce generated view)
26
- * @param ctor The type of the model
27
- * @param records SQL records
28
- * @param includeTree The include tree to use when parsing the records
29
- * @returns
30
- */
31
- export function modelsFromSql(ctor, records, includeTree) {
32
- const { ast, constructorRegistry } = MetaContainer.get();
33
- return _modelsFromSql(ctor.name, ast, constructorRegistry, records, includeTree);
34
- }
35
- /**
36
- * Cloesce entry point. Given a request, undergoes routing, validating,
65
+ * Runtime entry point. Given a request, undergoes: routing, validating,
37
66
  * hydrating, and method dispatch.
38
- * @param ast The CIDL AST
39
- * @param constructorRegistry A mapping of user defined class names to their respective constructor
40
- * @param instanceRegistry A mapping of a dependency class name to its instantiated object.
41
- * @param request An incoming request to the workers server
42
- * @param api_route The url's path to the api, e.g. api/v1/fooapi/
43
- * @param envMeta Meta information on the wrangler env and D1 databases
67
+ *
44
68
  * @returns A Response with an `HttpResult` JSON body.
45
69
  */
46
70
  export async function cloesce(request, ast, constructorRegistry, instanceRegistry, envMeta, api_route) {
47
- MetaContainer.init(ast, constructorRegistry);
71
+ await RuntimeContainer.init(ast, constructorRegistry);
48
72
  const d1 = instanceRegistry.get(envMeta.envName)[envMeta.dbName];
49
73
  // Match the route to a model method
50
74
  const route = matchRoute(request, ast, api_route);
@@ -53,25 +77,25 @@ export async function cloesce(request, ast, constructorRegistry, instanceRegistr
53
77
  }
54
78
  const { method, model, id } = route.value;
55
79
  // Validate request body to the model method
56
- const isValidRequest = await validateRequest(request, ast, model, method, id);
57
- if (!isValidRequest.ok) {
58
- return toResponse(isValidRequest.value);
80
+ const validation = await validateRequest(request, ast, model, method, id);
81
+ if (!validation.ok) {
82
+ return toResponse(validation.value);
59
83
  }
60
- const [requestParamMap, dataSource] = isValidRequest.value;
84
+ const { params, dataSource } = validation.value;
61
85
  // Instantatiate the model
62
86
  let instance;
63
87
  if (method.is_static) {
64
88
  instance = constructorRegistry[model.name];
65
89
  }
66
90
  else {
67
- const successfulModel = await hydrateModel(ast, constructorRegistry, d1, model, id, dataSource);
68
- if (!successfulModel.ok) {
69
- return toResponse(successfulModel.value);
91
+ const hydratedModel = await hydrateModel(constructorRegistry, d1, model, id, dataSource);
92
+ if (!hydratedModel.ok) {
93
+ return toResponse(hydratedModel.value);
70
94
  }
71
- instance = successfulModel.value;
95
+ instance = hydratedModel.value;
72
96
  }
73
97
  // Dispatch a method on the model and return the result
74
- return toResponse(await methodDispatch(instance, instanceRegistry, envMeta, method, requestParamMap));
98
+ return toResponse(await methodDispatch(instance, instanceRegistry, envMeta, method, params));
75
99
  }
76
100
  /**
77
101
  * Matches a request to a method on a model.
@@ -80,6 +104,8 @@ export async function cloesce(request, ast, constructorRegistry, instanceRegistr
80
104
  */
81
105
  function matchRoute(request, ast, api_route) {
82
106
  const url = new URL(request.url);
107
+ // Error state: We expect an exact request format, and expect that the model
108
+ // and are apart of the CIDL
83
109
  const notFound = (e) => left(errorState(404, `Path not found: ${e} ${url.pathname}`));
84
110
  const routeParts = url.pathname
85
111
  .slice(api_route.length)
@@ -129,13 +155,13 @@ async function validateRequest(request, ast, model, method, id) {
129
155
  const url = new URL(request.url);
130
156
  let dataSource = url.searchParams.get("dataSource");
131
157
  // Extract url or body parameters
132
- let requestBodyMap;
158
+ let params;
133
159
  if (method.http_verb === "GET") {
134
- requestBodyMap = Object.fromEntries(url.searchParams.entries());
160
+ params = Object.fromEntries(url.searchParams.entries());
135
161
  }
136
162
  else {
137
163
  try {
138
- requestBodyMap = await request.json();
164
+ params = await request.json();
139
165
  }
140
166
  catch {
141
167
  return invalidRequest("Could not retrieve JSON body.");
@@ -146,17 +172,17 @@ async function validateRequest(request, ast, model, method, id) {
146
172
  return invalidRequest(`Unknown data source ${dataSource}`);
147
173
  }
148
174
  // Ensure all required params exist
149
- if (!requiredParams.every((p) => p.name in requestBodyMap)) {
175
+ if (!requiredParams.every((p) => p.name in params)) {
150
176
  return invalidRequest(`Missing parameters.`);
151
177
  }
152
178
  // Validate all parameters type
153
179
  for (const p of requiredParams) {
154
- const value = requestBodyMap[p.name];
180
+ const value = params[p.name];
155
181
  if (!validateCidlType(ast, value, p.cidl_type)) {
156
182
  return invalidRequest("Invalid parameters.");
157
183
  }
158
184
  }
159
- return right([requestBodyMap, dataSource]);
185
+ return right({ params, dataSource });
160
186
  }
161
187
  /**
162
188
  * Queries D1 for a particular model's ID, then transforms the SQL column output into
@@ -165,7 +191,7 @@ async function validateRequest(request, ast, model, method, id) {
165
191
  * @returns 500 if the D1 database is not synced with Cloesce and yields an error
166
192
  * @returns The instantiated model on success
167
193
  */
168
- async function hydrateModel(ast, constructorRegistry, d1, model, id, dataSource) {
194
+ async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
169
195
  // Error state: If the D1 database has been tweaked outside of Cloesce
170
196
  // resulting in a malformed query, exit with a 500.
171
197
  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)}`));
@@ -192,8 +218,7 @@ async function hydrateModel(ast, constructorRegistry, d1, model, id, dataSource)
192
218
  // Get include tree
193
219
  const includeTree = dataSource !== null ? model.data_sources[dataSource].tree : {};
194
220
  // Hydrate
195
- const models = _modelsFromSql(model.name, ast, constructorRegistry, records.results, includeTree);
196
- console.log(JSON.stringify(models));
221
+ const models = modelsFromSql(constructorRegistry[model.name], records.results, includeTree);
197
222
  return right(models[0]);
198
223
  }
199
224
  /**
@@ -294,142 +319,65 @@ function validateCidlType(ast, value, cidlType) {
294
319
  return false;
295
320
  }
296
321
  /**
297
- * Actual implementation of sql to model mapping.
322
+ * Creates model instances given a properly formatted SQL record, being either:
298
323
  *
299
- * TODO: If we don't want to write this in every language, would it be possible to create a
300
- * single WASM binary for this method?
324
+ * 1. Flat, relationship-less (ex: id, name, location, ...)
325
+ * 2. `DataSource` formatted (ex: Horse.id, Horse.name, Horse.rider, ...)
301
326
  *
302
- * @throws generic errors if the metadata is missing some value
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.
303
331
  */
304
- // Main function that creates instances from SQL records
305
- function _modelsFromSql(modelName, ast, constructorRegistry, records, includeTree) {
306
- const model = ast.models[modelName];
307
- if (!model)
308
- return [];
309
- const Constructor = constructorRegistry[modelName];
310
- if (!Constructor)
311
- return [];
312
- const pkName = model.primary_key.name;
313
- const resultMap = new Map();
314
- for (const record of records) {
315
- const pkValue = record[`${modelName}.${pkName}`] ?? record[pkName];
316
- if (pkValue == null)
317
- continue;
318
- let instance = resultMap.get(pkValue);
319
- if (!instance) {
320
- instance = new Constructor();
321
- instance[pkName] = pkValue;
322
- // Set scalar attributes
323
- for (const attr of model.attributes) {
324
- const attrName = attr.value.name;
325
- const prefixedKey = `${modelName}.${attrName}`;
326
- const nonPrefixedKey = attrName;
327
- if (prefixedKey in record) {
328
- instance[attrName] = record[prefixedKey];
329
- }
330
- else if (nonPrefixedKey in record) {
331
- instance[attrName] = record[nonPrefixedKey];
332
- }
333
- }
334
- // Initialize ALL navigation properties at root level
335
- // If not in include tree, initialize OneToMany and ManyToMany as empty arrays
336
- for (const navProp of model.navigation_properties) {
337
- if ("OneToMany" in navProp.kind || "ManyToMany" in navProp.kind) {
338
- // Always initialize OneToMany and ManyToMany as empty arrays
339
- instance[navProp.var_name] = [];
340
- }
341
- // OneToOne properties left as undefined unless populated
342
- }
343
- resultMap.set(pkValue, instance);
344
- }
345
- // Process navigation properties that are in the include tree
346
- if (includeTree) {
347
- processNavigationProperties(instance, model, modelName, includeTree, record, ast, constructorRegistry);
348
- }
349
- }
350
- return Array.from(resultMap.values());
351
- }
352
- function processNavigationProperties(instance, model, prefix, includeTree, record, ast, constructorRegistry) {
353
- for (const navProp of model.navigation_properties) {
354
- if (!(navProp.var_name in includeTree)) {
355
- continue;
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)));
356
346
  }
357
- const nestedModel = ast.models[navProp.model_name];
358
- if (!nestedModel) {
359
- continue;
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);
360
355
  }
361
- // Extract nested model's primary key - check both prefixed and non-prefixed
362
- const nestedPkName = nestedModel.primary_key.name;
363
- const prefixedNestedPkKey = `${prefix}.${navProp.var_name}.${nestedPkName}`;
364
- const nonPrefixedNestedPkKey = `${navProp.var_name}.${nestedPkName}`;
365
- const nestedPkValue = record[prefixedNestedPkKey] ?? record[nonPrefixedNestedPkKey];
366
- if (nestedPkValue == null) {
367
- continue; // No nested object in this row
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;
368
364
  }
369
- // Determine if this is OneToMany/ManyToMany or OneToOne
370
- const isOneToMany = "OneToMany" in navProp.kind || "ManyToMany" in navProp.kind;
371
- // Check if we already added this nested object (for OneToMany)
372
- if (isOneToMany) {
373
- const navArray = instance[navProp.var_name];
374
- const alreadyExists = navArray.some((item) => item[nestedPkName] === nestedPkValue);
375
- if (alreadyExists) {
365
+ for (const navProp of meta.navigation_properties) {
366
+ const nestedIncludeTree = includeTree[navProp.var_name];
367
+ if (!nestedIncludeTree)
376
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));
377
374
  }
378
- }
379
- else {
380
- // For OneToOne, check if already set
381
- if (instance[navProp.var_name] != null) {
382
- continue;
375
+ // One to one
376
+ else if (value) {
377
+ m[navProp.var_name] = instantiateDfs(value, nestedMeta, nestedIncludeTree);
383
378
  }
384
379
  }
385
- const NestedConstructor = constructorRegistry[navProp.model_name];
386
- if (!NestedConstructor) {
387
- continue;
388
- }
389
- const nestedInstance = new NestedConstructor();
390
- nestedInstance[nestedPkName] = nestedPkValue;
391
- // Assign nested scalar attributes - check both prefixed and non-prefixed
392
- for (const nestedAttr of nestedModel.attributes) {
393
- const nestedAttrName = nestedAttr.value.name;
394
- const prefixedKey = `${prefix}.${navProp.var_name}.${nestedAttrName}`;
395
- const nonPrefixedKey = `${navProp.var_name}.${nestedAttrName}`;
396
- // Check prefixed key first, then non-prefixed
397
- if (prefixedKey in record) {
398
- nestedInstance[nestedAttrName] = record[prefixedKey];
399
- }
400
- else if (nonPrefixedKey in record) {
401
- nestedInstance[nestedAttrName] = record[nonPrefixedKey];
402
- }
403
- }
404
- // Initialize ALL navigation properties on the nested instance
405
- // If not in include tree, initialize OneToMany and ManyToMany as empty arrays
406
- const nestedIncludeTree = includeTree[navProp.var_name];
407
- for (const nestedNavProp of nestedModel.navigation_properties) {
408
- const isInIncludeTree = nestedIncludeTree &&
409
- typeof nestedIncludeTree === "object" &&
410
- nestedNavProp.var_name in nestedIncludeTree;
411
- if ("OneToMany" in nestedNavProp.kind ||
412
- "ManyToMany" in nestedNavProp.kind) {
413
- // Always initialize OneToMany and ManyToMany as arrays (empty if not in include tree)
414
- nestedInstance[nestedNavProp.var_name] = [];
415
- }
416
- else if (!isInIncludeTree) {
417
- // OneToOne not in include tree - leave as undefined or null
418
- // Will be set during recursive processing if in include tree
419
- }
420
- }
421
- // Recursively process nested navigation properties that are in the include tree
422
- if (nestedIncludeTree && typeof nestedIncludeTree === "object") {
423
- processNavigationProperties(nestedInstance, nestedModel, `${prefix}.${navProp.var_name}`, nestedIncludeTree, record, ast, constructorRegistry);
424
- }
425
- // Assign the nested instance based on relationship type
426
- if (isOneToMany) {
427
- instance[navProp.var_name].push(nestedInstance);
428
- }
429
- else {
430
- // OneToOne - assign directly
431
- instance[navProp.var_name] = nestedInstance;
432
- }
380
+ return m;
433
381
  }
434
382
  }
435
383
  function errorState(status, message) {
@@ -442,12 +390,11 @@ function toResponse(r) {
442
390
  });
443
391
  }
444
392
  /**
445
- * Each individual state of the `cloesce` function for testing purposes.
393
+ * For testing purposes
446
394
  */
447
395
  export const _cloesceInternal = {
448
396
  matchRoute,
449
397
  validateRequest,
450
- hydrateModel,
451
398
  methodDispatch,
452
- _modelsFromSql,
399
+ RuntimeContainer,
453
400
  };
Binary file
package/package.json CHANGED
@@ -1,23 +1,45 @@
1
1
  {
2
2
  "name": "cloesce",
3
- "version": "0.0.3-fix.1",
3
+ "version": "0.0.3-fix.3",
4
4
  "description": "A tool to extract and compile TypeScript code into something wrangler can consume and deploy for D1 Databases and Cloudflare Workers",
5
5
  "type": "module",
6
- "license": "Apache-2.0",
7
6
  "main": "./dist/index.js",
8
7
  "types": "./dist/index.d.ts",
8
+ "license": "Apache-2.0",
9
+ "scripts": {
10
+ "test": "vitest",
11
+ "format:fix": "prettier --write .",
12
+ "format": "prettier --check .",
13
+ "typecheck": "tsc --noEmit",
14
+ "build": "tsc -p tsconfig.json && npm run copy-rs-runtime-wasm && npm run copy-generator-wasm",
15
+ "copy-rs-runtime-wasm": "cp ../../runtime/target/wasm32-unknown-unknown/release/runtime.wasm ./dist/runtime.wasm",
16
+ "copy-generator-wasm": "cp ../../generator/target/wasm32-wasip1/release/cli.wasm ./dist/generator.wasm"
17
+ },
18
+ "dependencies": {
19
+ "cmd-ts": "^0.14.1",
20
+ "ts-morph": "^22.0.0",
21
+ "wrangler": "^4.34.0"
22
+ },
23
+ "devDependencies": {
24
+ "@cloudflare/workers-types": "^4.20250906.0",
25
+ "ts-node": "^10.9.2",
26
+ "typescript": "^5.6.0",
27
+ "prettier": "^3.6.2",
28
+ "vitest": "^3.2.4",
29
+ "vite-plugin-wasm": "^3.5.0"
30
+ },
9
31
  "exports": {
10
32
  ".": {
11
33
  "types": "./dist/index.d.ts",
12
34
  "import": "./dist/index.js"
13
35
  },
14
- "./cli": "./dist/cli.js"
36
+ "./cli": "./dist/extractor/cli.js"
15
37
  },
16
38
  "bin": {
17
- "cloesce": "./dist/cli.js"
39
+ "cloesce": "dist/extractor/cli.js"
18
40
  },
19
41
  "files": [
20
- "dist/**",
42
+ "dist/**/*",
21
43
  "README.md",
22
44
  "LICENSE"
23
45
  ],
@@ -25,26 +47,6 @@
25
47
  "engines": {
26
48
  "node": ">=18.17"
27
49
  },
28
- "scripts": {
29
- "build": "tsc -p tsconfig.json",
30
- "typecheck": "tsc --noEmit",
31
- "test": "vitest --run",
32
- "format": "prettier --check .",
33
- "format:fix": "prettier --write .",
34
- "prepublishOnly": "npm run typecheck && npm run build && npm run test"
35
- },
36
- "dependencies": {
37
- "cmd-ts": "^0.14.1",
38
- "ts-morph": "^22.0.0"
39
- },
40
- "devDependencies": {
41
- "@cloudflare/workers-types": "^4.20250906.0",
42
- "prettier": "^3.6.2",
43
- "ts-node": "^10.9.2",
44
- "typescript": "^5.6.0",
45
- "vitest": "^3.2.4",
46
- "wrangler": "^4.34.0"
47
- },
48
50
  "repository": {
49
51
  "type": "git",
50
52
  "url": "git+https://github.com/bens-schreiber/cloesce.git"