cloesce 0.0.4-unstable.0 → 0.0.4-unstable.10
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 → README.md} +212 -67
- package/dist/cli.js +37 -21
- package/dist/common.d.ts +89 -41
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +93 -38
- package/dist/extractor/extract.d.ts +9 -9
- package/dist/extractor/extract.d.ts.map +1 -1
- package/dist/extractor/extract.js +128 -149
- package/dist/generator.wasm +0 -0
- package/dist/orm.wasm +0 -0
- package/dist/router/crud.d.ts.map +1 -1
- package/dist/router/crud.js +25 -20
- package/dist/router/router.d.ts +71 -15
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +190 -109
- package/dist/router/wasm.d.ts +10 -3
- package/dist/router/wasm.d.ts.map +1 -1
- package/dist/router/wasm.js +12 -6
- package/dist/ui/backend.d.ts +431 -41
- package/dist/ui/backend.d.ts.map +1 -1
- package/dist/ui/backend.js +391 -90
- package/dist/ui/client.d.ts +2 -1
- package/dist/ui/client.d.ts.map +1 -1
- package/dist/ui/client.js +1 -0
- package/package.json +2 -2
package/dist/router/crud.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NO_DATA_SOURCE } from "../common.js";
|
|
1
|
+
import { HttpResult, NO_DATA_SOURCE } from "../common.js";
|
|
2
2
|
import { Orm } from "../ui/backend.js";
|
|
3
3
|
/**
|
|
4
4
|
* A wrapper for Model Instances, containing definitions for built-in CRUD methods.
|
|
@@ -24,7 +24,7 @@ export class CrudContext {
|
|
|
24
24
|
*/
|
|
25
25
|
interceptCrud(methodName) {
|
|
26
26
|
const map = {
|
|
27
|
-
|
|
27
|
+
save: this.upsert.bind(this),
|
|
28
28
|
get: this.get.bind(this),
|
|
29
29
|
list: this.list.bind(this),
|
|
30
30
|
};
|
|
@@ -32,34 +32,39 @@ export class CrudContext {
|
|
|
32
32
|
return fn ? fn.bind(this.instance) : map[methodName];
|
|
33
33
|
}
|
|
34
34
|
async upsert(obj, dataSource) {
|
|
35
|
-
const
|
|
36
|
-
const includeTree = normalizedDs ? this.ctor[normalizedDs] : null;
|
|
35
|
+
const includeTree = findIncludeTree(dataSource, this.ctor);
|
|
37
36
|
// Upsert
|
|
38
37
|
const orm = Orm.fromD1(this.d1);
|
|
39
38
|
const upsert = await orm.upsert(this.ctor, obj, includeTree);
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
39
|
+
if (upsert.isLeft()) {
|
|
40
|
+
return HttpResult.fail(500, upsert.value); // TODO: better status code?
|
|
42
41
|
}
|
|
43
42
|
// Get
|
|
44
|
-
const get = await orm.get(this.ctor, upsert.value,
|
|
45
|
-
return get.
|
|
46
|
-
?
|
|
47
|
-
:
|
|
43
|
+
const get = await orm.get(this.ctor, upsert.value, includeTree);
|
|
44
|
+
return get.isRight()
|
|
45
|
+
? HttpResult.ok(200, get.value)
|
|
46
|
+
: HttpResult.fail(500, get.value);
|
|
48
47
|
}
|
|
49
48
|
async get(id, dataSource) {
|
|
50
|
-
const
|
|
49
|
+
const includeTree = findIncludeTree(dataSource, this.ctor);
|
|
51
50
|
const orm = Orm.fromD1(this.d1);
|
|
52
|
-
const res = await orm.get(this.ctor, id,
|
|
53
|
-
return res.
|
|
54
|
-
?
|
|
55
|
-
:
|
|
51
|
+
const res = await orm.get(this.ctor, id, includeTree);
|
|
52
|
+
return res.isRight()
|
|
53
|
+
? HttpResult.ok(200, res.value)
|
|
54
|
+
: HttpResult.fail(500, res.value);
|
|
56
55
|
}
|
|
57
56
|
async list(dataSource) {
|
|
58
|
-
const
|
|
57
|
+
const includeTree = findIncludeTree(dataSource, this.ctor);
|
|
59
58
|
const orm = Orm.fromD1(this.d1);
|
|
60
|
-
const res = await orm.list(this.ctor,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
const res = await orm.list(this.ctor, {
|
|
60
|
+
includeTree,
|
|
61
|
+
});
|
|
62
|
+
return res.isRight()
|
|
63
|
+
? HttpResult.ok(200, res.value)
|
|
64
|
+
: HttpResult.fail(500, res.value);
|
|
64
65
|
}
|
|
65
66
|
}
|
|
67
|
+
function findIncludeTree(dataSource, ctor) {
|
|
68
|
+
const normalizedDs = dataSource === NO_DATA_SOURCE ? null : dataSource;
|
|
69
|
+
return normalizedDs ? ctor[normalizedDs] : null;
|
|
70
|
+
}
|
package/dist/router/router.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { HttpResult, Either, ModelMethod, CloesceAst, Model,
|
|
1
|
+
import { HttpResult, Either, ModelMethod, CloesceAst, Model, KeysOfType } from "../common.js";
|
|
2
2
|
import { OrmWasmExports } from "./wasm.js";
|
|
3
3
|
import { CrudContext } from "./crud.js";
|
|
4
|
+
/**
|
|
5
|
+
* Dependency injection container, mapping an object type name to an instance of that object.
|
|
6
|
+
*
|
|
7
|
+
* Comes with the WranglerEnv and Request by default.
|
|
8
|
+
*/
|
|
9
|
+
export type DependencyInjector = Map<string, any>;
|
|
4
10
|
/**
|
|
5
11
|
* Map of model names to their respective constructor.
|
|
6
12
|
*
|
|
@@ -8,11 +14,6 @@ import { CrudContext } from "./crud.js";
|
|
|
8
14
|
* is guaranteed to contain all model definitions.
|
|
9
15
|
*/
|
|
10
16
|
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
|
/**
|
|
17
18
|
* Meta information on the wrangler env and db bindings
|
|
18
19
|
*/
|
|
@@ -20,6 +21,68 @@ interface MetaWranglerEnv {
|
|
|
20
21
|
envName: string;
|
|
21
22
|
dbName: string;
|
|
22
23
|
}
|
|
24
|
+
export type MiddlewareFn = (request: Request, env: any, di: DependencyInjector) => Promise<HttpResult | void>;
|
|
25
|
+
export type ResultMiddlewareFn = (request: Request, env: any, di: DependencyInjector, result: HttpResult) => Promise<HttpResult | void>;
|
|
26
|
+
export declare class CloesceApp {
|
|
27
|
+
private globalMiddleware;
|
|
28
|
+
private modelMiddleware;
|
|
29
|
+
private methodMiddleware;
|
|
30
|
+
private resultMiddleware;
|
|
31
|
+
routePrefix: string;
|
|
32
|
+
/**
|
|
33
|
+
* Registers global middleware which runs before any route matching.
|
|
34
|
+
*
|
|
35
|
+
* @param m - The middleware function to register.
|
|
36
|
+
*/
|
|
37
|
+
onRequest(m: MiddlewareFn): void;
|
|
38
|
+
/**
|
|
39
|
+
* Registers middleware which runs after the response is generated, but before
|
|
40
|
+
* it is returned to the client.
|
|
41
|
+
*
|
|
42
|
+
* Optionally, return a new HttpResult to short-circuit the response.
|
|
43
|
+
*
|
|
44
|
+
* Errors thrown in response middleware are caught and returned as a 500 response.
|
|
45
|
+
*
|
|
46
|
+
* Errors thrown in earlier middleware or route processing are not caught here.
|
|
47
|
+
*
|
|
48
|
+
* @param m - The middleware function to register.
|
|
49
|
+
*/
|
|
50
|
+
onResult(m: ResultMiddlewareFn): void;
|
|
51
|
+
/**
|
|
52
|
+
* Registers middleware for a specific model type.
|
|
53
|
+
*
|
|
54
|
+
* Runs before request validation and method middleware.
|
|
55
|
+
*
|
|
56
|
+
* @typeParam T - The model type.
|
|
57
|
+
* @param ctor - The model constructor (used to derive its name).
|
|
58
|
+
* @param m - The middleware function to register.
|
|
59
|
+
*/
|
|
60
|
+
onModel<T>(ctor: new () => T, m: MiddlewareFn): void;
|
|
61
|
+
/**
|
|
62
|
+
* Registers middleware for a specific method on a model.
|
|
63
|
+
*
|
|
64
|
+
* Runs after model middleware and request validation.
|
|
65
|
+
*
|
|
66
|
+
* @typeParam T - The model type.
|
|
67
|
+
* @param ctor - The model constructor (used to derive its name).
|
|
68
|
+
* @param method - The method name on the model.
|
|
69
|
+
* @param m - The middleware function to register.
|
|
70
|
+
*/
|
|
71
|
+
onMethod<T>(ctor: new () => T, method: KeysOfType<T, (...args: any) => any>, m: MiddlewareFn): void;
|
|
72
|
+
/**
|
|
73
|
+
* Router entry point. Undergoes route matching, request validation, hydration, and method dispatch.
|
|
74
|
+
*/
|
|
75
|
+
private cloesce;
|
|
76
|
+
/**
|
|
77
|
+
* Runs the Cloesce app. Intended to be called from the generated workers code.
|
|
78
|
+
*/
|
|
79
|
+
run(request: Request, env: any, ast: CloesceAst, constructorRegistry: ModelConstructorRegistry, envMeta: MetaWranglerEnv): Promise<Response>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Given a request, this represents a map of each body / url param name to
|
|
83
|
+
* its actual value. Unknown, as the a request can be anything.
|
|
84
|
+
*/
|
|
85
|
+
export type RequestParamMap = Record<string, unknown>;
|
|
23
86
|
/**
|
|
24
87
|
* Singleton instance containing the cidl, constructor registry, and wasm binary.
|
|
25
88
|
* These values are guaranteed to never change throughout a workers lifetime.
|
|
@@ -33,19 +96,12 @@ export declare class RuntimeContainer {
|
|
|
33
96
|
static init(ast: CloesceAst, constructorRegistry: ModelConstructorRegistry, wasm?: WebAssembly.Instance): Promise<void>;
|
|
34
97
|
static get(): RuntimeContainer;
|
|
35
98
|
}
|
|
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
99
|
/**
|
|
44
100
|
* Matches a request to a method on a model.
|
|
45
101
|
* @param apiRoute The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
|
|
46
102
|
* @returns 404 or a `MatchedRoute`
|
|
47
103
|
*/
|
|
48
|
-
declare function matchRoute(request: Request, ast: CloesceAst,
|
|
104
|
+
declare function matchRoute(request: Request, ast: CloesceAst, routePrefix: string): Either<HttpResult, {
|
|
49
105
|
model: Model;
|
|
50
106
|
method: ModelMethod;
|
|
51
107
|
id: string | null;
|
|
@@ -63,7 +119,7 @@ declare function validateRequest(request: Request, ast: CloesceAst, model: Model
|
|
|
63
119
|
* Calls a method on a model given a list of parameters.
|
|
64
120
|
* @returns 500 on an uncaught client error, 200 with a result body on success
|
|
65
121
|
*/
|
|
66
|
-
declare function methodDispatch(crudCtx: CrudContext, instanceRegistry:
|
|
122
|
+
declare function methodDispatch(crudCtx: CrudContext, instanceRegistry: DependencyInjector, method: ModelMethod, params: Record<string, unknown>): Promise<HttpResult<unknown>>;
|
|
67
123
|
/**
|
|
68
124
|
* For testing purposes
|
|
69
125
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/router/router.ts"],"names":[],"mappings":"AACA,OAAO,EACL,UAAU,EACV,MAAM,EACN,WAAW,
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/router/router.ts"],"names":[],"mappings":"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"}
|
package/dist/router/router.js
CHANGED
|
@@ -1,6 +1,153 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { HttpResult, Either, isNullableType, getNavigationPropertyCidlType, NO_DATA_SOURCE, } from "../common.js";
|
|
2
|
+
import { mapSql, loadOrmWasm } from "./wasm.js";
|
|
3
3
|
import { CrudContext } from "./crud.js";
|
|
4
|
+
import { Orm } from "../ui/backend.js";
|
|
5
|
+
export class CloesceApp {
|
|
6
|
+
globalMiddleware = [];
|
|
7
|
+
modelMiddleware = new Map();
|
|
8
|
+
methodMiddleware = new Map();
|
|
9
|
+
resultMiddleware = [];
|
|
10
|
+
routePrefix = "api";
|
|
11
|
+
/**
|
|
12
|
+
* Registers global middleware which runs before any route matching.
|
|
13
|
+
*
|
|
14
|
+
* @param m - The middleware function to register.
|
|
15
|
+
*/
|
|
16
|
+
onRequest(m) {
|
|
17
|
+
this.globalMiddleware.push(m);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Registers middleware which runs after the response is generated, but before
|
|
21
|
+
* it is returned to the client.
|
|
22
|
+
*
|
|
23
|
+
* Optionally, return a new HttpResult to short-circuit the response.
|
|
24
|
+
*
|
|
25
|
+
* Errors thrown in response middleware are caught and returned as a 500 response.
|
|
26
|
+
*
|
|
27
|
+
* Errors thrown in earlier middleware or route processing are not caught here.
|
|
28
|
+
*
|
|
29
|
+
* @param m - The middleware function to register.
|
|
30
|
+
*/
|
|
31
|
+
onResult(m) {
|
|
32
|
+
this.resultMiddleware.push(m);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Registers middleware for a specific model type.
|
|
36
|
+
*
|
|
37
|
+
* Runs before request validation and method middleware.
|
|
38
|
+
*
|
|
39
|
+
* @typeParam T - The model type.
|
|
40
|
+
* @param ctor - The model constructor (used to derive its name).
|
|
41
|
+
* @param m - The middleware function to register.
|
|
42
|
+
*/
|
|
43
|
+
onModel(ctor, m) {
|
|
44
|
+
if (this.modelMiddleware.has(ctor.name)) {
|
|
45
|
+
this.modelMiddleware.get(ctor.name).push(m);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.modelMiddleware.set(ctor.name, [m]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Registers middleware for a specific method on a model.
|
|
53
|
+
*
|
|
54
|
+
* Runs after model middleware and request validation.
|
|
55
|
+
*
|
|
56
|
+
* @typeParam T - The model type.
|
|
57
|
+
* @param ctor - The model constructor (used to derive its name).
|
|
58
|
+
* @param method - The method name on the model.
|
|
59
|
+
* @param m - The middleware function to register.
|
|
60
|
+
*/
|
|
61
|
+
onMethod(ctor, method, m) {
|
|
62
|
+
if (!this.methodMiddleware.has(ctor.name)) {
|
|
63
|
+
this.methodMiddleware.set(ctor.name, new Map());
|
|
64
|
+
}
|
|
65
|
+
const methods = this.methodMiddleware.get(ctor.name);
|
|
66
|
+
if (!methods.has(method)) {
|
|
67
|
+
methods.set(method, []);
|
|
68
|
+
}
|
|
69
|
+
methods.get(method).push(m);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Router entry point. Undergoes route matching, request validation, hydration, and method dispatch.
|
|
73
|
+
*/
|
|
74
|
+
async cloesce(request, env, ast, constructorRegistry, di, d1) {
|
|
75
|
+
// Global middleware
|
|
76
|
+
for (const m of this.globalMiddleware) {
|
|
77
|
+
const res = await m(request, env, di);
|
|
78
|
+
if (res) {
|
|
79
|
+
return res;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Route match
|
|
83
|
+
const route = matchRoute(request, ast, this.routePrefix);
|
|
84
|
+
if (route.isLeft()) {
|
|
85
|
+
return route.value;
|
|
86
|
+
}
|
|
87
|
+
const { method, model, id } = route.unwrap();
|
|
88
|
+
// Model middleware
|
|
89
|
+
for (const m of this.modelMiddleware.get(model.name) ?? []) {
|
|
90
|
+
const res = await m(request, env, di);
|
|
91
|
+
if (res) {
|
|
92
|
+
return res;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Request validation
|
|
96
|
+
const validation = await validateRequest(request, ast, model, method, id);
|
|
97
|
+
if (validation.isLeft()) {
|
|
98
|
+
return validation.value;
|
|
99
|
+
}
|
|
100
|
+
const { params, dataSource } = validation.unwrap();
|
|
101
|
+
// Method middleware
|
|
102
|
+
for (const m of this.methodMiddleware.get(model.name)?.get(method.name) ??
|
|
103
|
+
[]) {
|
|
104
|
+
const res = await m(request, env, di);
|
|
105
|
+
if (res) {
|
|
106
|
+
return res;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Hydration
|
|
110
|
+
const crudCtx = await (async () => {
|
|
111
|
+
if (method.is_static) {
|
|
112
|
+
return Either.right(CrudContext.fromCtor(d1, constructorRegistry[model.name]));
|
|
113
|
+
}
|
|
114
|
+
const hydratedModel = await hydrateModel(constructorRegistry, d1, model, id, // id must exist after matchRoute
|
|
115
|
+
dataSource);
|
|
116
|
+
return hydratedModel.map((_) => CrudContext.fromInstance(d1, hydratedModel.value, constructorRegistry[model.name]));
|
|
117
|
+
})();
|
|
118
|
+
if (crudCtx.isLeft()) {
|
|
119
|
+
return crudCtx.value;
|
|
120
|
+
}
|
|
121
|
+
// Method dispatch
|
|
122
|
+
return await methodDispatch(crudCtx.unwrap(), di, method, params);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Runs the Cloesce app. Intended to be called from the generated workers code.
|
|
126
|
+
*/
|
|
127
|
+
async run(request, env, ast, constructorRegistry, envMeta) {
|
|
128
|
+
const di = new Map();
|
|
129
|
+
di.set(envMeta.envName, env);
|
|
130
|
+
di.set("Request", request);
|
|
131
|
+
await RuntimeContainer.init(ast, constructorRegistry);
|
|
132
|
+
const d1 = env[envMeta.dbName]; // TODO: multiple dbs
|
|
133
|
+
try {
|
|
134
|
+
// Core cloesce processing
|
|
135
|
+
const response = await this.cloesce(request, env, ast, constructorRegistry, di, d1);
|
|
136
|
+
// Response middleware
|
|
137
|
+
for (const m of this.resultMiddleware) {
|
|
138
|
+
const res = await m(request, env, di, response);
|
|
139
|
+
if (res) {
|
|
140
|
+
return res.toResponse();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return response.toResponse();
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
console.error(JSON.stringify(e));
|
|
147
|
+
return HttpResult.fail(500, e.toString()).toResponse();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
4
151
|
/**
|
|
5
152
|
* Singleton instance containing the cidl, constructor registry, and wasm binary.
|
|
6
153
|
* These values are guaranteed to never change throughout a workers lifetime.
|
|
@@ -25,97 +172,29 @@ export class RuntimeContainer {
|
|
|
25
172
|
return this.instance;
|
|
26
173
|
}
|
|
27
174
|
}
|
|
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
175
|
/**
|
|
99
176
|
* Matches a request to a method on a model.
|
|
100
177
|
* @param apiRoute The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
|
|
101
178
|
* @returns 404 or a `MatchedRoute`
|
|
102
179
|
*/
|
|
103
|
-
function matchRoute(request, ast,
|
|
180
|
+
function matchRoute(request, ast, routePrefix) {
|
|
104
181
|
const url = new URL(request.url);
|
|
182
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
183
|
+
const prefix = routePrefix.split("/").filter(Boolean);
|
|
105
184
|
// Error state: We expect an exact request format, and expect that the model
|
|
106
185
|
// and are apart of the CIDL
|
|
107
|
-
const notFound = (e) => left(
|
|
108
|
-
const
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
186
|
+
const notFound = (e) => Either.left(HttpResult.fail(404, `Path not found: ${e} ${url.pathname}`));
|
|
187
|
+
for (const p of prefix) {
|
|
188
|
+
if (parts.shift() !== p)
|
|
189
|
+
return notFound(`Missing prefix segment "${p}"`);
|
|
190
|
+
}
|
|
191
|
+
if (parts.length < 2) {
|
|
113
192
|
return notFound("Expected /model/method or /model/:id/method");
|
|
114
193
|
}
|
|
115
194
|
// Attempt to extract from routeParts
|
|
116
|
-
const modelName =
|
|
117
|
-
const methodName =
|
|
118
|
-
const id =
|
|
195
|
+
const modelName = parts[0];
|
|
196
|
+
const methodName = parts[parts.length - 1];
|
|
197
|
+
const id = parts.length === 3 ? parts[1] : null;
|
|
119
198
|
const model = ast.models[modelName];
|
|
120
199
|
if (!model) {
|
|
121
200
|
return notFound(`Unknown model ${modelName}`);
|
|
@@ -127,7 +206,7 @@ function matchRoute(request, ast, apiRoute) {
|
|
|
127
206
|
if (request.method !== method.http_verb) {
|
|
128
207
|
return notFound("Unmatched HTTP method");
|
|
129
208
|
}
|
|
130
|
-
return right({
|
|
209
|
+
return Either.right({
|
|
131
210
|
model,
|
|
132
211
|
method,
|
|
133
212
|
id,
|
|
@@ -140,7 +219,7 @@ function matchRoute(request, ast, apiRoute) {
|
|
|
140
219
|
*/
|
|
141
220
|
async function validateRequest(request, ast, model, method, id) {
|
|
142
221
|
// Error state: any missing parameter, body, or malformed input will exit with 400.
|
|
143
|
-
const invalidRequest = (e) => left(
|
|
222
|
+
const invalidRequest = (e) => Either.left(HttpResult.fail(400, `Invalid Request Body: ${e}`));
|
|
144
223
|
if (!method.is_static && id == null) {
|
|
145
224
|
return invalidRequest("Id's are required for instantiated methods.");
|
|
146
225
|
}
|
|
@@ -183,7 +262,7 @@ async function validateRequest(request, ast, model, method, id) {
|
|
|
183
262
|
!(dataSource in model.data_sources)) {
|
|
184
263
|
return invalidRequest(`Unknown data source ${dataSource}`);
|
|
185
264
|
}
|
|
186
|
-
return right({ params, dataSource });
|
|
265
|
+
return Either.right({ params, dataSource });
|
|
187
266
|
}
|
|
188
267
|
/**
|
|
189
268
|
* Queries D1 for a particular model's ID, then transforms the SQL column output into
|
|
@@ -195,17 +274,19 @@ async function validateRequest(request, ast, model, method, id) {
|
|
|
195
274
|
async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
|
|
196
275
|
// Error state: If the D1 database has been tweaked outside of Cloesce
|
|
197
276
|
// resulting in a malformed query, exit with a 500.
|
|
198
|
-
const malformedQuery = (e) => left(
|
|
277
|
+
const malformedQuery = (e) => Either.left(HttpResult.fail(500, `Error in hydration query, is the database out of sync with the backend?: ${e instanceof Error ? e.message : String(e)}`));
|
|
199
278
|
// Error state: If no record is found for the id, return a 404
|
|
200
|
-
const missingRecord = left(
|
|
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}" = ?`;
|
|
279
|
+
const missingRecord = Either.left(HttpResult.fail(404, "Record not found"));
|
|
205
280
|
// Query DB
|
|
206
281
|
let records;
|
|
207
282
|
try {
|
|
208
|
-
|
|
283
|
+
let includeTree = dataSource === NO_DATA_SOURCE
|
|
284
|
+
? null
|
|
285
|
+
: constructorRegistry[model.name][dataSource];
|
|
286
|
+
records = await d1
|
|
287
|
+
.prepare(Orm.getQuery(constructorRegistry[model.name], includeTree))
|
|
288
|
+
.bind(id)
|
|
289
|
+
.run();
|
|
209
290
|
if (!records?.results) {
|
|
210
291
|
return missingRecord;
|
|
211
292
|
}
|
|
@@ -217,8 +298,8 @@ async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
|
|
|
217
298
|
return malformedQuery(e);
|
|
218
299
|
}
|
|
219
300
|
// Hydrate
|
|
220
|
-
const models =
|
|
221
|
-
return right(models[0]);
|
|
301
|
+
const models = mapSql(constructorRegistry[model.name], records.results, model.data_sources[dataSource]?.tree ?? {}).value;
|
|
302
|
+
return Either.right(models[0]);
|
|
222
303
|
}
|
|
223
304
|
/**
|
|
224
305
|
* Calls a method on a model given a list of parameters.
|
|
@@ -226,7 +307,7 @@ async function hydrateModel(constructorRegistry, d1, model, id, dataSource) {
|
|
|
226
307
|
*/
|
|
227
308
|
async function methodDispatch(crudCtx, instanceRegistry, method, params) {
|
|
228
309
|
// Error state: Client code ran into an uncaught exception.
|
|
229
|
-
const uncaughtException = (e) =>
|
|
310
|
+
const uncaughtException = (e) => HttpResult.fail(500, `Uncaught exception in method dispatch: ${e instanceof Error ? e.message : String(e)}`);
|
|
230
311
|
const paramArray = [];
|
|
231
312
|
for (const param of method.parameters) {
|
|
232
313
|
if (params[param.name]) {
|
|
@@ -238,7 +319,7 @@ async function methodDispatch(crudCtx, instanceRegistry, method, params) {
|
|
|
238
319
|
if (!injected) {
|
|
239
320
|
// Error state: Injected parameters cannot be found at compile time, only at runtime.
|
|
240
321
|
// If a injected reference does not exist, throw a 500.
|
|
241
|
-
return
|
|
322
|
+
return HttpResult.fail(500, `An injected parameter was missing from the instance registry: ${JSON.stringify(param.cidl_type)}`);
|
|
242
323
|
}
|
|
243
324
|
paramArray.push(injected);
|
|
244
325
|
}
|
|
@@ -246,12 +327,12 @@ async function methodDispatch(crudCtx, instanceRegistry, method, params) {
|
|
|
246
327
|
const resultWrapper = (res) => {
|
|
247
328
|
const rt = method.return_type;
|
|
248
329
|
if (rt === null) {
|
|
249
|
-
return
|
|
330
|
+
return HttpResult.ok(200);
|
|
250
331
|
}
|
|
251
332
|
if (typeof rt === "object" && rt !== null && "HttpResult" in rt) {
|
|
252
333
|
return res;
|
|
253
334
|
}
|
|
254
|
-
return
|
|
335
|
+
return HttpResult.ok(200, res);
|
|
255
336
|
};
|
|
256
337
|
try {
|
|
257
338
|
const res = await crudCtx.interceptCrud(method.name)(...paramArray);
|
|
@@ -261,6 +342,11 @@ async function methodDispatch(crudCtx, instanceRegistry, method, params) {
|
|
|
261
342
|
return uncaughtException(e);
|
|
262
343
|
}
|
|
263
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Runtime type validation for CIDL types.
|
|
347
|
+
*
|
|
348
|
+
* Returns true if the value matches the CIDL type, false otherwise.
|
|
349
|
+
*/
|
|
264
350
|
function validateCidlType(ast, value, cidlType, isPartial) {
|
|
265
351
|
if (value === undefined)
|
|
266
352
|
return isPartial;
|
|
@@ -281,8 +367,11 @@ function validateCidlType(ast, value, cidlType, isPartial) {
|
|
|
281
367
|
return !Number.isNaN(Number(value));
|
|
282
368
|
case "Text":
|
|
283
369
|
return typeof value === "string";
|
|
284
|
-
case "
|
|
285
|
-
return
|
|
370
|
+
case "Boolean":
|
|
371
|
+
return typeof value === "boolean";
|
|
372
|
+
case "DateIso":
|
|
373
|
+
const date = new Date(value);
|
|
374
|
+
return !isNaN(date.getTime());
|
|
286
375
|
default:
|
|
287
376
|
return false;
|
|
288
377
|
}
|
|
@@ -323,6 +412,7 @@ function validateCidlType(ast, value, cidlType, isPartial) {
|
|
|
323
412
|
if (!poo.attributes.every((attr) => validateCidlType(ast, valueObj[attr.name], attr.cidl_type, isPartial))) {
|
|
324
413
|
return false;
|
|
325
414
|
}
|
|
415
|
+
return true;
|
|
326
416
|
}
|
|
327
417
|
if ("Array" in cidlType) {
|
|
328
418
|
const arr = cidlType.Array;
|
|
@@ -338,15 +428,6 @@ function validateCidlType(ast, value, cidlType, isPartial) {
|
|
|
338
428
|
}
|
|
339
429
|
return false;
|
|
340
430
|
}
|
|
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
431
|
/**
|
|
351
432
|
* For testing purposes
|
|
352
433
|
*/
|
package/dist/router/wasm.d.ts
CHANGED
|
@@ -10,8 +10,9 @@ export interface OrmWasmExports {
|
|
|
10
10
|
set_meta_ptr(ptr: number, len: number): number;
|
|
11
11
|
alloc(len: number): number;
|
|
12
12
|
dealloc(ptr: number, len: number): void;
|
|
13
|
-
|
|
13
|
+
map_sql(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
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
|
+
list_models(model_name_ptr: number, model_name_len: number, include_tree_ptr: number, include_tree_len: number, tag_cte_ptr: number, tag_cte_len: number, custom_from_ptr: number, custom_from_len: number): boolean;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* RAII for wasm memory
|
|
@@ -28,10 +29,16 @@ export declare class WasmResource {
|
|
|
28
29
|
static fromString(str: string, wasm: OrmWasmExports): WasmResource;
|
|
29
30
|
}
|
|
30
31
|
export declare function loadOrmWasm(ast: CloesceAst, wasm?: WebAssembly.Instance): Promise<OrmWasmExports>;
|
|
31
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Invokes a WASM ORM function with the provided arguments, handling memory
|
|
34
|
+
* allocation and deallocation.
|
|
35
|
+
*
|
|
36
|
+
* Returns an Either where Left is an error message and Right the raw string result.
|
|
37
|
+
*/
|
|
38
|
+
export declare function invokeOrmWasm(fn: (...args: number[]) => boolean, args: WasmResource[], wasm: OrmWasmExports): Either<string, string>;
|
|
32
39
|
/**
|
|
33
40
|
* Calls `object_relational_mapping` to turn a row of SQL records into
|
|
34
41
|
* an instantiated object.
|
|
35
42
|
*/
|
|
36
|
-
export declare function
|
|
43
|
+
export declare function mapSql<T extends object>(ctor: new () => T, records: Record<string, any>[], includeTree: IncludeTree<T> | CidlIncludeTree | null): Either<string, T[]>;
|
|
37
44
|
//# sourceMappingURL=wasm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wasm.d.ts","sourceRoot":"","sources":["../../src/router/wasm.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"wasm.d.ts","sourceRoot":"","sources":["../../src/router/wasm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAS,MAAM,cAAc,CAAC;AAC1E,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,OAAO,CACL,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;IAEX,WAAW,CACT,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,EACtB,gBAAgB,EAAE,MAAM,EACxB,gBAAgB,EAAE,MAAM,EACxB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,eAAe,EAAE,MAAM,GACtB,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;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,EAClC,IAAI,EAAE,YAAY,EAAE,EACpB,IAAI,EAAE,cAAc,GACnB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkBxB;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,EACrC,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,CAsDrB"}
|