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.
- package/dist/README.md +487 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +221 -254
- package/dist/common.d.ts +69 -1
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +72 -11
- package/dist/{extract.d.ts → extractor/extract.d.ts} +5 -2
- package/dist/extractor/extract.d.ts.map +1 -0
- package/dist/{extract.js → 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/router/router.js +358 -0
- 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/ui/client.d.ts +5 -0
- package/dist/ui/client.d.ts.map +1 -0
- package/dist/ui/client.js +7 -0
- package/package.json +70 -58
- package/LICENSE +0 -201
- package/README.md +0 -23
- package/dist/cli.wasm +0 -0
- package/dist/cloesce.d.ts +0 -108
- package/dist/cloesce.d.ts.map +0 -1
- package/dist/cloesce.js +0 -453
- package/dist/decorators.d.ts +0 -13
- package/dist/decorators.d.ts.map +0 -1
- package/dist/decorators.js +0 -13
- package/dist/dog.cloesce.js +0 -111
- package/dist/extract.d.ts.map +0 -1
- package/dist/index.d.ts +0 -24
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -24
- package/dist/types.d.ts +0 -4
- package/dist/types.d.ts.map +0 -1
- 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
|
+
}
|