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