@supacloud/elysia 0.16.0 → 0.18.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 +8 -7
- package/dist/fixtures/webhook-generated/application.d.ts +7 -0
- package/dist/index.d.ts +50 -38
- package/dist/index.js +123 -18
- package/dist/persistent-command.d.ts +1 -1
- package/dist/webhook-migration-example.d.ts +10 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -125,9 +125,9 @@ Runtime adapter that turns `@supacloud/compiler` output into a production-ready
|
|
|
125
125
|
controllers and services.
|
|
126
126
|
- **Request-scope teardown**: invokes the compiler-generated
|
|
127
127
|
`destroyRequestScope` after the response, including when the handler fails.
|
|
128
|
-
- **
|
|
129
|
-
|
|
130
|
-
|
|
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.
|
|
131
131
|
- **TypeBox schema binding**: attaches compiled parameter, query, body, headers,
|
|
132
132
|
cookie, single-response and status-map TypeBox schemas directly to Elysia
|
|
133
133
|
route definitions; Elysia performs request validation and normalization.
|
|
@@ -205,10 +205,11 @@ GraphQL endpoint serves a local, role-scoped snapshot only; it does not enable
|
|
|
205
205
|
server introspection or create a GraphQL resolver layer. Protect or omit these
|
|
206
206
|
routes in production when the schema is not public.
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
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.
|
|
212
213
|
|
|
213
214
|
For deterministic local verification, use the in-memory sandbox. It supplies
|
|
214
215
|
stable request identity, an isolated key-value database with optimistic
|
|
@@ -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,6 @@
|
|
|
1
1
|
import { Elysia } from "elysia";
|
|
2
|
+
import type { CommandRuntimeAudit, CommandRuntimeAuthorizer, CommandRuntimeGovernance, CommandRuntimeInvocation, CommandRuntimeMiddleware } from "@supacloud/app";
|
|
2
3
|
import { type CommandPreview } from "@supacloud/contracts";
|
|
3
|
-
import { type EnvironmentInjector } from "@supacloud/app";
|
|
4
4
|
import { type ExecutionObserver } from "./execution";
|
|
5
5
|
import { type ApplicationDocumentationOptions } from "./documentation";
|
|
6
6
|
export type { ExecutionEvent, ExecutionObserver } from "./execution";
|
|
@@ -46,6 +46,7 @@ export interface CompiledRoute {
|
|
|
46
46
|
command?: string;
|
|
47
47
|
/** Statically generated route aspects. */
|
|
48
48
|
aspects?: ApplicationAspect[];
|
|
49
|
+
aspectPipeline?: ApplicationAspectPipeline;
|
|
49
50
|
}
|
|
50
51
|
export interface CompiledCommand {
|
|
51
52
|
rpc?: string;
|
|
@@ -57,6 +58,7 @@ export interface CompiledCommand {
|
|
|
57
58
|
idempotency?: "required" | "none" | string;
|
|
58
59
|
/** Statically generated command aspects. */
|
|
59
60
|
aspects?: ApplicationAspect[];
|
|
61
|
+
aspectPipeline?: ApplicationAspectPipeline;
|
|
60
62
|
}
|
|
61
63
|
export interface CompiledJob {
|
|
62
64
|
className: string;
|
|
@@ -72,6 +74,7 @@ export interface CompiledJob {
|
|
|
72
74
|
maxAttempts?: number;
|
|
73
75
|
idempotency?: "required" | "none";
|
|
74
76
|
aspects?: ApplicationAspect[];
|
|
77
|
+
aspectPipeline?: ApplicationAspectPipeline;
|
|
75
78
|
}
|
|
76
79
|
export interface CompiledController {
|
|
77
80
|
/** Controller path prefix, e.g. "/cases". */
|
|
@@ -93,6 +96,7 @@ export interface CompiledModule {
|
|
|
93
96
|
jobs?: CompiledJob[];
|
|
94
97
|
/** Statically generated module aspects. */
|
|
95
98
|
aspects?: ApplicationAspect[];
|
|
99
|
+
aspectPipeline?: ApplicationAspectPipeline;
|
|
96
100
|
}
|
|
97
101
|
export type RequestContextFactory = (request: Request) => unknown | Promise<unknown>;
|
|
98
102
|
export { createSupAuthRequestContext } from "./identity";
|
|
@@ -113,19 +117,8 @@ export interface SupaCloudRequestContext {
|
|
|
113
117
|
identity: TrustedRequestIdentity;
|
|
114
118
|
idempotencyKey?: string;
|
|
115
119
|
}
|
|
116
|
-
export
|
|
117
|
-
|
|
118
|
-
input: {
|
|
119
|
-
body: unknown;
|
|
120
|
-
params: Record<string, unknown>;
|
|
121
|
-
query: Record<string, unknown>;
|
|
122
|
-
};
|
|
123
|
-
request: Request;
|
|
124
|
-
requestContext: unknown;
|
|
125
|
-
scope?: Record<string, unknown>;
|
|
126
|
-
services: Record<string, unknown>;
|
|
127
|
-
}
|
|
128
|
-
export type CommandExecutor = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
|
|
120
|
+
export type CommandInvocation = CommandRuntimeInvocation<CompiledCommand>;
|
|
121
|
+
export type CommandExecutor = CommandRuntimeMiddleware<CommandInvocation>;
|
|
129
122
|
export { assertFeatureTransition } from "./feature";
|
|
130
123
|
export type { FeatureTransitionSpec } from "./feature";
|
|
131
124
|
export interface ApplicationAspectContext {
|
|
@@ -145,28 +138,49 @@ export type ApplicationAspect = (context: ApplicationAspectContext, next: () =>
|
|
|
145
138
|
* registers aspects.
|
|
146
139
|
*/
|
|
147
140
|
export declare function composeAspects(...aspects: (ApplicationAspect | undefined | null)[]): ApplicationAspect;
|
|
148
|
-
export type
|
|
149
|
-
export type
|
|
150
|
-
export
|
|
151
|
-
|
|
152
|
-
|
|
141
|
+
export type ApplicationAspectPipeline = (context: ApplicationAspectContext, next: () => unknown | Promise<unknown>, observe?: (stage: string, run: () => unknown | Promise<unknown>) => unknown | Promise<unknown>) => unknown | Promise<unknown>;
|
|
142
|
+
export type CommandAuthorizer = CommandRuntimeAuthorizer<CommandInvocation>;
|
|
143
|
+
export type CommandMiddleware = CommandRuntimeMiddleware<CommandInvocation>;
|
|
144
|
+
export type CommandAudit = CommandRuntimeAudit<CommandInvocation>;
|
|
145
|
+
export type CommandGovernance = CommandRuntimeGovernance<CommandInvocation>;
|
|
146
|
+
export interface CommandAuthorizationRequest {
|
|
147
|
+
readonly principal: {
|
|
148
|
+
readonly kind: "user" | "service";
|
|
149
|
+
readonly issuer: string;
|
|
150
|
+
readonly subject: string;
|
|
151
|
+
};
|
|
152
|
+
readonly applicationId: string;
|
|
153
|
+
readonly domain: {
|
|
154
|
+
readonly type: string;
|
|
155
|
+
readonly id: string;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export interface CommandAuthorizationContext {
|
|
159
|
+
readonly applicationId: string;
|
|
160
|
+
readonly permissions: readonly string[];
|
|
161
|
+
readonly permissionCatalogVersion?: string;
|
|
162
|
+
readonly permissionCatalogDigest?: string;
|
|
163
|
+
}
|
|
164
|
+
export interface CommandAuthorizationCatalog {
|
|
165
|
+
readonly version: string;
|
|
166
|
+
readonly digest?: string;
|
|
153
167
|
}
|
|
154
|
-
export interface
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
execute: CommandMiddleware;
|
|
164
|
-
}>;
|
|
165
|
-
authorize: CommandAuthorizer;
|
|
166
|
-
idempotency?: CommandMiddleware;
|
|
167
|
-
transaction?: CommandMiddleware;
|
|
168
|
-
audit?: CommandAudit;
|
|
168
|
+
export interface CommandAuthorizationAdapterOptions {
|
|
169
|
+
readonly applicationId: string;
|
|
170
|
+
readonly issuer: string;
|
|
171
|
+
readonly domain: (invocation: CommandInvocation) => {
|
|
172
|
+
readonly type: string;
|
|
173
|
+
readonly id: string;
|
|
174
|
+
};
|
|
175
|
+
readonly resolve: (request: CommandAuthorizationRequest) => Promise<CommandAuthorizationContext>;
|
|
176
|
+
readonly catalog?: CommandAuthorizationCatalog;
|
|
169
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Adapt a SupAuth-style resolver to the standard command governance port.
|
|
180
|
+
* The resolver remains the authorization source of truth; this adapter only
|
|
181
|
+
* maps trusted request identity and normalizes public failure statuses.
|
|
182
|
+
*/
|
|
183
|
+
export declare function createCommandAuthorizationAdapter(options: CommandAuthorizationAdapterOptions): CommandAuthorizer;
|
|
170
184
|
/**
|
|
171
185
|
* Compose multiple CommandExecutors into a single onion-style pipeline.
|
|
172
186
|
* Outer executors run first before calling `next()`, and complete last.
|
|
@@ -211,8 +225,6 @@ export interface ApplicationOptions {
|
|
|
211
225
|
modules?: CompiledModule[];
|
|
212
226
|
/** Platform-level dependencies (db client etc.), passed to createServices. */
|
|
213
227
|
deps?: Record<string, unknown>;
|
|
214
|
-
/** Optional Angular-backed root injector used for async request contexts. */
|
|
215
|
-
injector?: EnvironmentInjector;
|
|
216
228
|
/** Builds the per-request context object. Defaults to { requestId, request }. */
|
|
217
229
|
requestContext?: RequestContextFactory;
|
|
218
230
|
/** Enforces permission/audit/idempotency policy for command-bound routes. */
|
|
@@ -248,7 +260,7 @@ export declare const createSupaCloudRequestContext: (request: Request) => SupaCl
|
|
|
248
260
|
export declare function createCommandExecutor(governance: CommandGovernance, observer?: ExecutionObserver): CommandExecutor;
|
|
249
261
|
/** Execute at the business-selected boundary without binding or re-entering an HTTP route. */
|
|
250
262
|
export declare function executeCompiledCommand<Input, Result>(options: {
|
|
251
|
-
module: Pick<CompiledModule, "name" | "commands" | "aspects">;
|
|
263
|
+
module: Pick<CompiledModule, "name" | "commands" | "aspects" | "aspectPipeline">;
|
|
252
264
|
command: string;
|
|
253
265
|
input: Input;
|
|
254
266
|
request: Request;
|
|
@@ -282,7 +294,7 @@ export declare function requireIdempotencyKey(invocation: CommandInvocation): st
|
|
|
282
294
|
* exposed on the `scope` context key. Request-scoped controller instances are
|
|
283
295
|
* looked up on `scope`, everything else on `services`.
|
|
284
296
|
*/
|
|
285
|
-
export declare function createModulePlugin(compiled: CompiledModule, services: Record<string, unknown>, ctxFactory?: RequestContextFactory, options?: Pick<ApplicationOptions, "commandGovernance" | "commandExecutor" | "errorMapper" | "onExecution" | "normalize"
|
|
297
|
+
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
298
|
/**
|
|
287
299
|
* Execute one compiler-emitted Job descriptor.
|
|
288
300
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Elysia as Elysia2 } from "elysia";
|
|
3
3
|
import { decodeCommandPreview } from "@supacloud/contracts";
|
|
4
|
-
import {
|
|
5
|
-
provideToken,
|
|
6
|
-
runInRequestContext
|
|
7
|
-
} from "@supacloud/app";
|
|
8
|
-
import { REQUEST_CONTEXT } from "@supacloud/app";
|
|
9
4
|
|
|
10
5
|
// src/command-errors.ts
|
|
11
6
|
function commandErrorCode(error) {
|
|
@@ -429,7 +424,7 @@ function createPersistentCommandAdapter(command, options) {
|
|
|
429
424
|
async execute(invocation) {
|
|
430
425
|
try {
|
|
431
426
|
const identity = decodeCommandIdentity(await options.identity(invocation));
|
|
432
|
-
return await command.execute(identity, requireIdempotencyKey(invocation), options.input(invocation));
|
|
427
|
+
return await command.execute(identity, requireIdempotencyKey(invocation), options.input(invocation), invocation.request.signal);
|
|
433
428
|
} catch (error) {
|
|
434
429
|
if (!(error instanceof CommandError))
|
|
435
430
|
throw error;
|
|
@@ -1285,6 +1280,53 @@ function observedAspects(aspects, boundary, observer) {
|
|
|
1285
1280
|
...executionTrace(context.requestContext)
|
|
1286
1281
|
}, () => aspect(context, next))));
|
|
1287
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
|
+
}
|
|
1294
|
+
function createCommandAuthorizationAdapter(options) {
|
|
1295
|
+
return async (invocation) => {
|
|
1296
|
+
const permission = invocation.command.permission;
|
|
1297
|
+
if (!permission || !/^[a-z][a-z0-9._-]*:[a-z][a-z0-9._-]*$/.test(permission)) {
|
|
1298
|
+
throw new ApplicationError("Command permission must use resource:action syntax", {
|
|
1299
|
+
status: 500,
|
|
1300
|
+
code: "COMMAND_PERMISSION_INVALID"
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
const context = invocation.requestContext;
|
|
1304
|
+
const identity = isRecord(context) && isTrustedIdentity(context.identity) ? context.identity : undefined;
|
|
1305
|
+
if (!isAuthenticatedTrustedIdentity(identity)) {
|
|
1306
|
+
throw new ApplicationError("Authentication required", { status: 401, code: "AUTHENTICATION_REQUIRED" });
|
|
1307
|
+
}
|
|
1308
|
+
const request = {
|
|
1309
|
+
principal: { kind: "user", issuer: options.issuer, subject: identity.subject },
|
|
1310
|
+
applicationId: options.applicationId,
|
|
1311
|
+
domain: options.domain(invocation)
|
|
1312
|
+
};
|
|
1313
|
+
let resolved;
|
|
1314
|
+
try {
|
|
1315
|
+
resolved = await options.resolve(request);
|
|
1316
|
+
} catch {
|
|
1317
|
+
throw new ApplicationError("Authorization is unavailable", { status: 503, code: "AUTHORIZATION_UNAVAILABLE" });
|
|
1318
|
+
}
|
|
1319
|
+
if (!isRecord(resolved) || !Array.isArray(resolved.permissions) || !resolved.permissions.every((value) => typeof value === "string") || resolved.permissionCatalogVersion !== undefined && typeof resolved.permissionCatalogVersion !== "string" || resolved.permissionCatalogDigest !== undefined && typeof resolved.permissionCatalogDigest !== "string" || resolved.applicationId !== options.applicationId || options.catalog !== undefined && (resolved.permissionCatalogVersion !== options.catalog.version || options.catalog.digest !== undefined && resolved.permissionCatalogDigest !== options.catalog.digest)) {
|
|
1320
|
+
throw new ApplicationError("Authorization context is not bound to this application", {
|
|
1321
|
+
status: 503,
|
|
1322
|
+
code: "AUTHORIZATION_CONTEXT_INVALID"
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
if (!resolved.permissions.includes(permission)) {
|
|
1326
|
+
throw new ApplicationError("Permission denied", { status: 403, code: "PERMISSION_DENIED" });
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1288
1330
|
function composeCommandExecutors(...executors) {
|
|
1289
1331
|
const active = executors.filter((e) => typeof e === "function");
|
|
1290
1332
|
if (active.length === 0)
|
|
@@ -1499,7 +1541,7 @@ async function executeCompiledCommand(options) {
|
|
|
1499
1541
|
services: options.services ?? {},
|
|
1500
1542
|
...options.scope ? { scope: options.scope } : {}
|
|
1501
1543
|
};
|
|
1502
|
-
const aspects = composeAspects(
|
|
1544
|
+
const aspects = composeAspects(descriptorPipeline(options.module, `module:${options.module.name}`, options.observer), descriptorPipeline(command, "command", options.observer));
|
|
1503
1545
|
const value = await createCommandExecutor(options.governance, options.observer)(invocation, once(() => aspects({
|
|
1504
1546
|
kind: "command",
|
|
1505
1547
|
name: command.name,
|
|
@@ -1579,6 +1621,67 @@ function requireIdempotencyKey(invocation) {
|
|
|
1579
1621
|
}
|
|
1580
1622
|
return key;
|
|
1581
1623
|
}
|
|
1624
|
+
function assertCommandGovernanceReady(compiled, governance, executor) {
|
|
1625
|
+
if ((compiled.commands ?? []).length === 0)
|
|
1626
|
+
return;
|
|
1627
|
+
if (executor !== undefined && typeof executor !== "function") {
|
|
1628
|
+
throw new ApplicationError(`Module "${compiled.name}" has no callable command executor`, {
|
|
1629
|
+
code: "COMMAND_EXECUTOR_UNCONFIGURED"
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
if (!governance && !executor) {
|
|
1633
|
+
throw new ApplicationError(`Module "${compiled.name}" has commands but no command governance`, {
|
|
1634
|
+
code: "COMMAND_GOVERNANCE_UNCONFIGURED"
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
if (!governance)
|
|
1638
|
+
return;
|
|
1639
|
+
if (typeof governance.authorize !== "function") {
|
|
1640
|
+
throw new ApplicationError(`Module "${compiled.name}" has no authorization adapter`, {
|
|
1641
|
+
code: "COMMAND_AUTHORIZATION_UNCONFIGURED"
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
for (const command of compiled.commands ?? []) {
|
|
1645
|
+
if (command.audit && command.rpc === undefined && (typeof governance.audit?.succeeded !== "function" || typeof governance.audit?.failed !== "function")) {
|
|
1646
|
+
throw new ApplicationError(`Command "${command.name}" has no audit adapter`, {
|
|
1647
|
+
code: "COMMAND_AUDIT_UNCONFIGURED"
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
if (command.idempotency === "required" && typeof governance.idempotency !== "function" && command.rpc === undefined) {
|
|
1651
|
+
throw new ApplicationError(`Command "${command.name}" has no idempotency adapter`, {
|
|
1652
|
+
code: "COMMAND_IDEMPOTENCY_UNCONFIGURED"
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
if (command.transaction === "required" && typeof governance.transaction !== "function" && command.rpc === undefined) {
|
|
1656
|
+
throw new ApplicationError(`Command "${command.name}" has no transaction adapter`, {
|
|
1657
|
+
code: "COMMAND_TRANSACTION_UNCONFIGURED"
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
if (command.rpc !== undefined) {
|
|
1661
|
+
const adapter = Object.hasOwn(governance.rpc ?? {}, command.rpc) ? governance.rpc?.[command.rpc] : undefined;
|
|
1662
|
+
if (!adapter || typeof adapter.execute !== "function" || !isRecord(adapter.capabilities)) {
|
|
1663
|
+
throw new ApplicationError(`Command "${command.name}" has no RPC governance adapter`, {
|
|
1664
|
+
code: "COMMAND_RPC_UNCONFIGURED"
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
if (command.audit && adapter.capabilities.audit !== true) {
|
|
1668
|
+
throw new ApplicationError(`Command "${command.name}" RPC adapter has no audit capability`, {
|
|
1669
|
+
code: "COMMAND_AUDIT_UNCONFIGURED"
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
if (command.idempotency === "required" && adapter.capabilities.idempotency !== true) {
|
|
1673
|
+
throw new ApplicationError(`Command "${command.name}" RPC adapter has no idempotency capability`, {
|
|
1674
|
+
code: "COMMAND_IDEMPOTENCY_UNCONFIGURED"
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
if (command.transaction === "required" && adapter.capabilities.transaction !== true) {
|
|
1678
|
+
throw new ApplicationError(`Command "${command.name}" RPC adapter has no transaction capability`, {
|
|
1679
|
+
code: "COMMAND_TRANSACTION_UNCONFIGURED"
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1582
1685
|
function joinPaths(prefix, path) {
|
|
1583
1686
|
const joined = `${prefix}/${path}`.replace(/\/{2,}/g, "/");
|
|
1584
1687
|
return joined.length > 1 ? joined.replace(/\/+$/, "") : joined;
|
|
@@ -1625,6 +1728,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1625
1728
|
"invoker",
|
|
1626
1729
|
"command",
|
|
1627
1730
|
"aspects",
|
|
1731
|
+
"aspectPipeline",
|
|
1628
1732
|
"paramTransforms",
|
|
1629
1733
|
"paramDefaults",
|
|
1630
1734
|
"queryTransforms",
|
|
@@ -1647,6 +1751,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1647
1751
|
if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
|
|
1648
1752
|
throw new ApplicationError(`Module "${compiled.name}" has command routes but no commandGovernance`, { code: "COMMAND_GOVERNANCE_UNCONFIGURED" });
|
|
1649
1753
|
}
|
|
1754
|
+
assertCommandGovernanceReady(compiled, options.commandGovernance, options.commandExecutor);
|
|
1650
1755
|
const commandsByClassName = new Map((compiled.commands ?? []).map((command) => [command.className, command]));
|
|
1651
1756
|
for (const controller of compiled.controllers) {
|
|
1652
1757
|
for (const route of controller.routes) {
|
|
@@ -1747,14 +1852,13 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1747
1852
|
services,
|
|
1748
1853
|
metadata: command ?? route
|
|
1749
1854
|
};
|
|
1750
|
-
const
|
|
1751
|
-
const
|
|
1752
|
-
const
|
|
1753
|
-
const
|
|
1754
|
-
const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
|
|
1855
|
+
const routePipeline = descriptorPipeline(route, "route", options.onExecution);
|
|
1856
|
+
const commandPipeline = descriptorPipeline(command ?? {}, "command", options.onExecution);
|
|
1857
|
+
const modulePipeline = descriptorPipeline(compiled, `module:${compiled.name}`, options.onExecution);
|
|
1858
|
+
const invokeRoute = () => modulePipeline(route.command ? commandContext : routeContext, once(() => routePipeline(routeContext, once(() => route.command ? commandPipeline(commandContext, once(invoke)) : invoke()))));
|
|
1755
1859
|
const invokeCommand = () => {
|
|
1756
1860
|
if (!route.command)
|
|
1757
|
-
return
|
|
1861
|
+
return invokeRoute();
|
|
1758
1862
|
if (!command) {
|
|
1759
1863
|
throw new ApplicationError(`Command "${route.command}" is not registered`, {
|
|
1760
1864
|
code: "COMMAND_NOT_REGISTERED"
|
|
@@ -1779,11 +1883,11 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
1779
1883
|
operation: command.name,
|
|
1780
1884
|
stage: "commandExecutor",
|
|
1781
1885
|
...executionTrace(requestContext)
|
|
1782
|
-
}, () => commandExecutor(invocation,
|
|
1886
|
+
}, () => commandExecutor(invocation, once(invokeRoute)));
|
|
1783
1887
|
};
|
|
1784
|
-
return
|
|
1888
|
+
return invokeCommand();
|
|
1785
1889
|
};
|
|
1786
|
-
const result =
|
|
1890
|
+
const result = await execute();
|
|
1787
1891
|
assertDeclaredResponseStatus(route, result, ctx.set.status);
|
|
1788
1892
|
return result;
|
|
1789
1893
|
};
|
|
@@ -1846,8 +1950,8 @@ async function executeJob(compiled, services, job, input, requestContext, import
|
|
|
1846
1950
|
stage: "handler",
|
|
1847
1951
|
...executionTrace(requestContext)
|
|
1848
1952
|
}, () => Reflect.apply(method, instance, [decodedInput])));
|
|
1849
|
-
const pipeline =
|
|
1850
|
-
const jobPipeline =
|
|
1953
|
+
const pipeline = descriptorPipeline(compiled, `module:${compiled.name}`, observer);
|
|
1954
|
+
const jobPipeline = descriptorPipeline(job, "job", observer);
|
|
1851
1955
|
const result = await pipeline(context, () => jobPipeline(context, () => observeExecution(observer, {
|
|
1852
1956
|
kind: "job",
|
|
1853
1957
|
operation: job.name,
|
|
@@ -1947,6 +2051,7 @@ export {
|
|
|
1947
2051
|
composeAspects,
|
|
1948
2052
|
composeCommandExecutors,
|
|
1949
2053
|
createApplication,
|
|
2054
|
+
createCommandAuthorizationAdapter,
|
|
1950
2055
|
createCommandExecutor,
|
|
1951
2056
|
createDocumentationPlugin,
|
|
1952
2057
|
createMemoryPolicy,
|
|
@@ -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: {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CommandAuthorization, type CommandIdentity } from "@supacloud/contracts";
|
|
2
2
|
import { type CommandDatabase, type CommandTransaction } from "@supacloud/db";
|
|
3
|
+
import { type CommandExecutor } from "./index";
|
|
3
4
|
export { decodeWebhookInput } from "./fixtures/webhook/contracts";
|
|
4
5
|
export declare const WEBHOOK_EXAMPLE_SQL = "CREATE TABLE IF NOT EXISTS public.webhook_module_example (\n tenant_id text NOT NULL, id text NOT NULL, enabled boolean NOT NULL,\n writes integer NOT NULL DEFAULT 0, PRIMARY KEY (tenant_id,id)\n);";
|
|
6
|
+
/**
|
|
7
|
+
* Register the existing native command boundary, not a pass-through executor.
|
|
8
|
+
* Controllers still call their generated UpdateWebhook service directly. Any
|
|
9
|
+
* explicit command invocation delegates to that same service, which owns
|
|
10
|
+
* authorization, durable receipts, transactions and audit. Never run a second
|
|
11
|
+
* route continuation or construct another command runtime here.
|
|
12
|
+
*/
|
|
13
|
+
export declare const executeWebhookCommand: CommandExecutor;
|
|
5
14
|
/** Composition only; the compiler generates factories and routes from the real module. */
|
|
6
15
|
export declare function createWebhookMigrationExample(options: {
|
|
7
16
|
database: CommandDatabase;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/elysia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|
|
@@ -46,21 +46,21 @@
|
|
|
46
46
|
"directory": "packages/elysia"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@supacloud/app": "0.
|
|
49
|
+
"@supacloud/app": "0.15.0",
|
|
50
50
|
"@supacloud/contracts": "0.4.0",
|
|
51
51
|
"jose": "^6.2.11"
|
|
52
52
|
},
|
|
53
53
|
"overrides": {
|
|
54
54
|
"@supacloud/contracts": "0.4.0",
|
|
55
|
-
"@supacloud/commands": "0.
|
|
55
|
+
"@supacloud/commands": "0.5.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"elysia": "^1.4.30"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@supacloud/compiler": "0.
|
|
62
|
-
"@supacloud/commands": "0.
|
|
63
|
-
"@supacloud/db": "0.
|
|
61
|
+
"@supacloud/compiler": "0.23.0",
|
|
62
|
+
"@supacloud/commands": "0.5.0",
|
|
63
|
+
"@supacloud/db": "0.8.0",
|
|
64
64
|
"@types/bun": "^1.4.2",
|
|
65
65
|
"elysia": "^1.4.30",
|
|
66
66
|
"typescript": "^7.0.2"
|