cloesce 0.0.4-unstable.9 → 0.0.5-unstable.1
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/ast.d.ts +141 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +30 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +375 -279
- package/dist/extractor/err.d.ts +32 -0
- package/dist/extractor/err.d.ts.map +1 -0
- package/dist/extractor/err.js +138 -0
- package/dist/extractor/extract.d.ts +62 -15
- package/dist/extractor/extract.d.ts.map +1 -1
- package/dist/extractor/extract.js +835 -647
- package/dist/generator.wasm +0 -0
- package/dist/orm.wasm +0 -0
- package/dist/router/crud.d.ts +5 -20
- package/dist/router/crud.d.ts.map +1 -1
- package/dist/router/crud.js +53 -64
- package/dist/router/router.d.ts +153 -95
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +451 -407
- package/dist/router/validator.d.ts +32 -0
- package/dist/router/validator.d.ts.map +1 -0
- package/dist/router/validator.js +193 -0
- package/dist/router/wasm.d.ts +59 -24
- package/dist/router/wasm.d.ts.map +1 -1
- package/dist/router/wasm.js +93 -79
- package/dist/ui/backend.d.ts +224 -180
- package/dist/ui/backend.d.ts.map +1 -1
- package/dist/ui/backend.js +264 -250
- package/dist/ui/client.d.ts +7 -5
- package/dist/ui/client.d.ts.map +1 -1
- package/dist/ui/client.js +2 -7
- package/dist/ui/common.d.ts +126 -0
- package/dist/ui/common.d.ts.map +1 -0
- package/dist/ui/common.js +203 -0
- package/package.json +4 -3
- package/README.md +0 -632
- package/dist/common.d.ts +0 -237
- package/dist/common.d.ts.map +0 -1
- package/dist/common.js +0 -169
package/dist/router/router.js
CHANGED
|
@@ -1,275 +1,395 @@
|
|
|
1
|
-
import { Either, isNullableType, getNavigationPropertyCidlType, NO_DATA_SOURCE, } from "../common.js";
|
|
2
1
|
import { mapSql, loadOrmWasm } from "./wasm.js";
|
|
3
|
-
import {
|
|
4
|
-
import { Orm } from "../ui/backend.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
methodMiddleware = new Map();
|
|
9
|
-
responseMiddleware = [];
|
|
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
|
-
onResponse(m) {
|
|
32
|
-
this.responseMiddleware.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 toResponse(res);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Route match
|
|
83
|
-
const route = matchRoute(request, ast, this.routePrefix);
|
|
84
|
-
if (route.isLeft()) {
|
|
85
|
-
return toResponse(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 toResponse(res);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// Request validation
|
|
96
|
-
const validation = await validateRequest(request, ast, model, method, id);
|
|
97
|
-
if (validation.isLeft()) {
|
|
98
|
-
return toResponse(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 toResponse(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 toResponse(crudCtx.value);
|
|
120
|
-
}
|
|
121
|
-
// Method dispatch
|
|
122
|
-
return toResponse(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.responseMiddleware) {
|
|
138
|
-
const res = await m(request, env, di, response);
|
|
139
|
-
if (res) {
|
|
140
|
-
return toResponse(res);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return response;
|
|
144
|
-
}
|
|
145
|
-
catch (e) {
|
|
146
|
-
console.error(JSON.stringify(e));
|
|
147
|
-
return new Response(JSON.stringify({
|
|
148
|
-
ok: false,
|
|
149
|
-
status: 500,
|
|
150
|
-
message: e.toString(),
|
|
151
|
-
}), {
|
|
152
|
-
status: 500,
|
|
153
|
-
headers: { "Content-Type": "application/json" },
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
2
|
+
import { proxyCrud } from "./crud.js";
|
|
3
|
+
import { HttpResult, Orm } from "../ui/backend.js";
|
|
4
|
+
import { Either } from "../ui/common.js";
|
|
5
|
+
import { NO_DATA_SOURCE, MediaType } from "../ast.js";
|
|
6
|
+
import { RuntimeValidator } from "./validator.js";
|
|
158
7
|
/**
|
|
159
8
|
* Singleton instance containing the cidl, constructor registry, and wasm binary.
|
|
160
9
|
* These values are guaranteed to never change throughout a workers lifetime.
|
|
161
10
|
*/
|
|
162
11
|
export class RuntimeContainer {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
12
|
+
ast;
|
|
13
|
+
constructorRegistry;
|
|
14
|
+
wasm;
|
|
15
|
+
static instance;
|
|
16
|
+
constructor(ast, constructorRegistry, wasm) {
|
|
17
|
+
this.ast = ast;
|
|
18
|
+
this.constructorRegistry = constructorRegistry;
|
|
19
|
+
this.wasm = wasm;
|
|
20
|
+
}
|
|
21
|
+
static async init(ast, constructorRegistry, wasm) {
|
|
22
|
+
if (this.instance) return;
|
|
23
|
+
const wasmAbi = await loadOrmWasm(ast, wasm);
|
|
24
|
+
this.instance = new RuntimeContainer(ast, constructorRegistry, wasmAbi);
|
|
25
|
+
}
|
|
26
|
+
static get() {
|
|
27
|
+
return this.instance;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Disposes the singleton instance. For testing purposes only.
|
|
31
|
+
*/
|
|
32
|
+
static dispose() {
|
|
33
|
+
this.instance = undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Expected states in which the router may exit.
|
|
38
|
+
*/
|
|
39
|
+
export var RouterError;
|
|
40
|
+
(function (RouterError) {
|
|
41
|
+
RouterError[(RouterError["UnknownPrefix"] = 0)] = "UnknownPrefix";
|
|
42
|
+
RouterError[(RouterError["UnknownRoute"] = 1)] = "UnknownRoute";
|
|
43
|
+
RouterError[(RouterError["UnmatchedHttpVerb"] = 2)] = "UnmatchedHttpVerb";
|
|
44
|
+
RouterError[(RouterError["InstantiatedMethodMissingId"] = 3)] =
|
|
45
|
+
"InstantiatedMethodMissingId";
|
|
46
|
+
RouterError[(RouterError["RequestMissingBody"] = 4)] = "RequestMissingBody";
|
|
47
|
+
RouterError[(RouterError["RequestBodyMissingParameters"] = 5)] =
|
|
48
|
+
"RequestBodyMissingParameters";
|
|
49
|
+
RouterError[(RouterError["RequestBodyInvalidParameter"] = 6)] =
|
|
50
|
+
"RequestBodyInvalidParameter";
|
|
51
|
+
RouterError[(RouterError["InstantiatedMethodMissingDataSource"] = 7)] =
|
|
52
|
+
"InstantiatedMethodMissingDataSource";
|
|
53
|
+
RouterError[(RouterError["MissingDependency"] = 8)] = "MissingDependency";
|
|
54
|
+
RouterError[(RouterError["InvalidDatabaseQuery"] = 9)] =
|
|
55
|
+
"InvalidDatabaseQuery";
|
|
56
|
+
RouterError[(RouterError["ModelNotFound"] = 10)] = "ModelNotFound";
|
|
57
|
+
RouterError[(RouterError["UncaughtException"] = 11)] = "UncaughtException";
|
|
58
|
+
})(RouterError || (RouterError = {}));
|
|
59
|
+
export class CloesceApp {
|
|
60
|
+
routePrefix = "api";
|
|
61
|
+
globalMiddleware = [];
|
|
62
|
+
/**
|
|
63
|
+
* Registers global middleware which runs before any route matching.
|
|
64
|
+
*
|
|
65
|
+
* TODO: Middleware may violate the API contract and return unexpected types
|
|
66
|
+
*
|
|
67
|
+
* @param m - The middleware function to register.
|
|
68
|
+
*/
|
|
69
|
+
onRequest(m) {
|
|
70
|
+
this.globalMiddleware.push(m);
|
|
71
|
+
}
|
|
72
|
+
resultMiddleware = [];
|
|
73
|
+
/**
|
|
74
|
+
* Registers middleware which runs after the response is generated, but before
|
|
75
|
+
* it is returned to the client.
|
|
76
|
+
*
|
|
77
|
+
* Optionally, return a new HttpResult to short-circuit the response.
|
|
78
|
+
*
|
|
79
|
+
* Errors thrown in response middleware are caught and returned as a 500 response.
|
|
80
|
+
*
|
|
81
|
+
* TODO: Middleware may violate the API contract and return unexpected types
|
|
82
|
+
*
|
|
83
|
+
* @param m - The middleware function to register.
|
|
84
|
+
*/
|
|
85
|
+
onResult(m) {
|
|
86
|
+
this.resultMiddleware.push(m);
|
|
87
|
+
}
|
|
88
|
+
namespaceMiddleware = new Map();
|
|
89
|
+
/**
|
|
90
|
+
* Registers middleware for a specific namespace (being, a model or service)
|
|
91
|
+
*
|
|
92
|
+
* Runs before request validation and method middleware.
|
|
93
|
+
*
|
|
94
|
+
* TODO: Middleware may violate the API contract and return unexpected types
|
|
95
|
+
*
|
|
96
|
+
* @typeParam T - The namespace type
|
|
97
|
+
* @param ctor - The namespace's constructor (used to derive its name).
|
|
98
|
+
* @param m - The middleware function to register.
|
|
99
|
+
*/
|
|
100
|
+
onNamespace(ctor, m) {
|
|
101
|
+
if (this.namespaceMiddleware.has(ctor.name)) {
|
|
102
|
+
this.namespaceMiddleware.get(ctor.name).push(m);
|
|
103
|
+
} else {
|
|
104
|
+
this.namespaceMiddleware.set(ctor.name, [m]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
methodMiddleware = new Map();
|
|
108
|
+
/**
|
|
109
|
+
* Registers middleware for a specific method on a namespace
|
|
110
|
+
*
|
|
111
|
+
* Runs after namespace middleware and request validation.
|
|
112
|
+
*
|
|
113
|
+
* TODO: Middleware may violate the API contract and return unexpected types
|
|
114
|
+
*
|
|
115
|
+
* @typeParam T - The namespace type
|
|
116
|
+
* @param ctor - The namespace constructor
|
|
117
|
+
* @param method - The method name on the namespace.
|
|
118
|
+
* @param m - The middleware function to register.
|
|
119
|
+
*/
|
|
120
|
+
onMethod(ctor, method, m) {
|
|
121
|
+
if (!this.methodMiddleware.has(ctor.name)) {
|
|
122
|
+
this.methodMiddleware.set(ctor.name, new Map());
|
|
123
|
+
}
|
|
124
|
+
const methods = this.methodMiddleware.get(ctor.name);
|
|
125
|
+
if (!methods.has(method)) {
|
|
126
|
+
methods.set(method, []);
|
|
127
|
+
}
|
|
128
|
+
methods.get(method).push(m);
|
|
129
|
+
}
|
|
130
|
+
async router(request, env, ast, ctorReg, di) {
|
|
131
|
+
// Global middleware
|
|
132
|
+
for (const m of this.globalMiddleware) {
|
|
133
|
+
const res = await m(di);
|
|
134
|
+
if (res) {
|
|
135
|
+
return res;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Route match
|
|
139
|
+
const routeRes = matchRoute(request, ast, this.routePrefix);
|
|
140
|
+
if (routeRes.isLeft()) {
|
|
141
|
+
return routeRes.value;
|
|
142
|
+
}
|
|
143
|
+
const route = routeRes.unwrap();
|
|
144
|
+
// Model middleware
|
|
145
|
+
for (const m of this.namespaceMiddleware.get(route.namespace) ?? []) {
|
|
146
|
+
const res = await m(di);
|
|
147
|
+
if (res) {
|
|
148
|
+
return res;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Request validation
|
|
152
|
+
const validation = await validateRequest(request, ast, ctorReg, route);
|
|
153
|
+
if (validation.isLeft()) {
|
|
154
|
+
return validation.value;
|
|
155
|
+
}
|
|
156
|
+
const { params, dataSource } = validation.unwrap();
|
|
157
|
+
// Method middleware
|
|
158
|
+
for (const m of this.methodMiddleware
|
|
159
|
+
.get(route.namespace)
|
|
160
|
+
?.get(route.method.name) ?? []) {
|
|
161
|
+
const res = await m(di);
|
|
162
|
+
if (res) {
|
|
163
|
+
return res;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Hydration
|
|
167
|
+
const hydrated = await (async () => {
|
|
168
|
+
if (route.kind == "model") {
|
|
169
|
+
// TODO: Support multiple D1 bindings
|
|
170
|
+
// It's been verified by the compiler that wrangler_env exists if a D1 model is present
|
|
171
|
+
const d1 = env[ast.wrangler_env.db_binding];
|
|
172
|
+
// Proxy CRUD
|
|
173
|
+
if (route.method.is_static) {
|
|
174
|
+
return Either.right(
|
|
175
|
+
proxyCrud(ctorReg[route.namespace], ctorReg[route.namespace], d1),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return await hydrateModelD1(
|
|
179
|
+
ctorReg,
|
|
180
|
+
d1,
|
|
181
|
+
route.model,
|
|
182
|
+
route.id,
|
|
183
|
+
dataSource,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
// Services
|
|
187
|
+
if (route.method.is_static) {
|
|
188
|
+
return Either.right(ctorReg[route.namespace]);
|
|
189
|
+
}
|
|
190
|
+
return Either.right(di.get(route.namespace));
|
|
191
|
+
})();
|
|
192
|
+
if (hydrated.isLeft()) {
|
|
193
|
+
return hydrated.value;
|
|
194
|
+
}
|
|
195
|
+
// Method dispatch
|
|
196
|
+
return await methodDispatch(hydrated.unwrap(), di, route, params);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Runs the Cloesce app. Intended to be called from the generated workers code.
|
|
200
|
+
*/
|
|
201
|
+
async run(request, env, ast, ctorReg) {
|
|
202
|
+
await RuntimeContainer.init(ast, ctorReg);
|
|
203
|
+
const di = new Map();
|
|
204
|
+
if (ast.wrangler_env) {
|
|
205
|
+
di.set(ast.wrangler_env.name, env);
|
|
206
|
+
}
|
|
207
|
+
di.set("Request", request);
|
|
208
|
+
// Note: Services are in topological order
|
|
209
|
+
for (const name in ast.services) {
|
|
210
|
+
const service = ast.services[name];
|
|
211
|
+
for (const attr of service.attributes) {
|
|
212
|
+
const injected = di.get(attr.injected);
|
|
213
|
+
if (!injected) {
|
|
214
|
+
return exit(
|
|
215
|
+
500,
|
|
216
|
+
RouterError.MissingDependency,
|
|
217
|
+
`An injected parameter was missing from the instance registry: ${JSON.stringify(attr.injected)}`,
|
|
218
|
+
)
|
|
219
|
+
.unwrapLeft()
|
|
220
|
+
.toResponse();
|
|
221
|
+
}
|
|
222
|
+
service[attr.var_name] = injected;
|
|
223
|
+
}
|
|
224
|
+
// Inject services
|
|
225
|
+
di.set(name, Object.assign(new ctorReg[name](), service));
|
|
180
226
|
}
|
|
227
|
+
try {
|
|
228
|
+
let httpResult = await this.router(request, env, ast, ctorReg, di);
|
|
229
|
+
// Response middleware
|
|
230
|
+
for (const m of this.resultMiddleware) {
|
|
231
|
+
const res = await m(di, httpResult);
|
|
232
|
+
if (res) {
|
|
233
|
+
return res.toResponse();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return httpResult.toResponse();
|
|
237
|
+
} catch (e) {
|
|
238
|
+
let debug;
|
|
239
|
+
if (e instanceof Error) {
|
|
240
|
+
debug = {
|
|
241
|
+
name: e.name,
|
|
242
|
+
message: e.message,
|
|
243
|
+
stack: e.stack,
|
|
244
|
+
cause: e.cause,
|
|
245
|
+
};
|
|
246
|
+
} else {
|
|
247
|
+
debug = {
|
|
248
|
+
name: "NonErrorThrown",
|
|
249
|
+
message: typeof e === "string" ? e : JSON.stringify(e),
|
|
250
|
+
stack: undefined,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const res = HttpResult.fail(500, JSON.stringify(debug));
|
|
254
|
+
console.error(
|
|
255
|
+
"An uncaught error occurred in the Cloesce Router: ",
|
|
256
|
+
debug,
|
|
257
|
+
);
|
|
258
|
+
return res.toResponse();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
181
261
|
}
|
|
182
262
|
/**
|
|
183
|
-
* Matches a request to
|
|
263
|
+
* Matches a request to an ApiInvocation
|
|
184
264
|
* @param apiRoute The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
|
|
185
|
-
* @returns 404 or a
|
|
265
|
+
* @returns 404 or a matched route.
|
|
186
266
|
*/
|
|
187
267
|
function matchRoute(request, ast, routePrefix) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (!model) {
|
|
207
|
-
return notFound(`Unknown model ${modelName}`);
|
|
208
|
-
}
|
|
268
|
+
const url = new URL(request.url);
|
|
269
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
270
|
+
const prefix = routePrefix.split("/").filter(Boolean);
|
|
271
|
+
// Error state: We expect an exact request format, and expect that the model
|
|
272
|
+
// and are apart of the CIDL
|
|
273
|
+
const notFound = (c) => exit(404, c, "Unknown route");
|
|
274
|
+
for (const p of prefix) {
|
|
275
|
+
if (parts.shift() !== p) return notFound(RouterError.UnknownPrefix);
|
|
276
|
+
}
|
|
277
|
+
if (parts.length < 2) {
|
|
278
|
+
return notFound(RouterError.UnknownPrefix);
|
|
279
|
+
}
|
|
280
|
+
// Extract pattern
|
|
281
|
+
const namespace = parts[0];
|
|
282
|
+
const methodName = parts[parts.length - 1];
|
|
283
|
+
const id = parts.length === 3 ? parts[1] : null;
|
|
284
|
+
const model = ast.models[namespace];
|
|
285
|
+
if (model) {
|
|
209
286
|
const method = model.methods[methodName];
|
|
210
|
-
if (!method)
|
|
211
|
-
|
|
287
|
+
if (!method) return notFound(RouterError.UnknownRoute);
|
|
288
|
+
if (request.method !== method.http_verb) {
|
|
289
|
+
return notFound(RouterError.UnmatchedHttpVerb);
|
|
212
290
|
}
|
|
291
|
+
return Either.right({
|
|
292
|
+
kind: "model",
|
|
293
|
+
namespace,
|
|
294
|
+
method,
|
|
295
|
+
model,
|
|
296
|
+
id,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const service = ast.services[namespace];
|
|
300
|
+
if (service) {
|
|
301
|
+
const method = service.methods[methodName];
|
|
302
|
+
// Services do not have IDs.
|
|
303
|
+
if (!method || id) return notFound(RouterError.UnknownRoute);
|
|
213
304
|
if (request.method !== method.http_verb) {
|
|
214
|
-
|
|
305
|
+
return notFound(RouterError.UnmatchedHttpVerb);
|
|
215
306
|
}
|
|
216
307
|
return Either.right({
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
308
|
+
kind: "service",
|
|
309
|
+
namespace,
|
|
310
|
+
method,
|
|
311
|
+
service,
|
|
312
|
+
id: null,
|
|
220
313
|
});
|
|
314
|
+
}
|
|
315
|
+
return notFound(RouterError.UnknownRoute);
|
|
221
316
|
}
|
|
222
317
|
/**
|
|
223
318
|
* Validates the request's body/search params against a ModelMethod
|
|
224
319
|
* @returns 400 or a `RequestParamMap` consisting of each parameters name mapped to its value, and
|
|
225
320
|
* a data source
|
|
226
321
|
*/
|
|
227
|
-
async function validateRequest(request, ast,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
322
|
+
async function validateRequest(request, ast, ctorReg, route) {
|
|
323
|
+
// Error state: any missing parameter, body, or malformed input will exit with 400.
|
|
324
|
+
const invalidRequest = (c) => exit(400, c, "Invalid Request Body");
|
|
325
|
+
// Models must have an ID on instantiated methods.
|
|
326
|
+
if (route.kind === "model" && !route.method.is_static && route.id == null) {
|
|
327
|
+
return invalidRequest(RouterError.InstantiatedMethodMissingId);
|
|
328
|
+
}
|
|
329
|
+
// Filter out injected parameters
|
|
330
|
+
const requiredParams = route.method.parameters.filter(
|
|
331
|
+
(p) => !(typeof p.cidl_type === "object" && "Inject" in p.cidl_type),
|
|
332
|
+
);
|
|
333
|
+
const url = new URL(request.url);
|
|
334
|
+
let params = Object.fromEntries(url.searchParams.entries());
|
|
335
|
+
// Extract all method parameters from the body
|
|
336
|
+
if (route.method.http_verb !== "GET") {
|
|
337
|
+
try {
|
|
338
|
+
switch (route.method.parameters_media) {
|
|
339
|
+
case MediaType.Json: {
|
|
340
|
+
const body = await request.json();
|
|
341
|
+
params = { ...params, ...body };
|
|
342
|
+
break;
|
|
245
343
|
}
|
|
246
|
-
|
|
247
|
-
|
|
344
|
+
case MediaType.Octet: {
|
|
345
|
+
// Octet streams are verified by the Cloesce compiler to have
|
|
346
|
+
// one Stream type
|
|
347
|
+
const streamParam = requiredParams.find(
|
|
348
|
+
(p) => typeof p.cidl_type === "string" && p.cidl_type === "Stream",
|
|
349
|
+
);
|
|
350
|
+
params[streamParam.name] = request.body;
|
|
351
|
+
break;
|
|
248
352
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (!requiredParams.every((p) => p.name in params)) {
|
|
252
|
-
return invalidRequest(`Missing parameters.`);
|
|
253
|
-
}
|
|
254
|
-
// Validate all parameters type
|
|
255
|
-
for (const p of requiredParams) {
|
|
256
|
-
const value = params[p.name];
|
|
257
|
-
const isPartial = typeof p.cidl_type !== "string" && "Partial" in p.cidl_type;
|
|
258
|
-
if (!validateCidlType(ast, value, p.cidl_type, isPartial)) {
|
|
259
|
-
return invalidRequest("Invalid parameters.");
|
|
353
|
+
default: {
|
|
354
|
+
throw new Error("not implemented");
|
|
260
355
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
return invalidRequest(RouterError.RequestMissingBody);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!requiredParams.every((p) => p.name in params)) {
|
|
362
|
+
return invalidRequest(RouterError.RequestBodyMissingParameters);
|
|
363
|
+
}
|
|
364
|
+
// Validate all parameters type
|
|
365
|
+
// Octet streams can be passed as is
|
|
366
|
+
if (route.method.parameters_media !== MediaType.Octet) {
|
|
367
|
+
for (const p of requiredParams) {
|
|
368
|
+
const res = RuntimeValidator.validate(
|
|
369
|
+
params[p.name],
|
|
370
|
+
p.cidl_type,
|
|
371
|
+
ast,
|
|
372
|
+
ctorReg,
|
|
373
|
+
);
|
|
374
|
+
if (res.isLeft()) {
|
|
375
|
+
return invalidRequest(RouterError.RequestBodyInvalidParameter);
|
|
376
|
+
}
|
|
377
|
+
params[p.name] = res.unwrap();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// A data source is required for instantiated model methods
|
|
381
|
+
const dataSource = requiredParams
|
|
382
|
+
.filter(
|
|
383
|
+
(p) =>
|
|
384
|
+
typeof p.cidl_type === "object" &&
|
|
385
|
+
"DataSource" in p.cidl_type &&
|
|
386
|
+
p.cidl_type.DataSource === route.namespace,
|
|
387
|
+
)
|
|
388
|
+
.map((p) => params[p.name])[0];
|
|
389
|
+
if (route.kind === "model" && !route.method.is_static && !dataSource) {
|
|
390
|
+
return invalidRequest(RouterError.InstantiatedMethodMissingDataSource);
|
|
391
|
+
}
|
|
392
|
+
return Either.right({ params, dataSource: dataSource ?? null });
|
|
273
393
|
}
|
|
274
394
|
/**
|
|
275
395
|
* Queries D1 for a particular model's ID, then transforms the SQL column output into
|
|
@@ -278,178 +398,102 @@ async function validateRequest(request, ast, model, method, id) {
|
|
|
278
398
|
* @returns 500 if the D1 database is not synced with Cloesce and yields an error
|
|
279
399
|
* @returns The instantiated model on success
|
|
280
400
|
*/
|
|
281
|
-
async function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
401
|
+
async function hydrateModelD1(constructorRegistry, d1, model, id, dataSource) {
|
|
402
|
+
// Error state: If some outside force tweaked the database schema, the query may fail.
|
|
403
|
+
// Otherwise, this indicates a bug in the compiler or runtime.
|
|
404
|
+
const malformedQuery = (e) =>
|
|
405
|
+
exit(
|
|
406
|
+
500,
|
|
407
|
+
RouterError.InvalidDatabaseQuery,
|
|
408
|
+
`Error in hydration query, is the database out of sync with the backend?: ${e instanceof Error ? e.message : String(e)}`,
|
|
409
|
+
);
|
|
410
|
+
// Query DB
|
|
411
|
+
let records;
|
|
412
|
+
try {
|
|
413
|
+
let includeTree =
|
|
414
|
+
dataSource === NO_DATA_SOURCE
|
|
415
|
+
? null
|
|
416
|
+
: constructorRegistry[model.name][dataSource];
|
|
417
|
+
records = await d1
|
|
418
|
+
.prepare(Orm.getQuery(constructorRegistry[model.name], includeTree))
|
|
419
|
+
.bind(id)
|
|
420
|
+
.run();
|
|
285
421
|
// Error state: If no record is found for the id, return a 404
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return malformedQuery(e);
|
|
306
|
-
}
|
|
307
|
-
// Hydrate
|
|
308
|
-
const models = mapSql(constructorRegistry[model.name], records.results, model.data_sources[dataSource]?.tree ?? {}).value;
|
|
309
|
-
return Either.right(models[0]);
|
|
422
|
+
if (!records?.results) {
|
|
423
|
+
return exit(404, RouterError.ModelNotFound, "Record not found");
|
|
424
|
+
}
|
|
425
|
+
if (records.error) {
|
|
426
|
+
return malformedQuery(records.error);
|
|
427
|
+
}
|
|
428
|
+
} catch (e) {
|
|
429
|
+
return malformedQuery(e);
|
|
430
|
+
}
|
|
431
|
+
// Hydrate
|
|
432
|
+
const models = mapSql(
|
|
433
|
+
constructorRegistry[model.name],
|
|
434
|
+
records.results,
|
|
435
|
+
model.data_sources[dataSource]?.tree ?? {},
|
|
436
|
+
);
|
|
437
|
+
if (models.isLeft()) {
|
|
438
|
+
return malformedQuery(models.value);
|
|
439
|
+
}
|
|
440
|
+
return Either.right(models.unwrap()[0]);
|
|
310
441
|
}
|
|
311
442
|
/**
|
|
312
443
|
* Calls a method on a model given a list of parameters.
|
|
313
444
|
* @returns 500 on an uncaught client error, 200 with a result body on success
|
|
314
445
|
*/
|
|
315
|
-
async function methodDispatch(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
*
|
|
355
|
-
* Returns true if the value matches the CIDL type, false otherwise.
|
|
356
|
-
*/
|
|
357
|
-
function validateCidlType(ast, value, cidlType, isPartial) {
|
|
358
|
-
if (value === undefined)
|
|
359
|
-
return isPartial;
|
|
360
|
-
// TODO: consequences of null checking like this? 'null' is passed in
|
|
361
|
-
// as a string for GET requests
|
|
362
|
-
const nullable = isNullableType(cidlType);
|
|
363
|
-
if (value == null || value === "null")
|
|
364
|
-
return nullable;
|
|
365
|
-
if (nullable) {
|
|
366
|
-
cidlType = cidlType.Nullable; // Unwrap the nullable type
|
|
367
|
-
}
|
|
368
|
-
// Handle primitive string types with switch
|
|
369
|
-
if (typeof cidlType === "string") {
|
|
370
|
-
switch (cidlType) {
|
|
371
|
-
case "Integer":
|
|
372
|
-
return Number.isInteger(Number(value));
|
|
373
|
-
case "Real":
|
|
374
|
-
return !Number.isNaN(Number(value));
|
|
375
|
-
case "Text":
|
|
376
|
-
return typeof value === "string";
|
|
377
|
-
case "Boolean":
|
|
378
|
-
return typeof value === "boolean";
|
|
379
|
-
case "DateIso":
|
|
380
|
-
const date = new Date(value);
|
|
381
|
-
return !isNaN(date.getTime());
|
|
382
|
-
default:
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
// Handle Data Sources
|
|
387
|
-
if ("DataSource" in cidlType) {
|
|
388
|
-
return typeof value === "string";
|
|
389
|
-
}
|
|
390
|
-
// Handle Models
|
|
391
|
-
let cidlTypeAccessor = "Partial" in cidlType
|
|
392
|
-
? cidlType.Partial
|
|
393
|
-
: "Object" in cidlType
|
|
394
|
-
? cidlType.Object
|
|
395
|
-
: undefined;
|
|
396
|
-
if (cidlTypeAccessor && ast.models[cidlTypeAccessor]) {
|
|
397
|
-
const model = ast.models[cidlTypeAccessor];
|
|
398
|
-
if (!model || typeof value !== "object")
|
|
399
|
-
return false;
|
|
400
|
-
const valueObj = value;
|
|
401
|
-
// Validate attributes
|
|
402
|
-
if (!model.attributes.every((attr) => validateCidlType(ast, valueObj[attr.value.name], attr.value.cidl_type, isPartial))) {
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
// Validate navigation properties
|
|
406
|
-
return model.navigation_properties.every((nav) => {
|
|
407
|
-
const navValue = valueObj[nav.var_name];
|
|
408
|
-
return (navValue == null ||
|
|
409
|
-
validateCidlType(ast, navValue, getNavigationPropertyCidlType(nav), isPartial));
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
// Handle Plain Old Objects
|
|
413
|
-
if (cidlTypeAccessor && ast.poos[cidlTypeAccessor]) {
|
|
414
|
-
const poo = ast.poos[cidlTypeAccessor];
|
|
415
|
-
if (!poo || typeof value !== "object")
|
|
416
|
-
return false;
|
|
417
|
-
const valueObj = value;
|
|
418
|
-
// Validate attributes
|
|
419
|
-
if (!poo.attributes.every((attr) => validateCidlType(ast, valueObj[attr.name], attr.cidl_type, isPartial))) {
|
|
420
|
-
return false;
|
|
421
|
-
}
|
|
422
|
-
return true;
|
|
423
|
-
}
|
|
424
|
-
if ("Array" in cidlType) {
|
|
425
|
-
const arr = cidlType.Array;
|
|
426
|
-
return (Array.isArray(value) &&
|
|
427
|
-
value.every((v) => validateCidlType(ast, v, arr, isPartial)));
|
|
428
|
-
}
|
|
429
|
-
if ("HttpResult" in cidlType) {
|
|
430
|
-
if (value === null)
|
|
431
|
-
return cidlType.HttpResult === null;
|
|
432
|
-
if (cidlType.HttpResult === null)
|
|
433
|
-
return false;
|
|
434
|
-
return validateCidlType(ast, value, cidlType.HttpResult, isPartial);
|
|
435
|
-
}
|
|
436
|
-
return false;
|
|
446
|
+
async function methodDispatch(obj, di, route, params) {
|
|
447
|
+
const paramArray = [];
|
|
448
|
+
for (const param of route.method.parameters) {
|
|
449
|
+
if (param.name in params) {
|
|
450
|
+
paramArray.push(params[param.name]);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
// Injected type
|
|
454
|
+
const injected = di.get(param.cidl_type["Inject"]);
|
|
455
|
+
if (!injected) {
|
|
456
|
+
// Error state: Injected parameters cannot be found at compile time, only at runtime.
|
|
457
|
+
// If a injected reference does not exist, throw a 500.
|
|
458
|
+
return exit(
|
|
459
|
+
500,
|
|
460
|
+
RouterError.MissingDependency,
|
|
461
|
+
`An injected parameter was missing from the instance registry: ${JSON.stringify(param.cidl_type)}`,
|
|
462
|
+
).unwrapLeft();
|
|
463
|
+
}
|
|
464
|
+
paramArray.push(injected);
|
|
465
|
+
}
|
|
466
|
+
const wrapResult = (res) => {
|
|
467
|
+
const rt = route.method.return_type;
|
|
468
|
+
const httpResult =
|
|
469
|
+
typeof rt === "object" && rt !== null && "HttpResult" in rt
|
|
470
|
+
? res
|
|
471
|
+
: HttpResult.ok(200, res);
|
|
472
|
+
return httpResult.setMediaType(route.method.return_media);
|
|
473
|
+
};
|
|
474
|
+
try {
|
|
475
|
+
const res = await obj[route.method.name](...paramArray);
|
|
476
|
+
return wrapResult(res);
|
|
477
|
+
} catch (e) {
|
|
478
|
+
// Error state: Client code threw an uncaught exception.
|
|
479
|
+
return exit(
|
|
480
|
+
500,
|
|
481
|
+
RouterError.UncaughtException,
|
|
482
|
+
`Uncaught exception in method dispatch: ${e instanceof Error ? e.message : String(e)}`,
|
|
483
|
+
).unwrapLeft();
|
|
484
|
+
}
|
|
437
485
|
}
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return new Response(JSON.stringify(r), {
|
|
443
|
-
status: r.status,
|
|
444
|
-
headers: { "Content-Type": "application/json" },
|
|
445
|
-
});
|
|
486
|
+
function exit(status, state, message, debugMessage = "") {
|
|
487
|
+
return Either.left(
|
|
488
|
+
HttpResult.fail(status, `${message} (ErrorCode: ${state}${debugMessage})`),
|
|
489
|
+
);
|
|
446
490
|
}
|
|
447
491
|
/**
|
|
448
492
|
* For testing purposes
|
|
449
493
|
*/
|
|
450
494
|
export const _cloesceInternal = {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
495
|
+
matchRoute,
|
|
496
|
+
validateRequest,
|
|
497
|
+
methodDispatch,
|
|
498
|
+
RuntimeContainer,
|
|
455
499
|
};
|