@supacloud/elysia 0.14.1 → 0.17.0
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 +97 -7
- package/compatibility.json +9 -0
- package/dist/command-errors.d.ts +1 -0
- package/dist/fixtures/webhook-generated/application.d.ts +7 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +88 -22
- package/dist/persistent-command.d.ts +1 -1
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# @supacloud/elysia
|
|
2
2
|
|
|
3
|
+
## Compatibility and Acceptance Boundary
|
|
4
|
+
|
|
5
|
+
The dependency range is not a claim that every allowed version has been tested.
|
|
6
|
+
The focused conformance suite was verified with Bun 1.4.2 and Elysia 1.4.30.
|
|
7
|
+
The package declares Elysia `^1.4.30` as a peer and TypeScript `^7.0.2` as a
|
|
8
|
+
development dependency. `compatibility.json` records the exact exercised tuple,
|
|
9
|
+
including the compiler's separate TypeScript 6 semantic API. The contract-upgrade
|
|
10
|
+
gate checks both that semantic API and the TypeScript 7 CLI. These tests do not
|
|
11
|
+
establish a wider version matrix or Node.js runtime compatibility.
|
|
12
|
+
|
|
13
|
+
Run `bun run test:conformance` in this package after building the local
|
|
14
|
+
`@supacloud/contracts` and `@supacloud/app` dependencies and installing this
|
|
15
|
+
package's dependencies. The suite runs through `app.handle(Request)` without a
|
|
16
|
+
network listener. It is included in the normal `bun test` discovery.
|
|
17
|
+
|
|
18
|
+
| Boundary | Acceptance evidence in `src/conformance.test.ts` |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| Decoded body, params, query, headers and cookies | Native/adapter response comparison, with explicit decoded-value assertions |
|
|
21
|
+
| Response normalization and declared status maps | Native/adapter comparison, including a 409 response |
|
|
22
|
+
| Native `Response` transport | Status, body, content type, custom header and outgoing cookie preserved |
|
|
23
|
+
| Parent lifecycle hooks | Request, before-handler, handler and after-handler order compared |
|
|
24
|
+
| Parent early return | 403 response compared; controller must not run |
|
|
25
|
+
| Local sibling hooks | Local hook cannot intercept compiled routes |
|
|
26
|
+
| Request schema failure | Native 422 status retained; controller must not run |
|
|
27
|
+
| Malformed JSON | Native 400 status retained for multiple malformed bodies; controller must not run |
|
|
28
|
+
| Error mapper precedence | Custom mapper handles parse failure before request context resolution |
|
|
29
|
+
| Module error isolation | Internal exception redacted; sibling native error handling remains unchanged |
|
|
30
|
+
| Invalid handler output | Intentional 500 response with `RESPONSE_VALIDATION_ERROR` |
|
|
31
|
+
| Unsupported route descriptors | Unsupported methods/native hooks rejected before registration |
|
|
32
|
+
| Duplicate protocol package copies | Known command errors retain their status; unknown codes remain internal |
|
|
33
|
+
|
|
34
|
+
### Intentional Adapter Semantics
|
|
35
|
+
|
|
36
|
+
- Default parse failures return HTTP 400 with `PARSE_ERROR`; request schema
|
|
37
|
+
failures return HTTP 422 with `VALIDATION_ERROR`. Their public messages do not
|
|
38
|
+
include parser details, submitted values or schema internals.
|
|
39
|
+
- Invalid handler output returns HTTP 500, not a client-input error. It may occur
|
|
40
|
+
after business work has completed and must not be interpreted as a rollback.
|
|
41
|
+
- Unknown handler exceptions are redacted. Known `CommandError` instances are
|
|
42
|
+
recognized by their Error identity, name and allowlisted code across separate
|
|
43
|
+
protocol package copies, never by exposing their message. A configured `errorMapper` can
|
|
44
|
+
override these defaults and owns the safety of its response.
|
|
45
|
+
- Cookie input passed to a compiled controller contains decoded values, not
|
|
46
|
+
Elysia's mutable cookie wrappers. Native `Response` headers can carry outgoing
|
|
47
|
+
cookies.
|
|
48
|
+
- Errors before context resolution have no request context. Error mappers must
|
|
49
|
+
not assume identity or request-scoped services are available.
|
|
50
|
+
|
|
51
|
+
### Not Yet Proven by This Suite
|
|
52
|
+
|
|
53
|
+
WebSockets, streaming and disconnect behavior, multipart uploads, signed-cookie
|
|
54
|
+
mutation, arbitrary third-party plugins, alternate runtime/version combinations,
|
|
55
|
+
concurrent tenant isolation, database transaction/idempotency guarantees and
|
|
56
|
+
published-package installation are not covered by the conformance suite alone.
|
|
57
|
+
Runtime safety is covered separately below. This list records
|
|
58
|
+
an evidence gap, not a declaration that all these features are unsupported.
|
|
59
|
+
Do not claim complete Elysia compatibility from this gate.
|
|
60
|
+
|
|
61
|
+
Compiled routes accept only the HTTP methods and schema fields declared by
|
|
62
|
+
`CompiledRoute`, plus compiler-emitted parameter transformation/default and
|
|
63
|
+
descriptive metadata. This metadata does not install native Elysia hooks.
|
|
64
|
+
Other descriptor fields (including browser guards/resolvers and native
|
|
65
|
+
`beforeHandle`) or unsupported methods throw `ROUTE_DESCRIPTOR_UNSUPPORTED`
|
|
66
|
+
at registration. It is not an arbitrary Elysia route-options passthrough.
|
|
67
|
+
TypeScript/decorator inference, compiler migrations and generated client parity
|
|
68
|
+
require their own acceptance gates.
|
|
69
|
+
|
|
70
|
+
### Runtime and Upgrade Gates
|
|
71
|
+
|
|
72
|
+
`bun run test:runtime-safety` requires `SUPACLOUD_COMMAND_TEST_URL` and fails
|
|
73
|
+
instead of skipping when it is missing. Use a dedicated loopback database named
|
|
74
|
+
`supacloud_commands_test`, with PostgreSQL 18 and PGMQ 1.10.0; initialize it using
|
|
75
|
+
`scripts/prepare-command-test-database.ts`. The gate opens real loopback HTTP
|
|
76
|
+
listeners and exercises compiler-generated request-scoped controllers. It proves
|
|
77
|
+
overlapping tenant/actor requests, duplicate-key concurrency, authorization
|
|
78
|
+
revocation, audit rollback, same-key retry and per-request provider teardown.
|
|
79
|
+
Its authentication uses a fixed test token map, not a production JWT provider.
|
|
80
|
+
|
|
81
|
+
`bun run test:contract-upgrade` copies a fixed legacy source fixture, previews and
|
|
82
|
+
applies its versioned migration, compiles factories/client/OpenAPI, checks positive
|
|
83
|
+
and negative types with both TypeScript engines, and calls the generated client
|
|
84
|
+
over real HTTP. It restores the old source checkpoint, regenerates artifacts and
|
|
85
|
+
executes the restored application. Build local contracts, app and compiler
|
|
86
|
+
(including declarations) before installing this package's copied file dependencies.
|
|
87
|
+
|
|
88
|
+
These gates prove the stated scenarios, not a full historical npm upgrade matrix
|
|
89
|
+
or all business-domain isolation. See [framework acceptance](../../docs/framework-acceptance.md)
|
|
90
|
+
for the evidence boundaries and upgrade policy.
|
|
91
|
+
|
|
3
92
|
## Persistent Command Adapters
|
|
4
93
|
|
|
5
94
|
`createPersistentCommandAdapter(command, { identity, input })` binds a
|
|
@@ -36,9 +125,9 @@ Runtime adapter that turns `@supacloud/compiler` output into a production-ready
|
|
|
36
125
|
controllers and services.
|
|
37
126
|
- **Request-scope teardown**: invokes the compiler-generated
|
|
38
127
|
`destroyRequestScope` after the response, including when the handler fails.
|
|
39
|
-
- **
|
|
40
|
-
|
|
41
|
-
|
|
128
|
+
- **Compile-time DI**: constructor dependencies are directly connected by generated
|
|
129
|
+
factories. Request/job context is passed explicitly without runtime injector
|
|
130
|
+
discovery, registration or token lookup.
|
|
42
131
|
- **TypeBox schema binding**: attaches compiled parameter, query, body, headers,
|
|
43
132
|
cookie, single-response and status-map TypeBox schemas directly to Elysia
|
|
44
133
|
route definitions; Elysia performs request validation and normalization.
|
|
@@ -116,10 +205,11 @@ GraphQL endpoint serves a local, role-scoped snapshot only; it does not enable
|
|
|
116
205
|
server introspection or create a GraphQL resolver layer. Protect or omit these
|
|
117
206
|
routes in production when the schema is not public.
|
|
118
207
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
and
|
|
208
|
+
Use constructor injection and the generated `createServices`,
|
|
209
|
+
`createRequestScope` and `createJobScope` factories. Compiled applications reject
|
|
210
|
+
property `inject()` and runtime injection contexts with `SC2012`. The host provides
|
|
211
|
+
platform dependencies through `deps` and owns their lifecycle; generated factories
|
|
212
|
+
own application scope construction and teardown.
|
|
123
213
|
|
|
124
214
|
For deterministic local verification, use the in-memory sandbox. It supplies
|
|
125
215
|
stable request identity, an isolated key-value database with optimistic
|
package/dist/command-errors.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface CompiledRoute {
|
|
|
33
33
|
title?: string;
|
|
34
34
|
data?: Record<string, unknown>;
|
|
35
35
|
aspects?: CompiledAspect[];
|
|
36
|
+
aspectPipeline?: CompiledAspectPipeline;
|
|
36
37
|
invoker?: (controller: unknown, request: {
|
|
37
38
|
params?: Record<string, unknown>;
|
|
38
39
|
query?: Record<string, unknown>;
|
|
@@ -52,6 +53,7 @@ export interface CompiledCommand {
|
|
|
52
53
|
idempotency: "required" | "none";
|
|
53
54
|
standalone?: boolean;
|
|
54
55
|
aspects?: CompiledAspect[];
|
|
56
|
+
aspectPipeline?: CompiledAspectPipeline;
|
|
55
57
|
}
|
|
56
58
|
export interface CompiledJob {
|
|
57
59
|
className: string;
|
|
@@ -65,6 +67,7 @@ export interface CompiledJob {
|
|
|
65
67
|
maxAttempts?: number;
|
|
66
68
|
idempotency?: "required" | "none";
|
|
67
69
|
aspects?: CompiledAspect[];
|
|
70
|
+
aspectPipeline?: CompiledAspectPipeline;
|
|
68
71
|
}
|
|
69
72
|
export interface CompiledAspectContext {
|
|
70
73
|
kind: "route" | "command" | "job";
|
|
@@ -94,7 +97,11 @@ export interface CompiledModule {
|
|
|
94
97
|
commands: CompiledCommand[];
|
|
95
98
|
jobs: CompiledJob[];
|
|
96
99
|
aspects?: CompiledAspect[];
|
|
100
|
+
aspectPipeline?: CompiledAspectPipeline;
|
|
97
101
|
}
|
|
102
|
+
type CompiledAspectObserver = (stage: string, run: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
|
|
103
|
+
type CompiledAspectPipeline = (context: CompiledAspectContext, next: () => unknown | Promise<unknown>, observe?: CompiledAspectObserver) => unknown | Promise<unknown>;
|
|
98
104
|
export declare function createCompiledModules(): CompiledModule[];
|
|
99
105
|
export declare function initializeApplication(services: Record<string, unknown>): Promise<void>;
|
|
100
106
|
export declare function destroyApplication(services: Record<string, unknown>): Promise<void>;
|
|
107
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Elysia } from "elysia";
|
|
2
2
|
import { type CommandPreview } from "@supacloud/contracts";
|
|
3
|
-
import { type EnvironmentInjector } from "@supacloud/app";
|
|
4
3
|
import { type ExecutionObserver } from "./execution";
|
|
5
4
|
import { type ApplicationDocumentationOptions } from "./documentation";
|
|
6
5
|
export type { ExecutionEvent, ExecutionObserver } from "./execution";
|
|
@@ -46,6 +45,7 @@ export interface CompiledRoute {
|
|
|
46
45
|
command?: string;
|
|
47
46
|
/** Statically generated route aspects. */
|
|
48
47
|
aspects?: ApplicationAspect[];
|
|
48
|
+
aspectPipeline?: ApplicationAspectPipeline;
|
|
49
49
|
}
|
|
50
50
|
export interface CompiledCommand {
|
|
51
51
|
rpc?: string;
|
|
@@ -57,6 +57,7 @@ export interface CompiledCommand {
|
|
|
57
57
|
idempotency?: "required" | "none" | string;
|
|
58
58
|
/** Statically generated command aspects. */
|
|
59
59
|
aspects?: ApplicationAspect[];
|
|
60
|
+
aspectPipeline?: ApplicationAspectPipeline;
|
|
60
61
|
}
|
|
61
62
|
export interface CompiledJob {
|
|
62
63
|
className: string;
|
|
@@ -72,6 +73,7 @@ export interface CompiledJob {
|
|
|
72
73
|
maxAttempts?: number;
|
|
73
74
|
idempotency?: "required" | "none";
|
|
74
75
|
aspects?: ApplicationAspect[];
|
|
76
|
+
aspectPipeline?: ApplicationAspectPipeline;
|
|
75
77
|
}
|
|
76
78
|
export interface CompiledController {
|
|
77
79
|
/** Controller path prefix, e.g. "/cases". */
|
|
@@ -93,6 +95,7 @@ export interface CompiledModule {
|
|
|
93
95
|
jobs?: CompiledJob[];
|
|
94
96
|
/** Statically generated module aspects. */
|
|
95
97
|
aspects?: ApplicationAspect[];
|
|
98
|
+
aspectPipeline?: ApplicationAspectPipeline;
|
|
96
99
|
}
|
|
97
100
|
export type RequestContextFactory = (request: Request) => unknown | Promise<unknown>;
|
|
98
101
|
export { createSupAuthRequestContext } from "./identity";
|
|
@@ -145,6 +148,7 @@ export type ApplicationAspect = (context: ApplicationAspectContext, next: () =>
|
|
|
145
148
|
* registers aspects.
|
|
146
149
|
*/
|
|
147
150
|
export declare function composeAspects(...aspects: (ApplicationAspect | undefined | null)[]): ApplicationAspect;
|
|
151
|
+
export type ApplicationAspectPipeline = (context: ApplicationAspectContext, next: () => unknown | Promise<unknown>, observe?: (stage: string, run: () => unknown | Promise<unknown>) => unknown | Promise<unknown>) => unknown | Promise<unknown>;
|
|
148
152
|
export type CommandAuthorizer = (invocation: CommandInvocation) => void | Promise<void>;
|
|
149
153
|
export type CommandMiddleware = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
|
|
150
154
|
export interface CommandAudit {
|
|
@@ -211,8 +215,6 @@ export interface ApplicationOptions {
|
|
|
211
215
|
modules?: CompiledModule[];
|
|
212
216
|
/** Platform-level dependencies (db client etc.), passed to createServices. */
|
|
213
217
|
deps?: Record<string, unknown>;
|
|
214
|
-
/** Optional Angular-backed root injector used for async request contexts. */
|
|
215
|
-
injector?: EnvironmentInjector;
|
|
216
218
|
/** Builds the per-request context object. Defaults to { requestId, request }. */
|
|
217
219
|
requestContext?: RequestContextFactory;
|
|
218
220
|
/** Enforces permission/audit/idempotency policy for command-bound routes. */
|
|
@@ -248,7 +250,7 @@ export declare const createSupaCloudRequestContext: (request: Request) => SupaCl
|
|
|
248
250
|
export declare function createCommandExecutor(governance: CommandGovernance, observer?: ExecutionObserver): CommandExecutor;
|
|
249
251
|
/** Execute at the business-selected boundary without binding or re-entering an HTTP route. */
|
|
250
252
|
export declare function executeCompiledCommand<Input, Result>(options: {
|
|
251
|
-
module: Pick<CompiledModule, "name" | "commands" | "aspects">;
|
|
253
|
+
module: Pick<CompiledModule, "name" | "commands" | "aspects" | "aspectPipeline">;
|
|
252
254
|
command: string;
|
|
253
255
|
input: Input;
|
|
254
256
|
request: Request;
|
|
@@ -282,7 +284,7 @@ export declare function requireIdempotencyKey(invocation: CommandInvocation): st
|
|
|
282
284
|
* exposed on the `scope` context key. Request-scoped controller instances are
|
|
283
285
|
* looked up on `scope`, everything else on `services`.
|
|
284
286
|
*/
|
|
285
|
-
export declare function createModulePlugin(compiled: CompiledModule, services: Record<string, unknown>, ctxFactory?: RequestContextFactory, options?: Pick<ApplicationOptions, "commandGovernance" | "commandExecutor" | "errorMapper" | "onExecution" | "normalize"
|
|
287
|
+
export declare function createModulePlugin(compiled: CompiledModule, services: Record<string, unknown>, ctxFactory?: RequestContextFactory, options?: Pick<ApplicationOptions, "commandGovernance" | "commandExecutor" | "errorMapper" | "onExecution" | "normalize">, imported?: Record<string, Record<string, unknown>>): Elysia;
|
|
286
288
|
/**
|
|
287
289
|
* Execute one compiler-emitted Job descriptor.
|
|
288
290
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Elysia as Elysia2 } from "elysia";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
provideToken,
|
|
6
|
-
runInRequestContext
|
|
7
|
-
} from "@supacloud/app";
|
|
8
|
-
import { REQUEST_CONTEXT } from "@supacloud/app";
|
|
3
|
+
import { decodeCommandPreview } from "@supacloud/contracts";
|
|
9
4
|
|
|
10
5
|
// src/command-errors.ts
|
|
6
|
+
function commandErrorCode(error) {
|
|
7
|
+
if (!(error instanceof Error) || error.name !== "CommandError" || !("code" in error))
|
|
8
|
+
return;
|
|
9
|
+
switch (error.code) {
|
|
10
|
+
case "COMMAND_INPUT_INVALID":
|
|
11
|
+
case "COMMAND_REJECTED":
|
|
12
|
+
case "COMMAND_IDEMPOTENCY_CONFLICT":
|
|
13
|
+
case "COMMAND_INPUT_EXPIRED":
|
|
14
|
+
case "COMMAND_UNAVAILABLE":
|
|
15
|
+
case "COMMAND_RECEIPT_INVALID":
|
|
16
|
+
case "COMMAND_OUTCOME_UNKNOWN":
|
|
17
|
+
return error.code;
|
|
18
|
+
default:
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
11
22
|
function commandErrorStatus(code) {
|
|
12
23
|
switch (code) {
|
|
13
24
|
case "COMMAND_INPUT_INVALID":
|
|
@@ -413,7 +424,7 @@ function createPersistentCommandAdapter(command, options) {
|
|
|
413
424
|
async execute(invocation) {
|
|
414
425
|
try {
|
|
415
426
|
const identity = decodeCommandIdentity(await options.identity(invocation));
|
|
416
|
-
return await command.execute(identity, requireIdempotencyKey(invocation), options.input(invocation));
|
|
427
|
+
return await command.execute(identity, requireIdempotencyKey(invocation), options.input(invocation), invocation.request.signal);
|
|
417
428
|
} catch (error) {
|
|
418
429
|
if (!(error instanceof CommandError))
|
|
419
430
|
throw error;
|
|
@@ -1269,6 +1280,17 @@ function observedAspects(aspects, boundary, observer) {
|
|
|
1269
1280
|
...executionTrace(context.requestContext)
|
|
1270
1281
|
}, () => aspect(context, next))));
|
|
1271
1282
|
}
|
|
1283
|
+
function descriptorPipeline(descriptor, boundary, observer) {
|
|
1284
|
+
const pipeline = descriptor.aspectPipeline;
|
|
1285
|
+
if (!pipeline)
|
|
1286
|
+
return observedAspects(descriptor.aspects ?? [], boundary, observer);
|
|
1287
|
+
return (context, next) => pipeline(context, next, observer ? (stage, run) => observeExecution(observer, {
|
|
1288
|
+
kind: context.kind,
|
|
1289
|
+
operation: context.name,
|
|
1290
|
+
stage: `${boundary}.${stage}`,
|
|
1291
|
+
...executionTrace(context.requestContext)
|
|
1292
|
+
}, run) : undefined);
|
|
1293
|
+
}
|
|
1272
1294
|
function composeCommandExecutors(...executors) {
|
|
1273
1295
|
const active = executors.filter((e) => typeof e === "function");
|
|
1274
1296
|
if (active.length === 0)
|
|
@@ -1483,7 +1505,7 @@ async function executeCompiledCommand(options) {
|
|
|
1483
1505
|
services: options.services ?? {},
|
|
1484
1506
|
...options.scope ? { scope: options.scope } : {}
|
|
1485
1507
|
};
|
|
1486
|
-
const aspects = composeAspects(
|
|
1508
|
+
const aspects = composeAspects(descriptorPipeline(options.module, `module:${options.module.name}`, options.observer), descriptorPipeline(command, "command", options.observer));
|
|
1487
1509
|
const value = await createCommandExecutor(options.governance, options.observer)(invocation, once(() => aspects({
|
|
1488
1510
|
kind: "command",
|
|
1489
1511
|
name: command.name,
|
|
@@ -1591,6 +1613,43 @@ function assertDeclaredResponseStatus(route, value, configuredStatus) {
|
|
|
1591
1613
|
throw new ApplicationError("Response validation failed", { status: 500, code: "RESPONSE_CONTRACT_UNDECLARED" });
|
|
1592
1614
|
}
|
|
1593
1615
|
function createModulePlugin(compiled, services, ctxFactory = defaultRequestContext, options = {}, imported = {}) {
|
|
1616
|
+
const supportedMethods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
1617
|
+
const supportedFields = new Set([
|
|
1618
|
+
"method",
|
|
1619
|
+
"path",
|
|
1620
|
+
"handler",
|
|
1621
|
+
"body",
|
|
1622
|
+
"params",
|
|
1623
|
+
"query",
|
|
1624
|
+
"headers",
|
|
1625
|
+
"cookie",
|
|
1626
|
+
"response",
|
|
1627
|
+
"responses",
|
|
1628
|
+
"contract",
|
|
1629
|
+
"schemaKinds",
|
|
1630
|
+
"nativeResponse",
|
|
1631
|
+
"invoker",
|
|
1632
|
+
"command",
|
|
1633
|
+
"aspects",
|
|
1634
|
+
"aspectPipeline",
|
|
1635
|
+
"paramTransforms",
|
|
1636
|
+
"paramDefaults",
|
|
1637
|
+
"queryTransforms",
|
|
1638
|
+
"queryDefaults",
|
|
1639
|
+
"title",
|
|
1640
|
+
"data",
|
|
1641
|
+
"input",
|
|
1642
|
+
"result",
|
|
1643
|
+
"request"
|
|
1644
|
+
]);
|
|
1645
|
+
for (const controller of compiled.controllers) {
|
|
1646
|
+
for (const route of controller.routes) {
|
|
1647
|
+
const unsupported = Object.keys(route).filter((field) => !supportedFields.has(field));
|
|
1648
|
+
if (!supportedMethods.has(route.method) || unsupported.length > 0) {
|
|
1649
|
+
throw new ApplicationError(`Unsupported compiled route ${route.method} ${controller.path}${route.path}` + (unsupported.length > 0 ? `: ${unsupported.join(", ")}` : ""), { code: "ROUTE_DESCRIPTOR_UNSUPPORTED" });
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1594
1653
|
const hasCommandRoutes = compiled.controllers.some((controller) => controller.routes.some((route) => route.command !== undefined));
|
|
1595
1654
|
if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
|
|
1596
1655
|
throw new ApplicationError(`Module "${compiled.name}" has command routes but no commandGovernance`, { code: "COMMAND_GOVERNANCE_UNCONFIGURED" });
|
|
@@ -1695,14 +1754,13 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1695
1754
|
services,
|
|
1696
1755
|
metadata: command ?? route
|
|
1697
1756
|
};
|
|
1698
|
-
const
|
|
1699
|
-
const
|
|
1700
|
-
const
|
|
1701
|
-
const
|
|
1702
|
-
const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
|
|
1757
|
+
const routePipeline = descriptorPipeline(route, "route", options.onExecution);
|
|
1758
|
+
const commandPipeline = descriptorPipeline(command ?? {}, "command", options.onExecution);
|
|
1759
|
+
const modulePipeline = descriptorPipeline(compiled, `module:${compiled.name}`, options.onExecution);
|
|
1760
|
+
const invokeRoute = () => modulePipeline(route.command ? commandContext : routeContext, once(() => routePipeline(routeContext, once(() => route.command ? commandPipeline(commandContext, once(invoke)) : invoke()))));
|
|
1703
1761
|
const invokeCommand = () => {
|
|
1704
1762
|
if (!route.command)
|
|
1705
|
-
return
|
|
1763
|
+
return invokeRoute();
|
|
1706
1764
|
if (!command) {
|
|
1707
1765
|
throw new ApplicationError(`Command "${route.command}" is not registered`, {
|
|
1708
1766
|
code: "COMMAND_NOT_REGISTERED"
|
|
@@ -1727,11 +1785,11 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1727
1785
|
operation: command.name,
|
|
1728
1786
|
stage: "commandExecutor",
|
|
1729
1787
|
...executionTrace(requestContext)
|
|
1730
|
-
}, () => commandExecutor(invocation,
|
|
1788
|
+
}, () => commandExecutor(invocation, once(invokeRoute)));
|
|
1731
1789
|
};
|
|
1732
|
-
return
|
|
1790
|
+
return invokeCommand();
|
|
1733
1791
|
};
|
|
1734
|
-
const result =
|
|
1792
|
+
const result = await execute();
|
|
1735
1793
|
assertDeclaredResponseStatus(route, result, ctx.set.status);
|
|
1736
1794
|
return result;
|
|
1737
1795
|
};
|
|
@@ -1794,8 +1852,8 @@ async function executeJob(compiled, services, job, input, requestContext, import
|
|
|
1794
1852
|
stage: "handler",
|
|
1795
1853
|
...executionTrace(requestContext)
|
|
1796
1854
|
}, () => Reflect.apply(method, instance, [decodedInput])));
|
|
1797
|
-
const pipeline =
|
|
1798
|
-
const jobPipeline =
|
|
1855
|
+
const pipeline = descriptorPipeline(compiled, `module:${compiled.name}`, observer);
|
|
1856
|
+
const jobPipeline = descriptorPipeline(job, "job", observer);
|
|
1799
1857
|
const result = await pipeline(context, () => jobPipeline(context, () => observeExecution(observer, {
|
|
1800
1858
|
kind: "job",
|
|
1801
1859
|
operation: job.name,
|
|
@@ -1810,9 +1868,10 @@ async function executeJob(compiled, services, job, input, requestContext, import
|
|
|
1810
1868
|
}
|
|
1811
1869
|
}
|
|
1812
1870
|
function defaultErrorResponse(error, frameworkCode) {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1871
|
+
const protocolCode = commandErrorCode(error);
|
|
1872
|
+
if (protocolCode) {
|
|
1873
|
+
return Response.json({ ok: false, code: protocolCode, message: protocolCode }, {
|
|
1874
|
+
status: commandErrorStatus(protocolCode)
|
|
1816
1875
|
});
|
|
1817
1876
|
}
|
|
1818
1877
|
if (isPublicApplicationError(error)) {
|
|
@@ -1823,6 +1882,13 @@ function defaultErrorResponse(error, frameworkCode) {
|
|
|
1823
1882
|
...error.details === undefined ? {} : { details: error.details }
|
|
1824
1883
|
}, { status: error.status });
|
|
1825
1884
|
}
|
|
1885
|
+
if (frameworkCode === "PARSE") {
|
|
1886
|
+
return Response.json({
|
|
1887
|
+
ok: false,
|
|
1888
|
+
code: "PARSE_ERROR",
|
|
1889
|
+
message: "Request body could not be parsed"
|
|
1890
|
+
}, { status: 400 });
|
|
1891
|
+
}
|
|
1826
1892
|
if (frameworkCode === "VALIDATION") {
|
|
1827
1893
|
if (isRecord(error) && error.type === "response") {
|
|
1828
1894
|
return Response.json({
|
|
@@ -2,7 +2,7 @@ import { type CommandIdentity, type DurableCommandReceipt } from "@supacloud/con
|
|
|
2
2
|
import { type CommandInvocation } from "./index";
|
|
3
3
|
export interface PersistentCommandHandler<Result> {
|
|
4
4
|
readonly kind: "transactional" | "external";
|
|
5
|
-
execute(identity: CommandIdentity, key: string, value: unknown): Promise<DurableCommandReceipt<Result>>;
|
|
5
|
+
execute(identity: CommandIdentity, key: string, value: unknown, signal?: AbortSignal): Promise<DurableCommandReceipt<Result>>;
|
|
6
6
|
}
|
|
7
7
|
/** Registered persistence owns the operation; a second route handler is never invoked. */
|
|
8
8
|
export declare function createPersistentCommandAdapter<Result>(command: PersistentCommandHandler<Result>, options: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/elysia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Elysia runtime adapter for SupaCloud compiled modules: application/request scopes, route registration and validation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"compatibility.json"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
22
|
"generate:example": "bun src/generate-webhook-example.ts",
|
|
@@ -25,6 +26,9 @@
|
|
|
25
26
|
"clean": "rm -rf dist",
|
|
26
27
|
"prepublishOnly": "bun run build",
|
|
27
28
|
"test": "bun test",
|
|
29
|
+
"test:conformance": "bun test src/conformance.test.ts",
|
|
30
|
+
"test:runtime-safety": "SUPACLOUD_REQUIRE_RUNTIME_SAFETY=1 bun test src/runtime-safety.test.ts",
|
|
31
|
+
"test:contract-upgrade": "bun test src/contract-upgrade.test.ts",
|
|
28
32
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
33
|
"typecheck:test": "tsc -p tsconfig.test.json --noEmit"
|
|
30
34
|
},
|
|
@@ -48,15 +52,15 @@
|
|
|
48
52
|
},
|
|
49
53
|
"overrides": {
|
|
50
54
|
"@supacloud/contracts": "0.4.0",
|
|
51
|
-
"@supacloud/commands": "0.
|
|
55
|
+
"@supacloud/commands": "0.5.0"
|
|
52
56
|
},
|
|
53
57
|
"peerDependencies": {
|
|
54
58
|
"elysia": "^1.4.30"
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
57
|
-
"@supacloud/compiler": "0.
|
|
58
|
-
"@supacloud/commands": "0.
|
|
59
|
-
"@supacloud/db": "0.
|
|
61
|
+
"@supacloud/compiler": "0.22.0",
|
|
62
|
+
"@supacloud/commands": "0.5.0",
|
|
63
|
+
"@supacloud/db": "0.8.0",
|
|
60
64
|
"@types/bun": "^1.4.2",
|
|
61
65
|
"elysia": "^1.4.30",
|
|
62
66
|
"typescript": "^7.0.2"
|