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.
- package/dist/README.md +154 -70
- package/dist/cli.js +65 -85
- package/dist/common.d.ts +49 -11
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +72 -11
- package/dist/extractor/extract.d.ts +2 -0
- package/dist/extractor/extract.d.ts.map +1 -1
- package/dist/extractor/extract.js +242 -43
- package/dist/generator.wasm +0 -0
- package/dist/orm.wasm +0 -0
- package/dist/router/crud.d.ts +22 -0
- package/dist/router/crud.d.ts.map +1 -0
- package/dist/router/crud.js +65 -0
- package/dist/router/router.d.ts +77 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/{runtime/runtime.js → router/router.js} +119 -161
- package/dist/router/wasm.d.ts +37 -0
- package/dist/router/wasm.d.ts.map +1 -0
- package/dist/router/wasm.js +98 -0
- package/dist/ui/backend.d.ts +124 -0
- package/dist/ui/backend.d.ts.map +1 -0
- package/dist/ui/backend.js +201 -0
- package/dist/{index → ui}/client.d.ts +1 -1
- package/dist/ui/client.d.ts.map +1 -0
- package/package.json +15 -13
- package/dist/LICENSE +0 -201
- package/dist/index/backend.d.ts +0 -22
- package/dist/index/backend.d.ts.map +0 -1
- package/dist/index/backend.js +0 -17
- package/dist/index/client.d.ts.map +0 -1
- package/dist/runtime/runtime.d.ts +0 -112
- package/dist/runtime/runtime.d.ts.map +0 -1
- package/dist/runtime.wasm +0 -0
- /package/dist/{index → ui}/client.js +0 -0
|
@@ -1,38 +1,11 @@
|
|
|
1
|
-
import { left, right, isNullableType, getNavigationPropertyCidlType, } from "../common.js";
|
|
2
|
-
|
|
3
|
-
import
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
88
|
+
return hydratedModel;
|
|
94
89
|
}
|
|
95
|
-
|
|
90
|
+
return right(CrudContext.fromInstance(d1, hydratedModel.value, constructorRegistry[model.name]));
|
|
91
|
+
})();
|
|
92
|
+
if (!crudCtx.ok) {
|
|
93
|
+
return toResponse(crudCtx.value);
|
|
96
94
|
}
|
|
97
|
-
|
|
98
|
-
return toResponse(await methodDispatch(
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 !==
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
282
|
-
|
|
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 (
|
|
299
|
-
const poo = ast.poos[
|
|
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) &&
|
|
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
|
+
}
|