@supacloud/elysia 0.10.0 → 0.14.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/README.md +186 -2
- package/dist/command-errors.d.ts +2 -0
- package/dist/documentation.d.ts +29 -0
- package/dist/execution.d.ts +1 -0
- package/dist/fixtures/webhook/contracts.d.ts +42 -0
- package/dist/fixtures/webhook/environment.d.ts +11 -0
- package/dist/fixtures/webhook/update.command.d.ts +13 -0
- package/dist/fixtures/webhook/webhook.controller.d.ts +15 -0
- package/dist/fixtures/webhook/webhook.module.d.ts +2 -0
- package/dist/fixtures/webhook-generated/application.d.ts +100 -0
- package/dist/generate-webhook-example.d.ts +19 -0
- package/dist/index.d.ts +53 -1
- package/dist/index.js +993 -112
- package/dist/persistent-command.d.ts +20 -0
- package/dist/schema_contract.d.ts +160 -0
- package/dist/webhook-migration-example.d.ts +41 -0
- package/dist/worker.d.ts +150 -0
- package/package.json +13 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { Elysia } from "elysia";
|
|
2
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
3
|
+
import { CommandError as CommandError2, decodeCommandPreview } from "@supacloud/contracts";
|
|
4
|
+
import {
|
|
5
|
+
provideToken,
|
|
6
|
+
runInRequestContext
|
|
7
|
+
} from "@supacloud/app";
|
|
8
|
+
import { REQUEST_CONTEXT } from "@supacloud/app";
|
|
9
|
+
|
|
10
|
+
// src/command-errors.ts
|
|
11
|
+
function commandErrorStatus(code) {
|
|
12
|
+
switch (code) {
|
|
13
|
+
case "COMMAND_INPUT_INVALID":
|
|
14
|
+
return 400;
|
|
15
|
+
case "COMMAND_REJECTED":
|
|
16
|
+
return 403;
|
|
17
|
+
case "COMMAND_IDEMPOTENCY_CONFLICT":
|
|
18
|
+
return 409;
|
|
19
|
+
case "COMMAND_INPUT_EXPIRED":
|
|
20
|
+
return 410;
|
|
21
|
+
case "COMMAND_UNAVAILABLE":
|
|
22
|
+
case "COMMAND_RECEIPT_INVALID":
|
|
23
|
+
case "COMMAND_OUTCOME_UNKNOWN":
|
|
24
|
+
return 503;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
3
27
|
|
|
4
28
|
// src/execution.ts
|
|
5
29
|
async function observeExecution(observer, event, next) {
|
|
@@ -7,7 +31,11 @@ async function observeExecution(observer, event, next) {
|
|
|
7
31
|
if (!observer)
|
|
8
32
|
return;
|
|
9
33
|
try {
|
|
10
|
-
Promise.resolve(observer(Object.freeze({
|
|
34
|
+
Promise.resolve(observer(Object.freeze({
|
|
35
|
+
...event,
|
|
36
|
+
phase,
|
|
37
|
+
...durationMs === undefined ? {} : { durationMs }
|
|
38
|
+
}))).catch(() => {});
|
|
11
39
|
} catch {}
|
|
12
40
|
};
|
|
13
41
|
const started = performance.now();
|
|
@@ -27,7 +55,374 @@ function executionRequestId(context) {
|
|
|
27
55
|
const value = context.requestId;
|
|
28
56
|
return typeof value === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value) ? value : undefined;
|
|
29
57
|
}
|
|
58
|
+
function executionTrace(context) {
|
|
59
|
+
const requestId = executionRequestId(context);
|
|
60
|
+
return requestId === undefined ? {} : { requestId };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/schema_contract.ts
|
|
64
|
+
import {
|
|
65
|
+
getSchemaValidator,
|
|
66
|
+
StatusMap,
|
|
67
|
+
ElysiaCustomStatusResponse
|
|
68
|
+
} from "elysia";
|
|
69
|
+
function defineRouteContract(schemas) {
|
|
70
|
+
return Object.freeze({ ...schemas });
|
|
71
|
+
}
|
|
72
|
+
var TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
|
|
73
|
+
var RESPONSE_STATUS_MIN = 100;
|
|
74
|
+
var RESPONSE_STATUS_MAX = 599;
|
|
75
|
+
var RESPONSE_FAMILY_PATTERN = /^([1-5])(?:xx|XX)$/;
|
|
76
|
+
var RESPONSE_STATUS_PATTERN = /^[1-5]\d{2}$/;
|
|
77
|
+
function isSchema(value) {
|
|
78
|
+
return value !== null && typeof value === "object" && ((TYPEBOX_KIND in value) || ("~standard" in value));
|
|
79
|
+
}
|
|
80
|
+
function isJsonSchemaObject(value) {
|
|
81
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
82
|
+
return false;
|
|
83
|
+
return [
|
|
84
|
+
"$schema",
|
|
85
|
+
"$id",
|
|
86
|
+
"$ref",
|
|
87
|
+
"$defs",
|
|
88
|
+
"definitions",
|
|
89
|
+
"type",
|
|
90
|
+
"properties",
|
|
91
|
+
"patternProperties",
|
|
92
|
+
"required",
|
|
93
|
+
"items",
|
|
94
|
+
"prefixItems",
|
|
95
|
+
"contains",
|
|
96
|
+
"allOf",
|
|
97
|
+
"anyOf",
|
|
98
|
+
"oneOf",
|
|
99
|
+
"not",
|
|
100
|
+
"enum",
|
|
101
|
+
"const",
|
|
102
|
+
"additionalProperties",
|
|
103
|
+
"minLength",
|
|
104
|
+
"maxLength",
|
|
105
|
+
"minimum",
|
|
106
|
+
"maximum",
|
|
107
|
+
"format",
|
|
108
|
+
"pattern"
|
|
109
|
+
].some((key) => (key in value));
|
|
110
|
+
}
|
|
111
|
+
function invalidResponseSelector(selector) {
|
|
112
|
+
throw new TypeError(`Unsupported response selector "${selector}". Use an HTTP status (100-599), ` + "a status family such as 4XX/5XX, or default.");
|
|
113
|
+
}
|
|
114
|
+
function conflictingResponseSchemas() {
|
|
115
|
+
throw new TypeError("Route contracts cannot declare both response and responses. " + "Remove response and use responses: { 200: Schema }.");
|
|
116
|
+
}
|
|
117
|
+
function duplicateResponseSelector(selector, existing) {
|
|
118
|
+
throw new TypeError(`Duplicate response selectors "${existing}" and "${selector}" differ only by case. ` + `Keep one ${selector.toUpperCase()} selector.`);
|
|
119
|
+
}
|
|
120
|
+
function responseSchemaForStatus(responses, status) {
|
|
121
|
+
const exact = Object.hasOwn(responses, status) ? responses[status] : Object.hasOwn(responses, String(status)) ? responses[String(status)] : undefined;
|
|
122
|
+
if (exact !== undefined || Object.hasOwn(responses, status) || Object.hasOwn(responses, String(status))) {
|
|
123
|
+
return exact;
|
|
124
|
+
}
|
|
125
|
+
const family = `${Math.floor(status / 100)}XX`;
|
|
126
|
+
if (Object.hasOwn(responses, family))
|
|
127
|
+
return responses[family];
|
|
128
|
+
const lowerFamily = family.toLowerCase();
|
|
129
|
+
if (Object.hasOwn(responses, lowerFamily))
|
|
130
|
+
return responses[lowerFamily];
|
|
131
|
+
return Object.hasOwn(responses, "default") ? responses.default : undefined;
|
|
132
|
+
}
|
|
133
|
+
function responseStatusDeclared(responses, status) {
|
|
134
|
+
return responseSchemaForStatus(responses, status) !== undefined || Object.hasOwn(responses, status) || Object.hasOwn(responses, String(status));
|
|
135
|
+
}
|
|
136
|
+
function responseStatusOf(value, configuredStatus) {
|
|
137
|
+
if (value instanceof Response)
|
|
138
|
+
return value.status;
|
|
139
|
+
if (value instanceof ElysiaCustomStatusResponse) {
|
|
140
|
+
const code = value.code;
|
|
141
|
+
if (typeof code === "number" && Number.isInteger(code) && code >= RESPONSE_STATUS_MIN && code <= RESPONSE_STATUS_MAX) {
|
|
142
|
+
return code;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (typeof configuredStatus === "number")
|
|
146
|
+
return configuredStatus;
|
|
147
|
+
if (typeof configuredStatus === "string") {
|
|
148
|
+
const mapped = StatusMap[configuredStatus];
|
|
149
|
+
if (typeof mapped === "number")
|
|
150
|
+
return mapped;
|
|
151
|
+
}
|
|
152
|
+
return 200;
|
|
153
|
+
}
|
|
154
|
+
function assertResponseStatusDeclared(responses, status) {
|
|
155
|
+
if (responseStatusDeclared(responses, status))
|
|
156
|
+
return;
|
|
157
|
+
throw new TypeError("Response validation failed");
|
|
158
|
+
}
|
|
159
|
+
function toElysiaResponseMap(responses) {
|
|
160
|
+
const exact = new Map;
|
|
161
|
+
const families = new Map;
|
|
162
|
+
const familySelectors = new Map;
|
|
163
|
+
let defaultSchema;
|
|
164
|
+
let hasDefault = false;
|
|
165
|
+
let requiresExpansion = false;
|
|
166
|
+
for (const [selector, schema] of Object.entries(responses)) {
|
|
167
|
+
if (RESPONSE_STATUS_PATTERN.test(selector)) {
|
|
168
|
+
exact.set(Number(selector), schema);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (selector === "default") {
|
|
172
|
+
defaultSchema = schema;
|
|
173
|
+
hasDefault = true;
|
|
174
|
+
requiresExpansion = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const family = RESPONSE_FAMILY_PATTERN.exec(selector);
|
|
178
|
+
if (family) {
|
|
179
|
+
const canonicalSelector = selector.toLowerCase();
|
|
180
|
+
const existingSelector = familySelectors.get(canonicalSelector);
|
|
181
|
+
if (existingSelector !== undefined)
|
|
182
|
+
duplicateResponseSelector(selector, existingSelector);
|
|
183
|
+
familySelectors.set(canonicalSelector, selector);
|
|
184
|
+
families.set(Number(family[1]), schema);
|
|
185
|
+
requiresExpansion = true;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
invalidResponseSelector(selector);
|
|
189
|
+
}
|
|
190
|
+
if (!requiresExpansion)
|
|
191
|
+
return responses;
|
|
192
|
+
const expanded = {};
|
|
193
|
+
if (hasDefault) {
|
|
194
|
+
for (let status = RESPONSE_STATUS_MIN;status <= RESPONSE_STATUS_MAX; status++) {
|
|
195
|
+
expanded[status] = defaultSchema;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const [family, schema] of families) {
|
|
199
|
+
const first = family * 100;
|
|
200
|
+
for (let status = first;status < first + 100; status++)
|
|
201
|
+
expanded[status] = schema;
|
|
202
|
+
}
|
|
203
|
+
for (const [status, schema] of exact)
|
|
204
|
+
expanded[status] = schema;
|
|
205
|
+
return expanded;
|
|
206
|
+
}
|
|
207
|
+
function toElysiaResponseSchema(response) {
|
|
208
|
+
if (typeof response === "string" || isSchema(response) || isJsonSchemaObject(response))
|
|
209
|
+
return response;
|
|
210
|
+
if (response === null || typeof response !== "object" || Array.isArray(response))
|
|
211
|
+
return response;
|
|
212
|
+
return toElysiaResponseMap(response);
|
|
213
|
+
}
|
|
214
|
+
function toElysiaRouteSchema(contract) {
|
|
215
|
+
if (contract.response !== undefined && contract.responses !== undefined) {
|
|
216
|
+
conflictingResponseSchemas();
|
|
217
|
+
}
|
|
218
|
+
const schema = {};
|
|
219
|
+
if (contract.body !== undefined)
|
|
220
|
+
schema.body = contract.body;
|
|
221
|
+
if (contract.params !== undefined)
|
|
222
|
+
schema.params = contract.params;
|
|
223
|
+
if (contract.query !== undefined)
|
|
224
|
+
schema.query = contract.query;
|
|
225
|
+
if (contract.headers !== undefined)
|
|
226
|
+
schema.headers = contract.headers;
|
|
227
|
+
if (contract.cookie !== undefined)
|
|
228
|
+
schema.cookie = contract.cookie;
|
|
229
|
+
if (contract.responses !== undefined)
|
|
230
|
+
schema.response = toElysiaResponseMap(contract.responses);
|
|
231
|
+
else if (contract.response !== undefined)
|
|
232
|
+
schema.response = toElysiaResponseSchema(contract.response);
|
|
233
|
+
return schema;
|
|
234
|
+
}
|
|
235
|
+
function defineElysiaRoute(method, path, contract, handler) {
|
|
236
|
+
toElysiaRouteSchema(contract);
|
|
237
|
+
const stableContract = Object.isFrozen(contract) ? contract : Object.freeze(contract);
|
|
238
|
+
return Object.freeze({
|
|
239
|
+
method,
|
|
240
|
+
path,
|
|
241
|
+
contract: stableContract,
|
|
242
|
+
handler
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function registerElysiaRoute(app, route) {
|
|
246
|
+
const handler = async (context) => {
|
|
247
|
+
const value = await route.handler(context);
|
|
248
|
+
if (route.contract.responses !== undefined) {
|
|
249
|
+
const status = responseStatusOf(value, context.set.status);
|
|
250
|
+
assertResponseStatusDeclared(route.contract.responses, status);
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
};
|
|
254
|
+
app.route(route.method, route.path, handler, toElysiaRouteSchema(route.contract));
|
|
255
|
+
return app;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
class SchemaContractError extends Error {
|
|
259
|
+
code = "SCHEMA_CONTRACT_INVALID";
|
|
260
|
+
constructor() {
|
|
261
|
+
super("Schema contract validation failed");
|
|
262
|
+
this.name = "SchemaContractError";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function createSchemaDecoder(schema, options = {}) {
|
|
266
|
+
const validator = getSchemaValidator(schema, {
|
|
267
|
+
coerce: options.coerce ?? false,
|
|
268
|
+
normalize: options.normalize ?? true
|
|
269
|
+
});
|
|
270
|
+
return (value) => {
|
|
271
|
+
try {
|
|
272
|
+
return validator.parse(value);
|
|
273
|
+
} catch {
|
|
274
|
+
throw new SchemaContractError;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function defineJsonContract(schemas, request, options = {}) {
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
...schemas,
|
|
281
|
+
input: createSchemaDecoder(schemas.body, options),
|
|
282
|
+
result: createSchemaDecoder(schemas.response, options),
|
|
283
|
+
request
|
|
284
|
+
});
|
|
285
|
+
}
|
|
30
286
|
|
|
287
|
+
// src/documentation.ts
|
|
288
|
+
import { Elysia } from "elysia";
|
|
289
|
+
function normalizePath(value, fallback) {
|
|
290
|
+
const path = value ?? fallback;
|
|
291
|
+
if (!path.trim() || !path.startsWith("/") || path.includes("?") || path.includes("#")) {
|
|
292
|
+
throw new Error(`Documentation path must be an absolute path without query or fragment: ${path}`);
|
|
293
|
+
}
|
|
294
|
+
return path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
295
|
+
}
|
|
296
|
+
function resolveSource(source) {
|
|
297
|
+
if (typeof source === "function") {
|
|
298
|
+
return Promise.resolve(source());
|
|
299
|
+
}
|
|
300
|
+
return Promise.resolve(source);
|
|
301
|
+
}
|
|
302
|
+
function jsonResponse(document) {
|
|
303
|
+
if (document === null || typeof document !== "object" || Array.isArray(document)) {
|
|
304
|
+
throw new Error("OpenAPI documentation must be a JSON object");
|
|
305
|
+
}
|
|
306
|
+
const serialized = JSON.stringify(document);
|
|
307
|
+
if (serialized === undefined)
|
|
308
|
+
throw new Error("OpenAPI documentation must be JSON-serializable");
|
|
309
|
+
return new Response(serialized, {
|
|
310
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function textResponse(schema) {
|
|
314
|
+
return new Response(schema, {
|
|
315
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function escapeHtml(value) {
|
|
319
|
+
return value.replace(/[&<>"']/g, (character) => ({
|
|
320
|
+
"&": "&",
|
|
321
|
+
"<": "<",
|
|
322
|
+
">": ">",
|
|
323
|
+
'"': """,
|
|
324
|
+
"'": "'"
|
|
325
|
+
})[character] ?? character);
|
|
326
|
+
}
|
|
327
|
+
function scriptString(value) {
|
|
328
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
329
|
+
}
|
|
330
|
+
function renderViewerPage(title, resourcePath, mode) {
|
|
331
|
+
const parse = mode === "json" ? "const value = await response.json(); output.textContent = JSON.stringify(value, null, 2);" : "output.textContent = await response.text();";
|
|
332
|
+
const safeTitle = escapeHtml(title);
|
|
333
|
+
const safeResourcePath = escapeHtml(resourcePath);
|
|
334
|
+
return `<!doctype html>
|
|
335
|
+
<html lang="en">
|
|
336
|
+
<head>
|
|
337
|
+
<meta charset="utf-8">
|
|
338
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
339
|
+
<title>${safeTitle}</title>
|
|
340
|
+
<style>
|
|
341
|
+
:root { color-scheme: light dark; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
342
|
+
body { margin: 0; padding: 24px; background: Canvas; color: CanvasText; }
|
|
343
|
+
header { display: flex; gap: 16px; align-items: baseline; flex-wrap: wrap; }
|
|
344
|
+
h1 { font: 600 20px system-ui, sans-serif; margin: 0; }
|
|
345
|
+
a { color: LinkText; }
|
|
346
|
+
pre { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.5; }
|
|
347
|
+
</style>
|
|
348
|
+
</head>
|
|
349
|
+
<body>
|
|
350
|
+
<header><h1>${safeTitle}</h1><a href="${safeResourcePath}">${safeResourcePath}</a></header>
|
|
351
|
+
<pre id="output">Loading...</pre>
|
|
352
|
+
<script>
|
|
353
|
+
const output = document.getElementById("output");
|
|
354
|
+
fetch(${scriptString(resourcePath)}, { headers: { "accept": "${mode === "json" ? "application/json" : "text/plain"}" } })
|
|
355
|
+
.then(async (response) => {
|
|
356
|
+
if (!response.ok) throw new Error("HTTP " + response.status);
|
|
357
|
+
${parse}
|
|
358
|
+
})
|
|
359
|
+
.catch((error) => { output.textContent = "Unable to load documentation: " + error.message; });
|
|
360
|
+
</script>
|
|
361
|
+
</body>
|
|
362
|
+
</html>`;
|
|
363
|
+
}
|
|
364
|
+
function createDocumentationPlugin(options = {}) {
|
|
365
|
+
const app = new Elysia({ name: "supacloud:documentation" });
|
|
366
|
+
const paths = new Set;
|
|
367
|
+
const reserve = (path) => {
|
|
368
|
+
if (paths.has(path))
|
|
369
|
+
throw new Error(`Duplicate documentation path: ${path}`);
|
|
370
|
+
paths.add(path);
|
|
371
|
+
};
|
|
372
|
+
if (options.openApi) {
|
|
373
|
+
const specPath = normalizePath(options.openApi.specPath, "/openapi.json");
|
|
374
|
+
const uiPath = normalizePath(options.openApi.uiPath, "/docs");
|
|
375
|
+
reserve(specPath);
|
|
376
|
+
reserve(uiPath);
|
|
377
|
+
const title = options.openApi.title ?? "OpenAPI Documentation";
|
|
378
|
+
app.get(specPath, async () => jsonResponse(await resolveSource(options.openApi.document)), {
|
|
379
|
+
detail: { hide: true }
|
|
380
|
+
});
|
|
381
|
+
app.get(uiPath, () => new Response(renderViewerPage(title, specPath, "json"), {
|
|
382
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
383
|
+
}), { detail: { hide: true } });
|
|
384
|
+
}
|
|
385
|
+
if (options.graphql) {
|
|
386
|
+
const schemaPath = normalizePath(options.graphql.schemaPath, "/graphql/schema.graphql");
|
|
387
|
+
const uiPath = normalizePath(options.graphql.uiPath, "/graphql/docs");
|
|
388
|
+
reserve(schemaPath);
|
|
389
|
+
reserve(uiPath);
|
|
390
|
+
const title = options.graphql.title ?? "GraphQL Schema";
|
|
391
|
+
app.get(schemaPath, async () => textResponse(await resolveSource(options.graphql.schema)), {
|
|
392
|
+
detail: { hide: true }
|
|
393
|
+
});
|
|
394
|
+
app.get(uiPath, () => new Response(renderViewerPage(title, schemaPath, "text"), {
|
|
395
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
396
|
+
}), { detail: { hide: true } });
|
|
397
|
+
}
|
|
398
|
+
return app;
|
|
399
|
+
}
|
|
400
|
+
// src/persistent-command.ts
|
|
401
|
+
import {
|
|
402
|
+
CommandError,
|
|
403
|
+
decodeCommandIdentity
|
|
404
|
+
} from "@supacloud/contracts";
|
|
405
|
+
function createPersistentCommandAdapter(command, options) {
|
|
406
|
+
return {
|
|
407
|
+
capabilities: {
|
|
408
|
+
audit: true,
|
|
409
|
+
idempotency: true,
|
|
410
|
+
transaction: command.kind === "transactional",
|
|
411
|
+
boundary: command.kind === "transactional" ? "database" : "external"
|
|
412
|
+
},
|
|
413
|
+
async execute(invocation) {
|
|
414
|
+
try {
|
|
415
|
+
const identity = decodeCommandIdentity(await options.identity(invocation));
|
|
416
|
+
return await command.execute(identity, requireIdempotencyKey(invocation), options.input(invocation));
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (!(error instanceof CommandError))
|
|
419
|
+
throw error;
|
|
420
|
+
const status = commandErrorStatus(error.code);
|
|
421
|
+
throw new ApplicationError(error.code, { code: error.code, status });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
31
426
|
// src/identity.ts
|
|
32
427
|
import { createRemoteJWKSet, jwtVerify, errors } from "jose";
|
|
33
428
|
function httpsUrl(value) {
|
|
@@ -449,6 +844,401 @@ function cloneTables(tables) {
|
|
|
449
844
|
function cloneValue(value) {
|
|
450
845
|
return structuredClone(value);
|
|
451
846
|
}
|
|
847
|
+
// src/worker.ts
|
|
848
|
+
function workerFailureMessage(error) {
|
|
849
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Worker job failed";
|
|
850
|
+
const normalized = message.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
|
|
851
|
+
return (normalized || "Worker job failed").slice(0, 4096);
|
|
852
|
+
}
|
|
853
|
+
function createQueueWorkerTransport(options) {
|
|
854
|
+
if (!options.queue || typeof options.queue.receive !== "function" || typeof options.queue.ack !== "function" || typeof options.queue.fail !== "function" || typeof options.decodeClaim !== "function" || typeof options.messageId !== "function") {
|
|
855
|
+
throw new TypeError("Queue worker transport requires queue, decodeClaim and messageId");
|
|
856
|
+
}
|
|
857
|
+
const receiveOptions = options.receive === undefined ? undefined : { ...options.receive };
|
|
858
|
+
return {
|
|
859
|
+
claim: async (signal) => {
|
|
860
|
+
signal.throwIfAborted();
|
|
861
|
+
const message = await options.queue.receive(receiveOptions);
|
|
862
|
+
signal.throwIfAborted();
|
|
863
|
+
return message === null ? null : options.decodeClaim(message);
|
|
864
|
+
},
|
|
865
|
+
ack: (claim) => options.queue.ack(options.messageId(claim)),
|
|
866
|
+
fail: (claim, error) => options.queue.fail(options.messageId(claim), {
|
|
867
|
+
error: workerFailureMessage(error)
|
|
868
|
+
})
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
class WorkerRegistrationError extends Error {
|
|
873
|
+
code;
|
|
874
|
+
constructor(code, message) {
|
|
875
|
+
super(message);
|
|
876
|
+
this.name = "WorkerRegistrationError";
|
|
877
|
+
this.code = code;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
class WorkerReceiptUnconfirmedError extends Error {
|
|
882
|
+
operation;
|
|
883
|
+
claimId;
|
|
884
|
+
code = "WORKER_RECEIPT_UNCONFIRMED";
|
|
885
|
+
mutationMayHaveApplied = true;
|
|
886
|
+
constructor(operation, claimId) {
|
|
887
|
+
super(`Worker ${operation} receipt could not be confirmed for claim "${claimId}"`);
|
|
888
|
+
this.operation = operation;
|
|
889
|
+
this.claimId = claimId;
|
|
890
|
+
this.name = "WorkerReceiptUnconfirmedError";
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function safeWorkerId(value) {
|
|
894
|
+
const workerId = value ?? `worker-${crypto.randomUUID()}`;
|
|
895
|
+
if (workerId.length === 0 || workerId.length > 256 || /[\u0000-\u001f\u007f]/.test(workerId)) {
|
|
896
|
+
throw new TypeError("Worker workerId must be a safe non-empty string");
|
|
897
|
+
}
|
|
898
|
+
return workerId;
|
|
899
|
+
}
|
|
900
|
+
function safeDescriptorText(value, maxLength) {
|
|
901
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength && !/[\u0000-\u001f\u007f]/.test(value);
|
|
902
|
+
}
|
|
903
|
+
function validJobScope(value) {
|
|
904
|
+
return value === "application" || value === "request" || value === "job";
|
|
905
|
+
}
|
|
906
|
+
function validJobMode(value) {
|
|
907
|
+
return value === "task" || value === "workflow";
|
|
908
|
+
}
|
|
909
|
+
function validJobIdempotency(value) {
|
|
910
|
+
return value === "required" || value === "none";
|
|
911
|
+
}
|
|
912
|
+
function validPositiveInteger(value) {
|
|
913
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
914
|
+
}
|
|
915
|
+
function safeClaim(claim) {
|
|
916
|
+
if (!claim || typeof claim !== "object") {
|
|
917
|
+
throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Worker claim is invalid");
|
|
918
|
+
}
|
|
919
|
+
if (typeof claim.id !== "string" || claim.id.length === 0 || claim.id.length > 512 || /[\u0000-\u001f\u007f]/.test(claim.id) || typeof claim.jobName !== "string" || claim.jobName.length === 0 || claim.jobName.length > 256 || /[\u0000-\u001f\u007f]/.test(claim.jobName)) {
|
|
920
|
+
throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Worker claim identity is invalid");
|
|
921
|
+
}
|
|
922
|
+
const attempt = claim.attempt ?? 1;
|
|
923
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1) {
|
|
924
|
+
throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Worker claim attempt is invalid");
|
|
925
|
+
}
|
|
926
|
+
return { ...claim, attempt };
|
|
927
|
+
}
|
|
928
|
+
function delay(milliseconds, signal) {
|
|
929
|
+
if (milliseconds <= 0)
|
|
930
|
+
return Promise.resolve();
|
|
931
|
+
return new Promise((resolve, reject) => {
|
|
932
|
+
let settled = false;
|
|
933
|
+
const finish = (callback) => {
|
|
934
|
+
if (settled)
|
|
935
|
+
return;
|
|
936
|
+
settled = true;
|
|
937
|
+
clearTimeout(timer);
|
|
938
|
+
signal.removeEventListener("abort", onAbort);
|
|
939
|
+
callback();
|
|
940
|
+
};
|
|
941
|
+
const onAbort = () => finish(() => reject(signal.reason ?? new DOMException("Aborted", "AbortError")));
|
|
942
|
+
const timer = setTimeout(() => finish(resolve), milliseconds);
|
|
943
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
944
|
+
if (signal.aborted)
|
|
945
|
+
onAbort();
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
async function destroyServiceValues(services) {
|
|
949
|
+
const errors = [];
|
|
950
|
+
const seen = new Set;
|
|
951
|
+
for (const value of Object.values(services).reverse()) {
|
|
952
|
+
if (seen.has(value) || !value || typeof value !== "object")
|
|
953
|
+
continue;
|
|
954
|
+
seen.add(value);
|
|
955
|
+
const candidate = value;
|
|
956
|
+
const hook = typeof candidate.onDestroy === "function" ? candidate.onDestroy : typeof candidate.ngOnDestroy === "function" ? candidate.ngOnDestroy : undefined;
|
|
957
|
+
if (!hook)
|
|
958
|
+
continue;
|
|
959
|
+
try {
|
|
960
|
+
await Reflect.apply(hook, value, []);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
errors.push(error);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (errors.length > 0)
|
|
966
|
+
throw new AggregateError(errors, "Worker service destruction failed");
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
class SupaCloudWorker {
|
|
970
|
+
registry = new Map;
|
|
971
|
+
modules = [];
|
|
972
|
+
moduleServices = new Map;
|
|
973
|
+
deps;
|
|
974
|
+
transport;
|
|
975
|
+
mapClaim;
|
|
976
|
+
requestContextFactory;
|
|
977
|
+
workerId;
|
|
978
|
+
concurrency;
|
|
979
|
+
pollIntervalMs;
|
|
980
|
+
executor;
|
|
981
|
+
observer;
|
|
982
|
+
onError;
|
|
983
|
+
destroyServices;
|
|
984
|
+
stateValue = "idle";
|
|
985
|
+
claimController;
|
|
986
|
+
loopPromise;
|
|
987
|
+
startPromise;
|
|
988
|
+
stopPromise;
|
|
989
|
+
stopRequested = false;
|
|
990
|
+
inFlight = new Set;
|
|
991
|
+
services = {};
|
|
992
|
+
imported = {};
|
|
993
|
+
constructor(options) {
|
|
994
|
+
if (!options.transport || typeof options.transport.claim !== "function" || typeof options.transport.fail !== "function" || typeof options.transport.ack !== "function" && typeof options.transport.acknowledge !== "function") {
|
|
995
|
+
throw new WorkerRegistrationError("WORKER_TRANSPORT_INVALID", "Worker transport must provide claim, ack and fail");
|
|
996
|
+
}
|
|
997
|
+
this.transport = options.transport;
|
|
998
|
+
this.deps = options.deps ?? {};
|
|
999
|
+
this.workerId = safeWorkerId(options.workerId);
|
|
1000
|
+
this.mapClaim = options.mapClaim ?? ((claim) => claim);
|
|
1001
|
+
this.requestContextFactory = options.requestContext ?? ((claim, signal) => ({
|
|
1002
|
+
workerId: this.workerId,
|
|
1003
|
+
jobId: claim.id,
|
|
1004
|
+
jobName: claim.jobName,
|
|
1005
|
+
attempt: claim.attempt,
|
|
1006
|
+
signal
|
|
1007
|
+
}));
|
|
1008
|
+
this.concurrency = this.capturePositiveInteger(options.concurrency, 1, 1000, "concurrency");
|
|
1009
|
+
this.pollIntervalMs = this.captureNonNegativeInteger(options.pollIntervalMs, 1000, 300000, "pollIntervalMs");
|
|
1010
|
+
this.executor = options.executor;
|
|
1011
|
+
this.observer = options.onExecution;
|
|
1012
|
+
this.onError = options.onError;
|
|
1013
|
+
this.destroyServices = options.destroyServices;
|
|
1014
|
+
for (const module of options.modules ?? [])
|
|
1015
|
+
this.registerModule(module);
|
|
1016
|
+
}
|
|
1017
|
+
get state() {
|
|
1018
|
+
return this.stateValue;
|
|
1019
|
+
}
|
|
1020
|
+
get id() {
|
|
1021
|
+
return this.workerId;
|
|
1022
|
+
}
|
|
1023
|
+
get jobNames() {
|
|
1024
|
+
return Object.freeze([...this.registry.keys()]);
|
|
1025
|
+
}
|
|
1026
|
+
get jobs() {
|
|
1027
|
+
return Object.freeze([...this.registry.values()].map((entry) => entry.job));
|
|
1028
|
+
}
|
|
1029
|
+
registerModule(module) {
|
|
1030
|
+
if (this.stateValue !== "idle" && this.stateValue !== "stopped") {
|
|
1031
|
+
throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Jobs cannot be registered while the worker is active");
|
|
1032
|
+
}
|
|
1033
|
+
if (!module || typeof module !== "object" || typeof module.name !== "string" || module.name.length === 0 || module.name.length > 256 || /[\u0000-\u001f\u007f]/.test(module.name) || typeof module.createServices !== "function" || module.jobs !== undefined && !Array.isArray(module.jobs)) {
|
|
1034
|
+
throw new WorkerRegistrationError("WORKER_INVALID_JOB", "A registered module is invalid");
|
|
1035
|
+
}
|
|
1036
|
+
if (this.modules.some((candidate) => candidate.name === module.name)) {
|
|
1037
|
+
throw new WorkerRegistrationError("WORKER_DUPLICATE_MODULE", `Module "${module.name}" is registered more than once`);
|
|
1038
|
+
}
|
|
1039
|
+
const jobs = module.jobs ?? [];
|
|
1040
|
+
const names = new Set;
|
|
1041
|
+
for (const job of jobs) {
|
|
1042
|
+
if (!job || typeof job !== "object" || !safeDescriptorText(job.className, 256) || !safeDescriptorText(job.name, 256) || !safeDescriptorText(job.serviceKey, 256) || !validJobScope(job.scope) || job.mode !== undefined && !validJobMode(job.mode) || job.idempotency !== undefined && !validJobIdempotency(job.idempotency) || job.timeoutSec !== undefined && !validPositiveInteger(job.timeoutSec) || job.maxAttempts !== undefined && !validPositiveInteger(job.maxAttempts)) {
|
|
1043
|
+
throw new WorkerRegistrationError("WORKER_INVALID_JOB", "A registered Job has invalid identity or execution metadata");
|
|
1044
|
+
}
|
|
1045
|
+
if (this.registry.has(job.name) || names.has(job.name)) {
|
|
1046
|
+
throw new WorkerRegistrationError("WORKER_DUPLICATE_JOB", `Job "${job.name}" is registered more than once`);
|
|
1047
|
+
}
|
|
1048
|
+
names.add(job.name);
|
|
1049
|
+
}
|
|
1050
|
+
this.modules.push(module);
|
|
1051
|
+
for (const job of jobs)
|
|
1052
|
+
this.registry.set(job.name, { module, job });
|
|
1053
|
+
}
|
|
1054
|
+
register(module) {
|
|
1055
|
+
this.registerModule(module);
|
|
1056
|
+
}
|
|
1057
|
+
async start() {
|
|
1058
|
+
if (this.stateValue === "running")
|
|
1059
|
+
return;
|
|
1060
|
+
if (this.stateValue === "starting" && this.startPromise)
|
|
1061
|
+
return this.startPromise;
|
|
1062
|
+
if (this.stateValue === "stopping" && this.stopPromise)
|
|
1063
|
+
await this.stopPromise;
|
|
1064
|
+
this.stopRequested = false;
|
|
1065
|
+
this.stateValue = "starting";
|
|
1066
|
+
const startPromise = Promise.resolve().then(() => this.initialize()).then(async () => {
|
|
1067
|
+
if (this.stopRequested || this.stateValue !== "starting") {
|
|
1068
|
+
await this.releaseServices();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
this.claimController = new AbortController;
|
|
1072
|
+
this.stateValue = "running";
|
|
1073
|
+
this.loopPromise = this.pollLoop(this.claimController.signal);
|
|
1074
|
+
}).catch(async (error) => {
|
|
1075
|
+
this.stateValue = "stopped";
|
|
1076
|
+
await this.releaseServices();
|
|
1077
|
+
throw error;
|
|
1078
|
+
});
|
|
1079
|
+
this.startPromise = startPromise;
|
|
1080
|
+
return startPromise;
|
|
1081
|
+
}
|
|
1082
|
+
async stop() {
|
|
1083
|
+
if (this.stateValue === "idle" || this.stateValue === "stopped")
|
|
1084
|
+
return;
|
|
1085
|
+
if (this.stopPromise)
|
|
1086
|
+
return this.stopPromise;
|
|
1087
|
+
this.stopRequested = true;
|
|
1088
|
+
this.stateValue = "stopping";
|
|
1089
|
+
this.claimController?.abort(new DOMException("Worker stopped", "AbortError"));
|
|
1090
|
+
const startPromise = this.startPromise;
|
|
1091
|
+
this.stopPromise = (async () => {
|
|
1092
|
+
await startPromise?.catch(() => {
|
|
1093
|
+
return;
|
|
1094
|
+
});
|
|
1095
|
+
await this.loopPromise?.catch((error) => this.reportError(error));
|
|
1096
|
+
await Promise.allSettled([...this.inFlight]);
|
|
1097
|
+
await this.releaseServices();
|
|
1098
|
+
this.stateValue = "stopped";
|
|
1099
|
+
this.claimController = undefined;
|
|
1100
|
+
this.loopPromise = undefined;
|
|
1101
|
+
this.startPromise = undefined;
|
|
1102
|
+
this.stopPromise = undefined;
|
|
1103
|
+
})();
|
|
1104
|
+
return this.stopPromise;
|
|
1105
|
+
}
|
|
1106
|
+
async runOnce() {
|
|
1107
|
+
if (this.stateValue !== "running") {
|
|
1108
|
+
throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker must be started before runOnce");
|
|
1109
|
+
}
|
|
1110
|
+
const controller = this.claimController;
|
|
1111
|
+
if (!controller)
|
|
1112
|
+
throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker claim lifecycle is unavailable");
|
|
1113
|
+
controller.signal.throwIfAborted();
|
|
1114
|
+
const claim = await this.transport.claim(controller.signal);
|
|
1115
|
+
if (claim === null)
|
|
1116
|
+
return null;
|
|
1117
|
+
return this.processClaim(claim);
|
|
1118
|
+
}
|
|
1119
|
+
async processClaim(rawClaim) {
|
|
1120
|
+
const claim = safeClaim(this.mapClaim(rawClaim));
|
|
1121
|
+
const entry = this.registry.get(claim.jobName);
|
|
1122
|
+
const controller = new AbortController;
|
|
1123
|
+
const context = Object.freeze({
|
|
1124
|
+
jobId: claim.id,
|
|
1125
|
+
jobName: claim.jobName,
|
|
1126
|
+
attempt: claim.attempt ?? 1,
|
|
1127
|
+
signal: controller.signal
|
|
1128
|
+
});
|
|
1129
|
+
if (!entry) {
|
|
1130
|
+
const receipt = await this.failClaim(rawClaim, claim, new WorkerRegistrationError("WORKER_INVALID_JOB", `Job "${claim.jobName}" is not registered`), context);
|
|
1131
|
+
return { claimId: claim.id, jobName: claim.jobName, status: "failed", receipt };
|
|
1132
|
+
}
|
|
1133
|
+
let output;
|
|
1134
|
+
try {
|
|
1135
|
+
if (this.stateValue !== "running" && this.stateValue !== "stopping") {
|
|
1136
|
+
throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker is not active");
|
|
1137
|
+
}
|
|
1138
|
+
const requestContext = claim.requestContext ?? await this.requestContextFactory(claim, controller.signal);
|
|
1139
|
+
const moduleServices = this.moduleServices.get(entry.module.name);
|
|
1140
|
+
if (!moduleServices)
|
|
1141
|
+
throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker services are unavailable");
|
|
1142
|
+
output = await executeJob(entry.module, moduleServices, entry.job, claim.input, requestContext, this.imported, this.executor, this.observer);
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
const receipt = await this.failClaim(rawClaim, claim, error, context);
|
|
1145
|
+
return { claimId: claim.id, jobName: claim.jobName, status: "failed", receipt };
|
|
1146
|
+
}
|
|
1147
|
+
const acknowledge = this.transport.ack ?? this.transport.acknowledge;
|
|
1148
|
+
if (!acknowledge)
|
|
1149
|
+
throw new WorkerRegistrationError("WORKER_TRANSPORT_INVALID", "Worker acknowledgement is unavailable");
|
|
1150
|
+
try {
|
|
1151
|
+
const receipt = await acknowledge(rawClaim, output, context);
|
|
1152
|
+
return { claimId: claim.id, jobName: claim.jobName, status: "acknowledged", receipt };
|
|
1153
|
+
} catch {
|
|
1154
|
+
throw new WorkerReceiptUnconfirmedError("ack", claim.id);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async failClaim(rawClaim, claim, error, context) {
|
|
1158
|
+
try {
|
|
1159
|
+
return await this.transport.fail(rawClaim, error, context);
|
|
1160
|
+
} catch {
|
|
1161
|
+
throw new WorkerReceiptUnconfirmedError("fail", claim.id);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
async initialize() {
|
|
1165
|
+
this.services = {};
|
|
1166
|
+
this.imported = {};
|
|
1167
|
+
this.moduleServices.clear();
|
|
1168
|
+
for (const module of this.modules) {
|
|
1169
|
+
const services = module.createServices(this.deps, this.imported);
|
|
1170
|
+
this.moduleServices.set(module.name, services);
|
|
1171
|
+
this.services = { ...this.services, ...services };
|
|
1172
|
+
this.imported[module.name] = services;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
async releaseServices() {
|
|
1176
|
+
const services = this.services;
|
|
1177
|
+
const imported = this.imported;
|
|
1178
|
+
this.services = {};
|
|
1179
|
+
this.imported = {};
|
|
1180
|
+
this.moduleServices.clear();
|
|
1181
|
+
if (Object.keys(services).length === 0)
|
|
1182
|
+
return;
|
|
1183
|
+
try {
|
|
1184
|
+
if (this.destroyServices)
|
|
1185
|
+
await this.destroyServices(services, imported);
|
|
1186
|
+
else
|
|
1187
|
+
await destroyServiceValues(services);
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
await this.reportError(error);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
async pollLoop(signal) {
|
|
1193
|
+
while (!signal.aborted && this.stateValue === "running") {
|
|
1194
|
+
if (this.inFlight.size >= this.concurrency) {
|
|
1195
|
+
await Promise.race(this.inFlight).catch((error) => this.reportError(error));
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
let claim;
|
|
1199
|
+
try {
|
|
1200
|
+
claim = await this.transport.claim(signal);
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
if (signal.aborted)
|
|
1203
|
+
break;
|
|
1204
|
+
await this.reportError(error);
|
|
1205
|
+
await delay(this.pollIntervalMs, signal).catch(() => {
|
|
1206
|
+
return;
|
|
1207
|
+
});
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (claim === null) {
|
|
1211
|
+
await delay(this.pollIntervalMs, signal).catch(() => {
|
|
1212
|
+
return;
|
|
1213
|
+
});
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
const execution = this.processClaim(claim).catch((error) => this.reportError(error));
|
|
1217
|
+
this.inFlight.add(execution);
|
|
1218
|
+
execution.finally(() => this.inFlight.delete(execution));
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async reportError(error) {
|
|
1222
|
+
try {
|
|
1223
|
+
await this.onError?.(error);
|
|
1224
|
+
} catch {}
|
|
1225
|
+
}
|
|
1226
|
+
capturePositiveInteger(value, fallback, max, name) {
|
|
1227
|
+
const result = value ?? fallback;
|
|
1228
|
+
if (!Number.isSafeInteger(result) || result < 1 || result > max)
|
|
1229
|
+
throw new TypeError(`Worker ${name} is invalid`);
|
|
1230
|
+
return result;
|
|
1231
|
+
}
|
|
1232
|
+
captureNonNegativeInteger(value, fallback, max, name) {
|
|
1233
|
+
const result = value ?? fallback;
|
|
1234
|
+
if (!Number.isSafeInteger(result) || result < 0 || result > max)
|
|
1235
|
+
throw new TypeError(`Worker ${name} is invalid`);
|
|
1236
|
+
return result;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function createWorker(options) {
|
|
1240
|
+
return new SupaCloudWorker(options);
|
|
1241
|
+
}
|
|
452
1242
|
|
|
453
1243
|
// src/index.ts
|
|
454
1244
|
var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
|
|
@@ -463,9 +1253,10 @@ function composeAspects(...aspects) {
|
|
|
463
1253
|
return Promise.reject(new Error("next() called multiple times"));
|
|
464
1254
|
}
|
|
465
1255
|
index = current;
|
|
466
|
-
|
|
1256
|
+
const aspect = active[current];
|
|
1257
|
+
if (!aspect)
|
|
467
1258
|
return Promise.resolve(next());
|
|
468
|
-
return Promise.resolve(
|
|
1259
|
+
return Promise.resolve(aspect(context, () => dispatch(current + 1)));
|
|
469
1260
|
};
|
|
470
1261
|
return dispatch(0);
|
|
471
1262
|
};
|
|
@@ -475,7 +1266,7 @@ function observedAspects(aspects, boundary, observer) {
|
|
|
475
1266
|
kind: context.kind,
|
|
476
1267
|
operation: context.name,
|
|
477
1268
|
stage: `${boundary}.aspect[${index}]:${aspect.name || "anonymous"}`,
|
|
478
|
-
|
|
1269
|
+
...executionTrace(context.requestContext)
|
|
479
1270
|
}, () => aspect(context, next))));
|
|
480
1271
|
}
|
|
481
1272
|
function composeCommandExecutors(...executors) {
|
|
@@ -537,6 +1328,19 @@ function validatedJsonResponse(validate, value, init) {
|
|
|
537
1328
|
headers.set("content-type", "application/json");
|
|
538
1329
|
return new Response(body, { ...init, headers });
|
|
539
1330
|
}
|
|
1331
|
+
function decodeJobContract(schema, value, boundary) {
|
|
1332
|
+
if (schema === undefined)
|
|
1333
|
+
return value;
|
|
1334
|
+
try {
|
|
1335
|
+
return createSchemaDecoder(schema)(value);
|
|
1336
|
+
} catch {
|
|
1337
|
+
const input = boundary === "input";
|
|
1338
|
+
throw new ApplicationError(input ? "Job input contract validation failed" : "Job output contract validation failed", {
|
|
1339
|
+
status: input ? 422 : 500,
|
|
1340
|
+
code: input ? "JOB_INPUT_VALIDATION_ERROR" : "JOB_OUTPUT_VALIDATION_ERROR"
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
540
1344
|
function safeHeaderValue(value, maxLength) {
|
|
541
1345
|
if (!value || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
542
1346
|
return;
|
|
@@ -555,7 +1359,7 @@ function isAuthenticatedTrustedIdentity(value) {
|
|
|
555
1359
|
function bearerToken(request) {
|
|
556
1360
|
const authorization = request.headers.get("authorization");
|
|
557
1361
|
const match = authorization?.match(/^Bearer\s+(.+)$/i);
|
|
558
|
-
return match ? safeHeaderValue(match[1], 16384) : undefined;
|
|
1362
|
+
return match?.[1] ? safeHeaderValue(match[1], 16384) : undefined;
|
|
559
1363
|
}
|
|
560
1364
|
var createSupaCloudRequestContext = (request) => {
|
|
561
1365
|
const subject = safeHeaderValue(request.headers.get(VERIFIED_JWT_SUBJECT_HEADER), 1024);
|
|
@@ -612,7 +1416,7 @@ function createCommandExecutor(governance, observer) {
|
|
|
612
1416
|
const event = {
|
|
613
1417
|
kind: "command",
|
|
614
1418
|
operation: command.name,
|
|
615
|
-
|
|
1419
|
+
...executionTrace(invocation.requestContext)
|
|
616
1420
|
};
|
|
617
1421
|
await observeExecution(observer, { ...event, stage: "authorize" }, () => governance.authorize(invocation));
|
|
618
1422
|
return observeExecution(observer, { ...event, stage: `rpc:${command.rpc}` }, () => adapter.execute(invocation, once(next)));
|
|
@@ -630,12 +1434,12 @@ function createCommandExecutor(governance, observer) {
|
|
|
630
1434
|
throw missingGovernanceAdapter(command, "idempotency");
|
|
631
1435
|
}
|
|
632
1436
|
try {
|
|
633
|
-
const observe = (stage,
|
|
1437
|
+
const observe = (stage, next) => observeExecution(observer, {
|
|
634
1438
|
kind: "command",
|
|
635
1439
|
operation: command.name,
|
|
636
1440
|
stage,
|
|
637
|
-
|
|
638
|
-
},
|
|
1441
|
+
...executionTrace(invocation.requestContext)
|
|
1442
|
+
}, next);
|
|
639
1443
|
await observe("authorize", () => governance.authorize(invocation));
|
|
640
1444
|
let execute = async () => {
|
|
641
1445
|
const result = await observe("handler", invokeOnce);
|
|
@@ -669,13 +1473,15 @@ async function executeCompiledCommand(options) {
|
|
|
669
1473
|
if (matches.length !== 1)
|
|
670
1474
|
throw new ApplicationError("Command descriptor missing or ambiguous", { code: "COMMAND_NOT_REGISTERED" });
|
|
671
1475
|
const command = matches[0];
|
|
1476
|
+
if (!command)
|
|
1477
|
+
throw new Error(`Command not found: ${options.command}`);
|
|
672
1478
|
const invocation = {
|
|
673
1479
|
command,
|
|
674
1480
|
input: { body: options.input, params: {}, query: {} },
|
|
675
1481
|
request: options.request,
|
|
676
1482
|
requestContext: options.requestContext,
|
|
677
1483
|
services: options.services ?? {},
|
|
678
|
-
scope: options.scope
|
|
1484
|
+
...options.scope ? { scope: options.scope } : {}
|
|
679
1485
|
};
|
|
680
1486
|
const aspects = composeAspects(observedAspects(options.module.aspects ?? [], `module:${options.module.name}`, options.observer), observedAspects(command.aspects ?? [], "command", options.observer));
|
|
681
1487
|
const value = await createCommandExecutor(options.governance, options.observer)(invocation, once(() => aspects({
|
|
@@ -685,11 +1491,44 @@ async function executeCompiledCommand(options) {
|
|
|
685
1491
|
request: options.request,
|
|
686
1492
|
requestContext: options.requestContext,
|
|
687
1493
|
services: invocation.services,
|
|
688
|
-
scope: invocation.scope,
|
|
1494
|
+
...invocation.scope ? { scope: invocation.scope } : {},
|
|
689
1495
|
metadata: command
|
|
690
1496
|
}, once(() => options.handler(options.input)))));
|
|
691
1497
|
return options.decode(value);
|
|
692
1498
|
}
|
|
1499
|
+
async function previewCompiledCommand(options) {
|
|
1500
|
+
const matches = options.module.commands?.filter((item) => item.className === options.command) ?? [];
|
|
1501
|
+
if (matches.length !== 1)
|
|
1502
|
+
throw new ApplicationError("Command descriptor missing or ambiguous", { code: "COMMAND_NOT_REGISTERED" });
|
|
1503
|
+
const command = matches[0];
|
|
1504
|
+
if (!command)
|
|
1505
|
+
throw new Error(`Command not found: ${options.command}`);
|
|
1506
|
+
const invocation = {
|
|
1507
|
+
command,
|
|
1508
|
+
input: { body: options.input, params: {}, query: {} },
|
|
1509
|
+
request: options.request,
|
|
1510
|
+
requestContext: options.requestContext,
|
|
1511
|
+
services: options.services ?? {},
|
|
1512
|
+
...options.scope ? { scope: options.scope } : {}
|
|
1513
|
+
};
|
|
1514
|
+
try {
|
|
1515
|
+
await options.governance.authorize(invocation);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
if (error instanceof ApplicationError && (error.status === 401 || error.status === 403)) {
|
|
1518
|
+
return decodeCommandPreview({
|
|
1519
|
+
command: command.name,
|
|
1520
|
+
allowed: false,
|
|
1521
|
+
blockers: [{ code: error.code, message: error.message }]
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
throw error;
|
|
1525
|
+
}
|
|
1526
|
+
const preview = decodeCommandPreview(await options.preview(options.input));
|
|
1527
|
+
if (preview.command !== command.name && preview.command !== command.className) {
|
|
1528
|
+
throw new TypeError("Mismatched command identity");
|
|
1529
|
+
}
|
|
1530
|
+
return preview;
|
|
1531
|
+
}
|
|
693
1532
|
function once(next) {
|
|
694
1533
|
let called = false;
|
|
695
1534
|
return async () => {
|
|
@@ -731,6 +1570,26 @@ function joinPaths(prefix, path) {
|
|
|
731
1570
|
function controllerInstance(value) {
|
|
732
1571
|
return isRecord(value) ? value : undefined;
|
|
733
1572
|
}
|
|
1573
|
+
function cookieValues(value) {
|
|
1574
|
+
if (!isRecord(value))
|
|
1575
|
+
return;
|
|
1576
|
+
const result = {};
|
|
1577
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
1578
|
+
result[name] = isRecord(entry) && "value" in entry ? entry.value : entry;
|
|
1579
|
+
}
|
|
1580
|
+
return result;
|
|
1581
|
+
}
|
|
1582
|
+
function assertDeclaredResponseStatus(route, value, configuredStatus) {
|
|
1583
|
+
if (route.responses === undefined)
|
|
1584
|
+
return;
|
|
1585
|
+
const status = responseStatusOf(value, configuredStatus);
|
|
1586
|
+
if (responseStatusDeclared(route.responses, status))
|
|
1587
|
+
return;
|
|
1588
|
+
const transport = route.contract?.response;
|
|
1589
|
+
if ((transport === "binary" || transport === "stream") && status >= 200 && status < 300)
|
|
1590
|
+
return;
|
|
1591
|
+
throw new ApplicationError("Response validation failed", { status: 500, code: "RESPONSE_CONTRACT_UNDECLARED" });
|
|
1592
|
+
}
|
|
734
1593
|
function createModulePlugin(compiled, services, ctxFactory = defaultRequestContext, options = {}, imported = {}) {
|
|
735
1594
|
const hasCommandRoutes = compiled.controllers.some((controller) => controller.routes.some((route) => route.command !== undefined));
|
|
736
1595
|
if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
|
|
@@ -749,7 +1608,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
749
1608
|
const requestContexts = new WeakMap;
|
|
750
1609
|
const governanceExecutor = options.commandGovernance ? createCommandExecutor(options.commandGovernance, options.onExecution) : undefined;
|
|
751
1610
|
const commandExecutor = options.commandExecutor && governanceExecutor ? composeCommandExecutors(options.commandExecutor, governanceExecutor) : options.commandExecutor ?? governanceExecutor;
|
|
752
|
-
const plugin = new
|
|
1611
|
+
const plugin = new Elysia2({ name: `supacloud:${compiled.name}`, normalize: options.normalize ?? true }).decorate("services", services);
|
|
753
1612
|
const createRequestScope = compiled.createRequestScope;
|
|
754
1613
|
const requestScopes = new WeakMap;
|
|
755
1614
|
if (compiled.destroyRequestScope) {
|
|
@@ -783,99 +1642,98 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
783
1642
|
for (const controller of compiled.controllers) {
|
|
784
1643
|
for (const route of controller.routes) {
|
|
785
1644
|
const path = joinPaths(controller.path, route.path);
|
|
786
|
-
const schema =
|
|
787
|
-
if (route.body !== undefined)
|
|
788
|
-
schema.body = route.body;
|
|
789
|
-
if (route.params !== undefined)
|
|
790
|
-
schema.params = route.params;
|
|
791
|
-
if (route.query !== undefined)
|
|
792
|
-
schema.query = route.query;
|
|
793
|
-
if (route.response !== undefined)
|
|
794
|
-
schema.response = route.response;
|
|
1645
|
+
const schema = toElysiaRouteSchema(route);
|
|
795
1646
|
const handler = async (ctx) => {
|
|
796
1647
|
const requestContext = ctx.requestContext ?? await ctxFactory(ctx.request);
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const input = {
|
|
807
|
-
body: ctx.body,
|
|
808
|
-
params: ctx.params,
|
|
809
|
-
query: ctx.query,
|
|
810
|
-
headers: Object.fromEntries(ctx.request.headers.entries()),
|
|
811
|
-
context: requestContext,
|
|
812
|
-
request: ctx.request,
|
|
813
|
-
scope: requestScope,
|
|
814
|
-
requestContext
|
|
815
|
-
};
|
|
816
|
-
const handlerCall = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
|
|
817
|
-
const invoke = once(() => route.command && options.commandGovernance ? handlerCall() : observeExecution(options.onExecution, {
|
|
818
|
-
kind: route.command ? "command" : "route",
|
|
819
|
-
operation: route.command ?? `${route.method} ${path}`,
|
|
820
|
-
stage: "handler",
|
|
821
|
-
requestId: executionRequestId(requestContext)
|
|
822
|
-
}, handlerCall));
|
|
823
|
-
const routeContext = {
|
|
824
|
-
kind: "route",
|
|
825
|
-
name: `${route.method} ${path}`,
|
|
826
|
-
input,
|
|
827
|
-
request: ctx.request,
|
|
828
|
-
requestContext,
|
|
829
|
-
scope: requestScope,
|
|
830
|
-
services,
|
|
831
|
-
metadata: route
|
|
832
|
-
};
|
|
833
|
-
const command = route.command ? commandsByClassName.get(route.command) : undefined;
|
|
834
|
-
const commandContext = {
|
|
835
|
-
kind: "command",
|
|
836
|
-
name: command?.name ?? route.command ?? `${route.method} ${path}`,
|
|
837
|
-
input,
|
|
838
|
-
request: ctx.request,
|
|
839
|
-
requestContext,
|
|
840
|
-
scope: requestScope,
|
|
841
|
-
services,
|
|
842
|
-
metadata: command ?? route
|
|
843
|
-
};
|
|
844
|
-
const commandAspects = route.command ? commandsByClassName.get(route.command)?.aspects ?? [] : [];
|
|
845
|
-
const routePipeline = observedAspects(route.aspects ?? [], "route", options.onExecution);
|
|
846
|
-
const commandPipeline = observedAspects(commandAspects, "command", options.onExecution);
|
|
847
|
-
const modulePipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, options.onExecution);
|
|
848
|
-
const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
|
|
849
|
-
const invokeCommand = () => {
|
|
850
|
-
if (!route.command)
|
|
851
|
-
return invoke();
|
|
852
|
-
if (!command) {
|
|
853
|
-
throw new ApplicationError(`Command "${route.command}" is not registered`, {
|
|
854
|
-
code: "COMMAND_NOT_REGISTERED"
|
|
855
|
-
});
|
|
1648
|
+
const execute = async () => {
|
|
1649
|
+
const requestScope = createRequestScope ? await createRequestScope(services, requestContext, imported) : undefined;
|
|
1650
|
+
if (requestScope && compiled.destroyRequestScope)
|
|
1651
|
+
requestScopes.set(ctx.request, requestScope);
|
|
1652
|
+
const source = controller.scope === "request" ? requestScope : services;
|
|
1653
|
+
const instance = controllerInstance(source?.[controller.serviceKey]);
|
|
1654
|
+
const method = instance?.[route.handler];
|
|
1655
|
+
if (typeof method !== "function") {
|
|
1656
|
+
throw new Error(`supacloud: controller "${controller.serviceKey}" has no handler "${route.handler}" in scope "${controller.scope}"`);
|
|
856
1657
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1658
|
+
const normalizedCookie = ctx.cookie === undefined ? undefined : cookieValues(ctx.cookie);
|
|
1659
|
+
const input = {
|
|
1660
|
+
body: ctx.body,
|
|
1661
|
+
params: ctx.params,
|
|
1662
|
+
query: ctx.query,
|
|
1663
|
+
headers: ctx.headers ?? Object.fromEntries(ctx.request.headers.entries()),
|
|
1664
|
+
...normalizedCookie === undefined ? {} : { cookie: normalizedCookie },
|
|
1665
|
+
context: requestContext,
|
|
1666
|
+
request: ctx.request,
|
|
1667
|
+
...requestScope ? { scope: requestScope } : {},
|
|
1668
|
+
requestContext
|
|
1669
|
+
};
|
|
1670
|
+
const handlerCall = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
|
|
1671
|
+
const invoke = once(() => route.command && options.commandGovernance ? handlerCall() : observeExecution(options.onExecution, {
|
|
1672
|
+
kind: route.command ? "command" : "route",
|
|
1673
|
+
operation: route.command ?? `${route.method} ${path}`,
|
|
1674
|
+
stage: "handler",
|
|
1675
|
+
...executionTrace(requestContext)
|
|
1676
|
+
}, handlerCall));
|
|
1677
|
+
const routeContext = {
|
|
1678
|
+
kind: "route",
|
|
1679
|
+
name: `${route.method} ${path}`,
|
|
865
1680
|
input,
|
|
866
1681
|
request: ctx.request,
|
|
867
1682
|
requestContext,
|
|
868
|
-
scope: requestScope,
|
|
869
|
-
services
|
|
1683
|
+
...requestScope ? { scope: requestScope } : {},
|
|
1684
|
+
services,
|
|
1685
|
+
metadata: route
|
|
870
1686
|
};
|
|
871
|
-
|
|
1687
|
+
const command = route.command ? commandsByClassName.get(route.command) : undefined;
|
|
1688
|
+
const commandContext = {
|
|
872
1689
|
kind: "command",
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1690
|
+
name: command?.name ?? route.command ?? `${route.method} ${path}`,
|
|
1691
|
+
input,
|
|
1692
|
+
request: ctx.request,
|
|
1693
|
+
requestContext,
|
|
1694
|
+
...requestScope ? { scope: requestScope } : {},
|
|
1695
|
+
services,
|
|
1696
|
+
metadata: command ?? route
|
|
1697
|
+
};
|
|
1698
|
+
const commandAspects = route.command ? commandsByClassName.get(route.command)?.aspects ?? [] : [];
|
|
1699
|
+
const routePipeline = observedAspects(route.aspects ?? [], "route", options.onExecution);
|
|
1700
|
+
const commandPipeline = observedAspects(commandAspects, "command", options.onExecution);
|
|
1701
|
+
const modulePipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, options.onExecution);
|
|
1702
|
+
const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
|
|
1703
|
+
const invokeCommand = () => {
|
|
1704
|
+
if (!route.command)
|
|
1705
|
+
return invoke();
|
|
1706
|
+
if (!command) {
|
|
1707
|
+
throw new ApplicationError(`Command "${route.command}" is not registered`, {
|
|
1708
|
+
code: "COMMAND_NOT_REGISTERED"
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
if (!commandExecutor) {
|
|
1712
|
+
throw new ApplicationError(`Command "${command.name}" has no executor`, {
|
|
1713
|
+
status: 501,
|
|
1714
|
+
code: "COMMAND_EXECUTOR_UNAVAILABLE"
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
const invocation = {
|
|
1718
|
+
command,
|
|
1719
|
+
input,
|
|
1720
|
+
request: ctx.request,
|
|
1721
|
+
requestContext,
|
|
1722
|
+
...requestScope ? { scope: requestScope } : {},
|
|
1723
|
+
services
|
|
1724
|
+
};
|
|
1725
|
+
return observeExecution(options.onExecution, {
|
|
1726
|
+
kind: "command",
|
|
1727
|
+
operation: command.name,
|
|
1728
|
+
stage: "commandExecutor",
|
|
1729
|
+
...executionTrace(requestContext)
|
|
1730
|
+
}, () => commandExecutor(invocation, invoke));
|
|
1731
|
+
};
|
|
1732
|
+
return modulePipeline(route.command ? commandContext : routeContext, invokeRoute);
|
|
877
1733
|
};
|
|
878
|
-
|
|
1734
|
+
const result = options.injector ? await runInRequestContext(options.injector, [provideToken(REQUEST_CONTEXT, requestContext)], execute) : await execute();
|
|
1735
|
+
assertDeclaredResponseStatus(route, result, ctx.set.status);
|
|
1736
|
+
return result;
|
|
879
1737
|
};
|
|
880
1738
|
switch (route.method) {
|
|
881
1739
|
case "GET":
|
|
@@ -905,6 +1763,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
905
1763
|
return plugin;
|
|
906
1764
|
}
|
|
907
1765
|
async function executeJob(compiled, services, job, input, requestContext, imported = {}, executor, observer) {
|
|
1766
|
+
const decodedInput = decodeJobContract(job.input, input, "input");
|
|
908
1767
|
const jobScope = job.scope === "job" && compiled.createJobScope ? await compiled.createJobScope(services, requestContext, imported) : undefined;
|
|
909
1768
|
try {
|
|
910
1769
|
const source = job.scope === "job" ? jobScope : services;
|
|
@@ -915,17 +1774,17 @@ async function executeJob(compiled, services, job, input, requestContext, import
|
|
|
915
1774
|
}
|
|
916
1775
|
const invocation = {
|
|
917
1776
|
job,
|
|
918
|
-
input,
|
|
1777
|
+
input: decodedInput,
|
|
919
1778
|
requestContext,
|
|
920
|
-
scope: jobScope,
|
|
1779
|
+
...jobScope ? { scope: jobScope } : {},
|
|
921
1780
|
services
|
|
922
1781
|
};
|
|
923
1782
|
const context = {
|
|
924
1783
|
kind: "job",
|
|
925
1784
|
name: job.name,
|
|
926
|
-
input,
|
|
1785
|
+
input: decodedInput,
|
|
927
1786
|
requestContext,
|
|
928
|
-
scope: jobScope,
|
|
1787
|
+
...jobScope ? { scope: jobScope } : {},
|
|
929
1788
|
services,
|
|
930
1789
|
metadata: job
|
|
931
1790
|
};
|
|
@@ -933,16 +1792,17 @@ async function executeJob(compiled, services, job, input, requestContext, import
|
|
|
933
1792
|
kind: "job",
|
|
934
1793
|
operation: job.name,
|
|
935
1794
|
stage: "handler",
|
|
936
|
-
|
|
937
|
-
}, () => Reflect.apply(method, instance, [
|
|
1795
|
+
...executionTrace(requestContext)
|
|
1796
|
+
}, () => Reflect.apply(method, instance, [decodedInput])));
|
|
938
1797
|
const pipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, observer);
|
|
939
1798
|
const jobPipeline = observedAspects(job.aspects ?? [], "job", observer);
|
|
940
|
-
|
|
1799
|
+
const result = await pipeline(context, () => jobPipeline(context, () => observeExecution(observer, {
|
|
941
1800
|
kind: "job",
|
|
942
1801
|
operation: job.name,
|
|
943
1802
|
stage: "jobExecutor",
|
|
944
|
-
|
|
1803
|
+
...executionTrace(requestContext)
|
|
945
1804
|
}, executor ? () => executor(invocation, invoke) : invoke)));
|
|
1805
|
+
return decodeJobContract(job.output, result, "output");
|
|
946
1806
|
} finally {
|
|
947
1807
|
if (jobScope && compiled.destroyJobScope) {
|
|
948
1808
|
await compiled.destroyJobScope(jobScope);
|
|
@@ -950,6 +1810,11 @@ async function executeJob(compiled, services, job, input, requestContext, import
|
|
|
950
1810
|
}
|
|
951
1811
|
}
|
|
952
1812
|
function defaultErrorResponse(error, frameworkCode) {
|
|
1813
|
+
if (error instanceof CommandError2) {
|
|
1814
|
+
return Response.json({ ok: false, code: error.code, message: error.code }, {
|
|
1815
|
+
status: commandErrorStatus(error.code)
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
953
1818
|
if (isPublicApplicationError(error)) {
|
|
954
1819
|
return Response.json({
|
|
955
1820
|
ok: false,
|
|
@@ -984,7 +1849,9 @@ function isPublicApplicationError(error) {
|
|
|
984
1849
|
return error.expose === true && typeof error.status === "number" && Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 && typeof error.code === "string" && error.code.length > 0;
|
|
985
1850
|
}
|
|
986
1851
|
function createApplication(options) {
|
|
987
|
-
const app = new
|
|
1852
|
+
const app = new Elysia2({ name: options.name ?? "supacloud:app", normalize: options.normalize ?? true });
|
|
1853
|
+
if (options.documentation)
|
|
1854
|
+
app.use(createDocumentationPlugin(options.documentation));
|
|
988
1855
|
const configuredContextFactory = options.requestContext ?? defaultRequestContext;
|
|
989
1856
|
const contextCache = new WeakMap;
|
|
990
1857
|
const ctxFactory = (request) => {
|
|
@@ -999,12 +1866,7 @@ function createApplication(options) {
|
|
|
999
1866
|
for (const module of options.modules ?? []) {
|
|
1000
1867
|
const services = module.createServices(options.deps ?? {}, imported);
|
|
1001
1868
|
imported[module.name] = services;
|
|
1002
|
-
app.use(createModulePlugin(module, services, ctxFactory,
|
|
1003
|
-
commandGovernance: options.commandGovernance,
|
|
1004
|
-
commandExecutor: options.commandExecutor,
|
|
1005
|
-
errorMapper: options.errorMapper,
|
|
1006
|
-
onExecution: options.onExecution
|
|
1007
|
-
}, imported));
|
|
1869
|
+
app.use(createModulePlugin(module, services, ctxFactory, options, imported));
|
|
1008
1870
|
}
|
|
1009
1871
|
return app;
|
|
1010
1872
|
}
|
|
@@ -1015,22 +1877,41 @@ export {
|
|
|
1015
1877
|
ApplicationError,
|
|
1016
1878
|
EXECUTION_ID_HEADER,
|
|
1017
1879
|
IDEMPOTENCY_KEY_HEADER,
|
|
1880
|
+
SchemaContractError,
|
|
1881
|
+
SupaCloudWorker,
|
|
1018
1882
|
VERIFIED_JWT_SUBJECT_HEADER,
|
|
1883
|
+
WorkerReceiptUnconfirmedError,
|
|
1884
|
+
WorkerRegistrationError,
|
|
1019
1885
|
assertFeatureTransition,
|
|
1886
|
+
assertResponseStatusDeclared,
|
|
1020
1887
|
composeAspects,
|
|
1021
1888
|
composeCommandExecutors,
|
|
1022
1889
|
createApplication,
|
|
1023
1890
|
createCommandExecutor,
|
|
1891
|
+
createDocumentationPlugin,
|
|
1024
1892
|
createMemoryPolicy,
|
|
1025
1893
|
createMemorySandbox,
|
|
1026
1894
|
createModulePlugin,
|
|
1895
|
+
createPersistentCommandAdapter,
|
|
1896
|
+
createQueueWorkerTransport,
|
|
1897
|
+
createSchemaDecoder,
|
|
1027
1898
|
createSupAuthRequestContext,
|
|
1028
1899
|
createSupaCloudRequestContext,
|
|
1029
1900
|
createTestApp,
|
|
1901
|
+
createWorker,
|
|
1030
1902
|
defaultErrorResponse,
|
|
1903
|
+
defineElysiaRoute,
|
|
1904
|
+
defineJsonContract,
|
|
1905
|
+
defineRouteContract,
|
|
1031
1906
|
executeCompiledCommand,
|
|
1032
1907
|
executeJob,
|
|
1908
|
+
previewCompiledCommand,
|
|
1909
|
+
registerElysiaRoute,
|
|
1033
1910
|
requireIdempotencyKey,
|
|
1034
1911
|
requireTrustedIdentity,
|
|
1912
|
+
responseSchemaForStatus,
|
|
1913
|
+
responseStatusDeclared,
|
|
1914
|
+
responseStatusOf,
|
|
1915
|
+
toElysiaRouteSchema,
|
|
1035
1916
|
validatedJsonResponse
|
|
1036
1917
|
};
|