@supacloud/elysia 0.4.0 → 0.5.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 +23 -2
- package/dist/index.d.ts +51 -1
- package/dist/index.js +154 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,13 +8,19 @@ Runtime adapter that turns `@supacloud/compiler` output into a production-ready
|
|
|
8
8
|
- **Decoupled compilation**: takes the output of `@supacloud/compiler` directly.
|
|
9
9
|
- **Topological initialization**: modules are registered in dependency order,
|
|
10
10
|
passing exported services downstream via Elysia plugins.
|
|
11
|
-
- **Request-scoped providers**: creates a fresh scope per HTTP request via
|
|
12
|
-
`createRequestScope`, mapping request-scoped
|
|
11
|
+
- **Request-scoped providers**: creates a fresh scope per HTTP request via the
|
|
12
|
+
asynchronous compiler-generated `createRequestScope`, mapping request-scoped
|
|
13
|
+
controllers and services.
|
|
14
|
+
- **Request-scope teardown**: invokes the compiler-generated
|
|
15
|
+
`destroyRequestScope` after the response, including when the handler fails.
|
|
13
16
|
- **TypeBox schema binding**: attaches compiled parameter, query, body, and
|
|
14
17
|
response TypeBox schemas directly to Elysia route definitions.
|
|
15
18
|
- **Unified Command Pipeline**: runs `@Command`-decorated handlers through a
|
|
16
19
|
structured `commandGovernance` adapter chain or a custom `composeCommandExecutors`
|
|
17
20
|
pipeline (fail-closed if command routes lack an executor).
|
|
21
|
+
- **Static AOP pipeline**: executes compiler-emitted module, route, command, and
|
|
22
|
+
job aspects with `composeAspects`; no runtime discovery or registration is
|
|
23
|
+
performed.
|
|
18
24
|
- **Public error mapping**: transforms framework / application errors via
|
|
19
25
|
`errorMapper` with standard `ApplicationError` envelope support, preserving
|
|
20
26
|
Elysia's default behavior (422) for schema validation errors.
|
|
@@ -52,6 +58,11 @@ const app = createApplication({
|
|
|
52
58
|
export default app;
|
|
53
59
|
```
|
|
54
60
|
|
|
61
|
+
Jobs are executed explicitly with `executeJob(compiledModule, services, job,
|
|
62
|
+
input, requestContext)`. The asynchronous compiler-generated job scope is
|
|
63
|
+
destroyed after execution, including when the job throws or scope construction
|
|
64
|
+
fails partway through.
|
|
65
|
+
|
|
55
66
|
## API
|
|
56
67
|
|
|
57
68
|
### `createApplication(options: ApplicationOptions): Elysia`
|
|
@@ -67,6 +78,16 @@ directly onto an existing Elysia app.
|
|
|
67
78
|
|
|
68
79
|
Composes multiple `CommandExecutor` middleware functions into an onion-style pipeline.
|
|
69
80
|
|
|
81
|
+
### `composeAspects(...aspects): ApplicationAspect`
|
|
82
|
+
|
|
83
|
+
Composes static `around(context, next)` functions. Calling `next()` more than
|
|
84
|
+
once is rejected.
|
|
85
|
+
|
|
86
|
+
### `executeJob(...)`
|
|
87
|
+
|
|
88
|
+
Executes a compiler-emitted Job descriptor with its static aspect list and
|
|
89
|
+
compiler-generated job scope.
|
|
90
|
+
|
|
70
91
|
### `ApplicationError`
|
|
71
92
|
|
|
72
93
|
Lightweight error class carrying HTTP `status`, machine-readable `code`, and
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface CompiledRoute {
|
|
|
11
11
|
response?: unknown;
|
|
12
12
|
/** Class name of the @Command explicitly bound to this route. */
|
|
13
13
|
command?: string;
|
|
14
|
+
/** Statically generated route aspects. */
|
|
15
|
+
aspects?: ApplicationAspect[];
|
|
14
16
|
}
|
|
15
17
|
export interface CompiledCommand {
|
|
16
18
|
className: string;
|
|
@@ -19,6 +21,15 @@ export interface CompiledCommand {
|
|
|
19
21
|
transaction?: "required" | "none" | string;
|
|
20
22
|
audit?: string;
|
|
21
23
|
idempotency?: "required" | "none" | string;
|
|
24
|
+
/** Statically generated command aspects. */
|
|
25
|
+
aspects?: ApplicationAspect[];
|
|
26
|
+
}
|
|
27
|
+
export interface CompiledJob {
|
|
28
|
+
className: string;
|
|
29
|
+
name: string;
|
|
30
|
+
serviceKey: string;
|
|
31
|
+
scope: "application" | "request" | "job";
|
|
32
|
+
aspects?: ApplicationAspect[];
|
|
22
33
|
}
|
|
23
34
|
export interface CompiledController {
|
|
24
35
|
/** Controller path prefix, e.g. "/cases". */
|
|
@@ -31,9 +42,15 @@ export interface CompiledController {
|
|
|
31
42
|
export interface CompiledModule {
|
|
32
43
|
name: string;
|
|
33
44
|
createServices(deps: Record<string, unknown>, imported: Record<string, Record<string, unknown>>): Record<string, unknown>;
|
|
34
|
-
createRequestScope?(services: Record<string, unknown>, ctx: unknown, imported?: Record<string, Record<string, unknown>>): Record<string, unknown
|
|
45
|
+
createRequestScope?(services: Record<string, unknown>, ctx: unknown, imported?: Record<string, Record<string, unknown>>): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
46
|
+
destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
|
|
47
|
+
createJobScope?(services: Record<string, unknown>, ctx: unknown, imported?: Record<string, Record<string, unknown>>): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
48
|
+
destroyJobScope?(scope: Record<string, unknown>): Promise<void>;
|
|
35
49
|
controllers: CompiledController[];
|
|
36
50
|
commands?: CompiledCommand[];
|
|
51
|
+
jobs?: CompiledJob[];
|
|
52
|
+
/** Statically generated module aspects. */
|
|
53
|
+
aspects?: ApplicationAspect[];
|
|
37
54
|
}
|
|
38
55
|
export type RequestContextFactory = (request: Request) => unknown | Promise<unknown>;
|
|
39
56
|
export declare const VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
|
|
@@ -65,6 +82,23 @@ export interface CommandInvocation {
|
|
|
65
82
|
services: Record<string, unknown>;
|
|
66
83
|
}
|
|
67
84
|
export type CommandExecutor = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
|
|
85
|
+
export interface ApplicationAspectContext {
|
|
86
|
+
kind: "route" | "command" | "job";
|
|
87
|
+
name: string;
|
|
88
|
+
input: unknown;
|
|
89
|
+
request?: Request;
|
|
90
|
+
requestContext?: unknown;
|
|
91
|
+
scope?: Record<string, unknown>;
|
|
92
|
+
services?: Record<string, unknown>;
|
|
93
|
+
metadata?: unknown;
|
|
94
|
+
}
|
|
95
|
+
export type ApplicationAspect = (context: ApplicationAspectContext, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
|
|
96
|
+
/**
|
|
97
|
+
* Compose the compiler-emitted aspect list into a deterministic onion chain.
|
|
98
|
+
* The runtime only executes the functions it receives; it never discovers or
|
|
99
|
+
* registers aspects.
|
|
100
|
+
*/
|
|
101
|
+
export declare function composeAspects(...aspects: (ApplicationAspect | undefined | null)[]): ApplicationAspect;
|
|
68
102
|
export type CommandAuthorizer = (invocation: CommandInvocation) => void | Promise<void>;
|
|
69
103
|
export type CommandMiddleware = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
|
|
70
104
|
export interface CommandAudit {
|
|
@@ -121,6 +155,14 @@ export interface ApplicationOptions {
|
|
|
121
155
|
/** Maps framework or application failures to the public HTTP contract. */
|
|
122
156
|
errorMapper?: ErrorMapper;
|
|
123
157
|
}
|
|
158
|
+
export interface JobInvocation {
|
|
159
|
+
job: CompiledJob;
|
|
160
|
+
input: unknown;
|
|
161
|
+
requestContext: unknown;
|
|
162
|
+
scope?: Record<string, unknown>;
|
|
163
|
+
services: Record<string, unknown>;
|
|
164
|
+
}
|
|
165
|
+
export type JobExecutor = (invocation: JobInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
|
|
124
166
|
/**
|
|
125
167
|
* Build the standard request context for applications behind SupaCloud Edge
|
|
126
168
|
* Runtime. The runtime strips incoming x-supacloud-jwt-sub values and writes
|
|
@@ -144,6 +186,14 @@ export declare function requireIdempotencyKey(invocation: CommandInvocation): st
|
|
|
144
186
|
* looked up on `scope`, everything else on `services`.
|
|
145
187
|
*/
|
|
146
188
|
export declare function createModulePlugin(compiled: CompiledModule, services: Record<string, unknown>, ctxFactory?: RequestContextFactory, options?: Pick<ApplicationOptions, "commandGovernance" | "commandExecutor" | "errorMapper">, imported?: Record<string, Record<string, unknown>>): Elysia;
|
|
189
|
+
/**
|
|
190
|
+
* Execute one compiler-emitted Job descriptor.
|
|
191
|
+
*
|
|
192
|
+
* Job classes use `run(input)` as their entry method; `execute(input)` is also
|
|
193
|
+
* accepted for command-like job implementations. The scope factory and
|
|
194
|
+
* destruction hooks are generated statically by the compiler.
|
|
195
|
+
*/
|
|
196
|
+
export declare function executeJob(compiled: CompiledModule, services: Record<string, unknown>, job: CompiledJob, input: unknown, requestContext: unknown, imported?: Record<string, Record<string, unknown>>, executor?: JobExecutor): Promise<unknown>;
|
|
147
197
|
export declare function defaultErrorResponse(error: unknown, frameworkCode?: string | number): Response;
|
|
148
198
|
/**
|
|
149
199
|
* Create the root Elysia application from compiled modules.
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,22 @@ import { Elysia } from "elysia";
|
|
|
3
3
|
var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
|
|
4
4
|
var EXECUTION_ID_HEADER = "x-sb-execution-id";
|
|
5
5
|
var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
6
|
+
function composeAspects(...aspects) {
|
|
7
|
+
const active = aspects.filter((aspect) => typeof aspect === "function");
|
|
8
|
+
return (context, next) => {
|
|
9
|
+
let index = -1;
|
|
10
|
+
const dispatch = (current) => {
|
|
11
|
+
if (current <= index) {
|
|
12
|
+
return Promise.reject(new Error("next() called multiple times"));
|
|
13
|
+
}
|
|
14
|
+
index = current;
|
|
15
|
+
if (current === active.length)
|
|
16
|
+
return Promise.resolve(next());
|
|
17
|
+
return Promise.resolve(active[current](context, () => dispatch(current + 1)));
|
|
18
|
+
};
|
|
19
|
+
return dispatch(0);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
6
22
|
function composeCommandExecutors(...executors) {
|
|
7
23
|
const active = executors.filter((e) => typeof e === "function");
|
|
8
24
|
if (active.length === 0)
|
|
@@ -46,6 +62,15 @@ function safeHeaderValue(value, maxLength) {
|
|
|
46
62
|
}
|
|
47
63
|
return value;
|
|
48
64
|
}
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
function isTrustedIdentity(value) {
|
|
69
|
+
return isRecord(value) && typeof value.authenticated === "boolean" && (value.subject === undefined || typeof value.subject === "string") && (value.accessToken === undefined || typeof value.accessToken === "string");
|
|
70
|
+
}
|
|
71
|
+
function isAuthenticatedTrustedIdentity(value) {
|
|
72
|
+
return value?.authenticated === true && typeof value.subject === "string" && value.subject.length > 0 && typeof value.accessToken === "string" && value.accessToken.length > 0;
|
|
73
|
+
}
|
|
49
74
|
function bearerToken(request) {
|
|
50
75
|
const authorization = request.headers.get("authorization");
|
|
51
76
|
const match = authorization?.match(/^Bearer\s+(.+)$/i);
|
|
@@ -89,43 +114,50 @@ function missingGovernanceAdapter(command, adapter) {
|
|
|
89
114
|
function createCommandExecutor(governance) {
|
|
90
115
|
return async (invocation, next) => {
|
|
91
116
|
const { command } = invocation;
|
|
92
|
-
|
|
117
|
+
const audit = command.audit ? governance.audit : undefined;
|
|
118
|
+
const transaction = command.transaction === "required" ? governance.transaction : undefined;
|
|
119
|
+
const idempotency = command.idempotency === "required" ? governance.idempotency : undefined;
|
|
120
|
+
if (command.audit && !audit) {
|
|
93
121
|
throw missingGovernanceAdapter(command, "audit");
|
|
94
122
|
}
|
|
95
|
-
if (command.transaction === "required" && !
|
|
123
|
+
if (command.transaction === "required" && !transaction) {
|
|
96
124
|
throw missingGovernanceAdapter(command, "transaction");
|
|
97
125
|
}
|
|
98
|
-
if (command.idempotency === "required" && !
|
|
126
|
+
if (command.idempotency === "required" && !idempotency) {
|
|
99
127
|
throw missingGovernanceAdapter(command, "idempotency");
|
|
100
128
|
}
|
|
101
129
|
try {
|
|
102
130
|
await governance.authorize(invocation);
|
|
103
131
|
let execute = async () => {
|
|
104
132
|
const result = await next();
|
|
105
|
-
if (
|
|
106
|
-
await
|
|
133
|
+
if (audit)
|
|
134
|
+
await audit.succeeded.call(audit, invocation, result);
|
|
107
135
|
return result;
|
|
108
136
|
};
|
|
109
137
|
if (command.transaction === "required") {
|
|
138
|
+
if (!transaction)
|
|
139
|
+
throw missingGovernanceAdapter(command, "transaction");
|
|
110
140
|
const inner = execute;
|
|
111
|
-
execute = () => Promise.resolve(
|
|
141
|
+
execute = () => Promise.resolve(transaction.call(governance, invocation, inner));
|
|
112
142
|
}
|
|
113
143
|
if (command.idempotency === "required") {
|
|
144
|
+
if (!idempotency)
|
|
145
|
+
throw missingGovernanceAdapter(command, "idempotency");
|
|
114
146
|
const inner = execute;
|
|
115
|
-
execute = () => Promise.resolve(
|
|
147
|
+
execute = () => Promise.resolve(idempotency.call(governance, invocation, inner));
|
|
116
148
|
}
|
|
117
149
|
return await execute();
|
|
118
150
|
} catch (error) {
|
|
119
|
-
if (
|
|
120
|
-
await
|
|
151
|
+
if (audit)
|
|
152
|
+
await audit.failed.call(audit, invocation, error);
|
|
121
153
|
throw error;
|
|
122
154
|
}
|
|
123
155
|
};
|
|
124
156
|
}
|
|
125
157
|
function requireTrustedIdentity(requestContext) {
|
|
126
|
-
const context = requestContext;
|
|
127
|
-
const identity = context
|
|
128
|
-
if (!identity
|
|
158
|
+
const context = isRecord(requestContext) ? requestContext : {};
|
|
159
|
+
const identity = isTrustedIdentity(context.identity) ? context.identity : undefined;
|
|
160
|
+
if (!isAuthenticatedTrustedIdentity(identity)) {
|
|
129
161
|
throw new ApplicationError("Authenticated user context is required", {
|
|
130
162
|
status: 401,
|
|
131
163
|
code: "AUTHENTICATION_REQUIRED"
|
|
@@ -134,8 +166,8 @@ function requireTrustedIdentity(requestContext) {
|
|
|
134
166
|
return identity;
|
|
135
167
|
}
|
|
136
168
|
function requireIdempotencyKey(invocation) {
|
|
137
|
-
const context = invocation.requestContext;
|
|
138
|
-
const key = context
|
|
169
|
+
const context = isRecord(invocation.requestContext) ? invocation.requestContext : {};
|
|
170
|
+
const key = (typeof context.idempotencyKey === "string" ? context.idempotencyKey : undefined) ?? safeHeaderValue(invocation.request.headers.get(IDEMPOTENCY_KEY_HEADER), 512);
|
|
139
171
|
if (!key) {
|
|
140
172
|
throw new ApplicationError("Idempotency-Key header is required", {
|
|
141
173
|
status: 400,
|
|
@@ -148,6 +180,9 @@ function joinPaths(prefix, path) {
|
|
|
148
180
|
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
149
181
|
return joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
150
182
|
}
|
|
183
|
+
function controllerInstance(value) {
|
|
184
|
+
return isRecord(value) ? value : undefined;
|
|
185
|
+
}
|
|
151
186
|
function createModulePlugin(compiled, services, ctxFactory = defaultRequestContext, options = {}, imported = {}) {
|
|
152
187
|
const hasCommandRoutes = compiled.controllers.some((controller) => controller.routes.some((route) => route.command !== undefined));
|
|
153
188
|
if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
|
|
@@ -168,6 +203,21 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
168
203
|
const commandExecutor = options.commandExecutor && governanceExecutor ? composeCommandExecutors(options.commandExecutor, governanceExecutor) : options.commandExecutor ?? governanceExecutor;
|
|
169
204
|
const plugin = new Elysia({ name: `supacloud:${compiled.name}` }).decorate("services", services);
|
|
170
205
|
const createRequestScope = compiled.createRequestScope;
|
|
206
|
+
const requestScopes = new WeakMap;
|
|
207
|
+
if (compiled.destroyRequestScope) {
|
|
208
|
+
const destroyRequestScope = compiled.destroyRequestScope;
|
|
209
|
+
plugin.onAfterResponse(async ({ request }) => {
|
|
210
|
+
const scope = requestScopes.get(request);
|
|
211
|
+
if (!scope)
|
|
212
|
+
return;
|
|
213
|
+
requestScopes.delete(request);
|
|
214
|
+
try {
|
|
215
|
+
await destroyRequestScope(scope);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(`supacloud: request scope cleanup failed for "${compiled.name}"`, error);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
171
221
|
plugin.resolve(async ({ request }) => {
|
|
172
222
|
const requestContext = await ctxFactory(request);
|
|
173
223
|
requestContexts.set(request, requestContext);
|
|
@@ -187,9 +237,11 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
187
237
|
schema.response = route.response;
|
|
188
238
|
const handler = async (ctx) => {
|
|
189
239
|
const requestContext = ctx.requestContext ?? await ctxFactory(ctx.request);
|
|
190
|
-
const requestScope = createRequestScope ? createRequestScope(services, requestContext, imported) : undefined;
|
|
240
|
+
const requestScope = createRequestScope ? await createRequestScope(services, requestContext, imported) : undefined;
|
|
241
|
+
if (requestScope && compiled.destroyRequestScope)
|
|
242
|
+
requestScopes.set(ctx.request, requestScope);
|
|
191
243
|
const source = controller.scope === "request" ? requestScope : services;
|
|
192
|
-
const instance = source?.[controller.serviceKey];
|
|
244
|
+
const instance = controllerInstance(source?.[controller.serviceKey]);
|
|
193
245
|
const method = instance?.[route.handler];
|
|
194
246
|
if (typeof method !== "function") {
|
|
195
247
|
throw new Error(`supacloud: controller "${controller.serviceKey}" has no handler "${route.handler}" in scope "${controller.scope}"`);
|
|
@@ -202,24 +254,58 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
202
254
|
scope: requestScope,
|
|
203
255
|
requestContext
|
|
204
256
|
};
|
|
205
|
-
const invoke = () =>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
257
|
+
const invoke = () => Reflect.apply(method, instance, [input]);
|
|
258
|
+
const routeContext = {
|
|
259
|
+
kind: "route",
|
|
260
|
+
name: `${route.method} ${path}`,
|
|
261
|
+
input,
|
|
262
|
+
request: ctx.request,
|
|
263
|
+
requestContext,
|
|
264
|
+
scope: requestScope,
|
|
265
|
+
services,
|
|
266
|
+
metadata: route
|
|
267
|
+
};
|
|
268
|
+
const command = route.command ? commandsByClassName.get(route.command) : undefined;
|
|
269
|
+
const commandContext = {
|
|
270
|
+
kind: "command",
|
|
271
|
+
name: command?.name ?? route.command ?? `${route.method} ${path}`,
|
|
217
272
|
input,
|
|
218
273
|
request: ctx.request,
|
|
219
274
|
requestContext,
|
|
220
275
|
scope: requestScope,
|
|
221
|
-
services
|
|
222
|
-
|
|
276
|
+
services,
|
|
277
|
+
metadata: command ?? route
|
|
278
|
+
};
|
|
279
|
+
const commandAspects = route.command ? commandsByClassName.get(route.command)?.aspects ?? [] : [];
|
|
280
|
+
const routePipeline = composeAspects(...route.aspects ?? []);
|
|
281
|
+
const commandPipeline = composeAspects(...commandAspects);
|
|
282
|
+
const modulePipeline = composeAspects(...compiled.aspects ?? []);
|
|
283
|
+
const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
|
|
284
|
+
const invokeCommand = () => {
|
|
285
|
+
if (!route.command)
|
|
286
|
+
return invoke();
|
|
287
|
+
if (!command) {
|
|
288
|
+
throw new ApplicationError(`Command "${route.command}" is not registered`, {
|
|
289
|
+
code: "COMMAND_NOT_REGISTERED"
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (!commandExecutor) {
|
|
293
|
+
throw new ApplicationError(`Command "${command.name}" has no executor`, {
|
|
294
|
+
status: 501,
|
|
295
|
+
code: "COMMAND_EXECUTOR_UNAVAILABLE"
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const invocation = {
|
|
299
|
+
command,
|
|
300
|
+
input,
|
|
301
|
+
request: ctx.request,
|
|
302
|
+
requestContext,
|
|
303
|
+
scope: requestScope,
|
|
304
|
+
services
|
|
305
|
+
};
|
|
306
|
+
return commandExecutor(invocation, invoke);
|
|
307
|
+
};
|
|
308
|
+
return modulePipeline(route.command ? commandContext : routeContext, invokeRoute);
|
|
223
309
|
};
|
|
224
310
|
switch (route.method) {
|
|
225
311
|
case "GET":
|
|
@@ -257,6 +343,40 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
257
343
|
});
|
|
258
344
|
return plugin;
|
|
259
345
|
}
|
|
346
|
+
async function executeJob(compiled, services, job, input, requestContext, imported = {}, executor) {
|
|
347
|
+
const jobScope = job.scope === "job" && compiled.createJobScope ? await compiled.createJobScope(services, requestContext, imported) : undefined;
|
|
348
|
+
try {
|
|
349
|
+
const source = job.scope === "job" ? jobScope : services;
|
|
350
|
+
const instance = controllerInstance(source?.[job.serviceKey]);
|
|
351
|
+
const method = instance?.run ?? instance?.execute;
|
|
352
|
+
if (typeof method !== "function") {
|
|
353
|
+
throw new ApplicationError(`Job "${job.name}" has no run(input) or execute(input) handler`, { code: "JOB_HANDLER_UNAVAILABLE" });
|
|
354
|
+
}
|
|
355
|
+
const invocation = {
|
|
356
|
+
job,
|
|
357
|
+
input,
|
|
358
|
+
requestContext,
|
|
359
|
+
scope: jobScope,
|
|
360
|
+
services
|
|
361
|
+
};
|
|
362
|
+
const context = {
|
|
363
|
+
kind: "job",
|
|
364
|
+
name: job.name,
|
|
365
|
+
input,
|
|
366
|
+
requestContext,
|
|
367
|
+
scope: jobScope,
|
|
368
|
+
services,
|
|
369
|
+
metadata: job
|
|
370
|
+
};
|
|
371
|
+
const invoke = () => Reflect.apply(method, instance, [input]);
|
|
372
|
+
const pipeline = composeAspects(...compiled.aspects ?? [], ...job.aspects ?? []);
|
|
373
|
+
return await pipeline(context, executor ? () => executor(invocation, invoke) : invoke);
|
|
374
|
+
} finally {
|
|
375
|
+
if (jobScope && compiled.destroyJobScope) {
|
|
376
|
+
await compiled.destroyJobScope(jobScope);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
260
380
|
function defaultErrorResponse(error, frameworkCode) {
|
|
261
381
|
if (isPublicApplicationError(error)) {
|
|
262
382
|
return Response.json({
|
|
@@ -280,10 +400,9 @@ function defaultErrorResponse(error, frameworkCode) {
|
|
|
280
400
|
}, { status: 500 });
|
|
281
401
|
}
|
|
282
402
|
function isPublicApplicationError(error) {
|
|
283
|
-
if (!(error instanceof Error))
|
|
403
|
+
if (!(error instanceof Error) || !isRecord(error))
|
|
284
404
|
return false;
|
|
285
|
-
|
|
286
|
-
return candidate.expose === true && typeof candidate.status === "number" && Number.isInteger(candidate.status) && candidate.status >= 400 && candidate.status <= 599 && typeof candidate.code === "string" && candidate.code.length > 0;
|
|
405
|
+
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;
|
|
287
406
|
}
|
|
288
407
|
function createApplication(options) {
|
|
289
408
|
const app = new Elysia({ name: options.name ?? "supacloud:app" });
|
|
@@ -317,6 +436,7 @@ export {
|
|
|
317
436
|
EXECUTION_ID_HEADER,
|
|
318
437
|
IDEMPOTENCY_KEY_HEADER,
|
|
319
438
|
VERIFIED_JWT_SUBJECT_HEADER,
|
|
439
|
+
composeAspects,
|
|
320
440
|
composeCommandExecutors,
|
|
321
441
|
createApplication,
|
|
322
442
|
createCommandExecutor,
|
|
@@ -324,6 +444,7 @@ export {
|
|
|
324
444
|
createSupaCloudRequestContext,
|
|
325
445
|
createTestApp,
|
|
326
446
|
defaultErrorResponse,
|
|
447
|
+
executeJob,
|
|
327
448
|
requireIdempotencyKey,
|
|
328
449
|
requireTrustedIdentity
|
|
329
450
|
};
|
package/package.json
CHANGED