@zerotal/core 1.0.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/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +72 -0
- package/src/application/Application.ts +1671 -0
- package/src/application/BootDoctor.ts +108 -0
- package/src/application/DevErrorPage.ts +567 -0
- package/src/application/ExceptionHandler.ts +183 -0
- package/src/application/currentApp.ts +73 -0
- package/src/assets/assets.ts +79 -0
- package/src/assets/index.ts +16 -0
- package/src/auth/AuthenticatedUser.ts +18 -0
- package/src/build/PackageLinter.ts +146 -0
- package/src/build/PackageScaffold.ts +127 -0
- package/src/build/codemod.ts +64 -0
- package/src/build/index.ts +12 -0
- package/src/command/Command.ts +254 -0
- package/src/command/CommandRunner.ts +593 -0
- package/src/command/OutputWriter.ts +61 -0
- package/src/command/builtin/CompileCommand.ts +46 -0
- package/src/command/builtin/CssBuildCommand.ts +71 -0
- package/src/command/builtin/KeyGenerateCommand.ts +58 -0
- package/src/command/builtin/LintPackagesCommand.ts +72 -0
- package/src/command/builtin/MakeCommandCommand.ts +85 -0
- package/src/command/builtin/MakeControllerCommand.ts +95 -0
- package/src/command/builtin/MakeEventCommand.ts +85 -0
- package/src/command/builtin/MakeJobCommand.ts +53 -0
- package/src/command/builtin/MakeListenerCommand.ts +35 -0
- package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
- package/src/command/builtin/MakeNotificationCommand.ts +48 -0
- package/src/command/builtin/MakeObserverCommand.ts +78 -0
- package/src/command/builtin/MakePackageCommand.ts +45 -0
- package/src/command/builtin/MakePolicyCommand.ts +66 -0
- package/src/command/builtin/MakeProviderCommand.ts +75 -0
- package/src/command/builtin/MakeRequestCommand.ts +47 -0
- package/src/command/builtin/MakeResourceCommand.ts +61 -0
- package/src/command/builtin/MakeTestCommand.ts +120 -0
- package/src/command/builtin/ReloadCommand.ts +52 -0
- package/src/command/builtin/ReplCommand.ts +174 -0
- package/src/command/builtin/RouteListCommand.ts +188 -0
- package/src/command/builtin/ServeCommand.ts +321 -0
- package/src/command/builtin/StartCommand.ts +3 -0
- package/src/command/builtin/StatusCommand.ts +71 -0
- package/src/command/builtin/TestCommand.ts +172 -0
- package/src/command/builtin/WorkerCommand.ts +27 -0
- package/src/command/builtin/index.ts +53 -0
- package/src/command/scaffold/worker.ts.txt +12 -0
- package/src/command/scaffold/zerotal.ts.txt +26 -0
- package/src/command/startZerotal.ts +55 -0
- package/src/config/AppConfig.ts +253 -0
- package/src/config/ConfigLoader.ts +117 -0
- package/src/config/ConfigManager.ts +169 -0
- package/src/config/index.ts +46 -0
- package/src/config/registry.ts +59 -0
- package/src/config/validation.ts +117 -0
- package/src/container/Container.ts +606 -0
- package/src/container/ContextualBindingBuilder.ts +57 -0
- package/src/container/ScopedResolver.ts +117 -0
- package/src/container/index.ts +32 -0
- package/src/container/inject.ts +55 -0
- package/src/container/types.ts +71 -0
- package/src/context/RequestContext.ts +91 -0
- package/src/contracts/auth.ts +24 -0
- package/src/contracts/index.ts +23 -0
- package/src/contracts/session.ts +70 -0
- package/src/contracts/transaction.ts +26 -0
- package/src/conventions/ConventionLoader.ts +128 -0
- package/src/conventions/builtinConcerns.ts +131 -0
- package/src/crypt/Crypt.ts +141 -0
- package/src/crypt/URLSigner.ts +96 -0
- package/src/datetime/Carbon.ts +1396 -0
- package/src/datetime/CarbonInterval.ts +421 -0
- package/src/datetime/clock.ts +28 -0
- package/src/datetime/index.ts +23 -0
- package/src/datetime/temporal-shim.ts +1 -0
- package/src/dev/BuildOutput.ts +131 -0
- package/src/dev/CssPlugins.ts +184 -0
- package/src/dev/DevBuildHook.ts +74 -0
- package/src/dev/DevOrchestrator.ts +213 -0
- package/src/dev/DevReloadMiddleware.ts +101 -0
- package/src/dev/DevReloadServer.ts +85 -0
- package/src/dev/DevWsServer.ts +45 -0
- package/src/dev/index.ts +19 -0
- package/src/dev/reloadClient.ts +39 -0
- package/src/env/Def.ts +232 -0
- package/src/env/EnvSchema.ts +105 -0
- package/src/env/index.ts +34 -0
- package/src/env/t.ts +128 -0
- package/src/errors/ConfigError.ts +12 -0
- package/src/errors/ContainerErrors.ts +143 -0
- package/src/errors/HttpError.ts +127 -0
- package/src/errors/ValidationError.ts +19 -0
- package/src/errors/ZerotalError.ts +25 -0
- package/src/errors/index.ts +46 -0
- package/src/events/CallQueuedListener.ts +66 -0
- package/src/events/Emitter.ts +280 -0
- package/src/events/EventFake.ts +160 -0
- package/src/events/FrameworkEvents.ts +252 -0
- package/src/facade/Facade.ts +101 -0
- package/src/facade/facades/App.ts +155 -0
- package/src/facade/facades/Artisan.ts +63 -0
- package/src/facade/facades/Config.ts +21 -0
- package/src/facade/facades/Events.ts +19 -0
- package/src/facade/facades/index.ts +28 -0
- package/src/global.d.ts +9 -0
- package/src/hash/Hash.ts +60 -0
- package/src/health/Health.ts +221 -0
- package/src/health/index.ts +27 -0
- package/src/helpers/Collection.ts +435 -0
- package/src/helpers/config.ts +59 -0
- package/src/helpers/fluent.ts +52 -0
- package/src/helpers/html.ts +11 -0
- package/src/helpers/index.ts +266 -0
- package/src/helpers/make.ts +35 -0
- package/src/helpers/markdown.ts +73 -0
- package/src/helpers/pageElements.ts +27 -0
- package/src/helpers/request.ts +62 -0
- package/src/helpers/response.ts +411 -0
- package/src/helpers/str.ts +208 -0
- package/src/http/Http.ts +298 -0
- package/src/http/HttpClient.ts +289 -0
- package/src/http/Resource.ts +171 -0
- package/src/http/UploadedFile.ts +204 -0
- package/src/http/Uri.ts +490 -0
- package/src/http/index.ts +46 -0
- package/src/http/negotiate.ts +213 -0
- package/src/http/originGuard.ts +76 -0
- package/src/http/sniffContentType.ts +105 -0
- package/src/http/url.ts +204 -0
- package/src/http/withHeaders.ts +24 -0
- package/src/index.ts +250 -0
- package/src/lock/LockManager.ts +228 -0
- package/src/lock/config.ts +49 -0
- package/src/lock/drivers/LockDriver.ts +32 -0
- package/src/lock/drivers/MemoryLockDriver.ts +52 -0
- package/src/lock/drivers/RedisLockDriver.ts +58 -0
- package/src/lock/drivers/SqliteLockDriver.ts +85 -0
- package/src/lock/errors.ts +20 -0
- package/src/lock/facades/Lock.ts +114 -0
- package/src/lock/index.ts +53 -0
- package/src/logger/Log.ts +35 -0
- package/src/logger/LogManager.ts +430 -0
- package/src/logger/LoggerMiddleware.ts +125 -0
- package/src/logger/channels/ConsoleChannel.ts +139 -0
- package/src/logger/channels/DailyChannel.ts +74 -0
- package/src/logger/channels/NullChannel.ts +17 -0
- package/src/logger/channels/SingleChannel.ts +34 -0
- package/src/logger/channels/StackChannel.ts +29 -0
- package/src/logger/config.ts +90 -0
- package/src/logger/format.ts +96 -0
- package/src/logger/frameworkLog.ts +93 -0
- package/src/logger/index.ts +68 -0
- package/src/logger/renderTable.ts +111 -0
- package/src/logger/types.ts +212 -0
- package/src/macros/config.macro.ts +50 -0
- package/src/metrics/HttpMetrics.ts +114 -0
- package/src/metrics/index.ts +18 -0
- package/src/middleware/BaseMiddleware.ts +72 -0
- package/src/middleware/CorsMiddleware.ts +152 -0
- package/src/middleware/RateLimiter.ts +255 -0
- package/src/middleware/SecureHeadersMiddleware.ts +127 -0
- package/src/middleware/ThrottleMiddleware.ts +252 -0
- package/src/middleware/WebhookMiddleware.ts +204 -0
- package/src/pipeline/ContextRegistry.ts +42 -0
- package/src/pipeline/HttpContext.ts +865 -0
- package/src/pipeline/Pipeline.ts +150 -0
- package/src/pipeline/currentPage.ts +46 -0
- package/src/pipeline/types.ts +80 -0
- package/src/provider/LockProvider.ts +64 -0
- package/src/provider/LogProvider.ts +137 -0
- package/src/provider/ServiceProvider.ts +84 -0
- package/src/provider/StorageProvider.ts +45 -0
- package/src/router/FileRouter.ts +526 -0
- package/src/router/Route.ts +76 -0
- package/src/router/RouteHandler.ts +335 -0
- package/src/router/Router.ts +1247 -0
- package/src/router/domain.ts +65 -0
- package/src/security/index.ts +22 -0
- package/src/storage/FakeDisk.ts +233 -0
- package/src/storage/StorageFilesMiddleware.ts +150 -0
- package/src/storage/StorageManager.ts +173 -0
- package/src/storage/config.ts +47 -0
- package/src/storage/drivers/LocalDriver.ts +138 -0
- package/src/storage/drivers/S3Driver.ts +169 -0
- package/src/storage/errors.ts +135 -0
- package/src/storage/facades/Storage.ts +3 -0
- package/src/storage/global.d.ts +7 -0
- package/src/storage/index.ts +22 -0
- package/src/storage/root.ts +59 -0
- package/src/storage/types.ts +104 -0
- package/src/support/appKey.ts +38 -0
- package/src/support/cookie.ts +72 -0
- package/src/support/crypto.ts +52 -0
- package/src/support/deepMerge.ts +117 -0
- package/src/support/env.ts +71 -0
- package/src/support/network.ts +79 -0
- package/src/support/port.ts +197 -0
- package/src/support/str.ts +122 -0
- package/src/view/FileRouteResolver.ts +59 -0
- package/src/view/index.ts +144 -0
- package/src/view/jsx-runtime.ts +233 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a compiled route into a Bun request handler: it resolves model
|
|
3
|
+
* bindings, runs the middleware pipeline with the controller action as the
|
|
4
|
+
* final pipe, renders failures through the exception handler, and emits the
|
|
5
|
+
* framework request lifecycle events.
|
|
6
|
+
*/
|
|
7
|
+
import { RequestContext } from "../context/RequestContext.ts";
|
|
8
|
+
import { HttpContext } from "../pipeline/HttpContext.ts";
|
|
9
|
+
import type { RequestIPProvider } from "../pipeline/HttpContext.ts";
|
|
10
|
+
import { Pipeline } from "../pipeline/Pipeline.ts";
|
|
11
|
+
import { ExceptionHandler } from "../application/ExceptionHandler.ts";
|
|
12
|
+
import { recordHttp, beginHttp, endHttp } from "../metrics/HttpMetrics.ts";
|
|
13
|
+
import { FrameworkEvents, RequestHandled, RequestFailed } from "../events/FrameworkEvents.ts";
|
|
14
|
+
import { injectRegistry } from "../container/inject.ts";
|
|
15
|
+
import type { Pipe, NextFn } from "../pipeline/types.ts";
|
|
16
|
+
import type { Container } from "../container/Container.ts";
|
|
17
|
+
import type { RouteDefinition, MiddlewareClass } from "./Route.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Per-request provider hooks dispatched by Application on every HTTP request.
|
|
21
|
+
* Passed to createRouteHandler via Router.compile() so RouteHandler has no
|
|
22
|
+
* hard import dependency on Application (avoids a circular module graph).
|
|
23
|
+
*/
|
|
24
|
+
export interface ProviderHooks {
|
|
25
|
+
onRequestReceived(ctx: HttpContext): Promise<void>;
|
|
26
|
+
onRequestProcessed(ctx: HttpContext): Promise<void>;
|
|
27
|
+
/** Register onResponseSent for all providers via ctx.afterResponse(). */
|
|
28
|
+
scheduleResponseSent(ctx: HttpContext): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run a middleware chain against an HttpContext and report whether the
|
|
33
|
+
* request passed all the way through.
|
|
34
|
+
*
|
|
35
|
+
* "Passed" means every middleware called `next()` — a terminal probe pipe at
|
|
36
|
+
* the end of the chain executed. A middleware that short-circuits (returns
|
|
37
|
+
* without calling next, typically after setting `ctx.response`) yields
|
|
38
|
+
* `passed: false`; inspect `ctx.response` for the redirect/error it produced.
|
|
39
|
+
*
|
|
40
|
+
* Used by framework packages that need to re-run route middleware outside the
|
|
41
|
+
* normal HTTP pipeline (e.g. @zerotal/flow persistent middleware on WebSocket
|
|
42
|
+
* updates).
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* const middleware = Router.middlewareFor('GET', '/dashboard');
|
|
46
|
+
* const passed = await runMiddleware(ctx, middleware, container);
|
|
47
|
+
* if (!passed) {
|
|
48
|
+
* // blocked — ctx.response holds the middleware's redirect/401/…
|
|
49
|
+
* }
|
|
50
|
+
*/
|
|
51
|
+
export async function runMiddleware(
|
|
52
|
+
ctx: HttpContext,
|
|
53
|
+
middleware: MiddlewareClass[],
|
|
54
|
+
container?: Container,
|
|
55
|
+
): Promise<boolean> {
|
|
56
|
+
if (middleware.length === 0) return true;
|
|
57
|
+
|
|
58
|
+
let passed = false;
|
|
59
|
+
const TerminalProbe = class implements Pipe<HttpContext> {
|
|
60
|
+
async handle(_context: HttpContext, next: NextFn): Promise<Response | void> {
|
|
61
|
+
passed = true;
|
|
62
|
+
return next();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const pipeline = Pipeline.send<HttpContext>(ctx).through([
|
|
67
|
+
...middleware,
|
|
68
|
+
TerminalProbe as MiddlewareClass,
|
|
69
|
+
]);
|
|
70
|
+
if (container) pipeline.via(container);
|
|
71
|
+
await pipeline.thenReturn();
|
|
72
|
+
|
|
73
|
+
return passed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The parts of the request lifecycle that vary between the matched-route path
|
|
78
|
+
* (`createRouteHandler`) and the unmatched fallback (`Application.start()`'s
|
|
79
|
+
* fetch handler). Everything else — scoping, `HttpContext` creation, timing,
|
|
80
|
+
* the in-flight gauge, framework events, exception rendering, after-response
|
|
81
|
+
* hooks, and metrics — is shared in {@link dispatchRequest}.
|
|
82
|
+
*/
|
|
83
|
+
export interface DispatchOptions {
|
|
84
|
+
container: Container;
|
|
85
|
+
exceptionHandler?: ExceptionHandler | undefined;
|
|
86
|
+
providerHooks?: ProviderHooks | undefined;
|
|
87
|
+
/** Configure the freshly created context (params, route info) before dispatch. */
|
|
88
|
+
configure?: (ctx: HttpContext) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Path-specific work: run the pipeline and produce the response (or leave it
|
|
91
|
+
* on `ctx.response`). Errors thrown here are rendered by the exception handler.
|
|
92
|
+
*/
|
|
93
|
+
execute: (ctx: HttpContext) => Promise<Response | undefined>;
|
|
94
|
+
/**
|
|
95
|
+
* Render an unhandled error to a Response. Defaults to reporting + rendering
|
|
96
|
+
* through `exceptionHandler` when present, else `ExceptionHandler.defaultRender`.
|
|
97
|
+
*/
|
|
98
|
+
renderError?: (error: unknown, ctx: HttpContext) => Promise<Response>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Run one HTTP request through the full framework lifecycle:
|
|
103
|
+
*
|
|
104
|
+
* runScoped → HttpContext → RequestContext → in-flight gauge → provider
|
|
105
|
+
* hooks → execute() → exception handling → RequestHandled/RequestFailed →
|
|
106
|
+
* after-response callbacks → metrics.
|
|
107
|
+
*
|
|
108
|
+
* Shared by `createRouteHandler` (matched routes) and `Application.start()`'s
|
|
109
|
+
* fetch fallback (unmatched requests) so instrumentation — notably the
|
|
110
|
+
* `beginHttp`/`endHttp` in-flight gauge — applies uniformly to both.
|
|
111
|
+
*/
|
|
112
|
+
export async function dispatchRequest(
|
|
113
|
+
req: Request,
|
|
114
|
+
server: unknown,
|
|
115
|
+
options: DispatchOptions,
|
|
116
|
+
): Promise<Response> {
|
|
117
|
+
const { container, exceptionHandler, providerHooks } = options;
|
|
118
|
+
return container.runScoped(async (scoped) => {
|
|
119
|
+
const ctx = new HttpContext(req, scoped);
|
|
120
|
+
ctx._server = server as RequestIPProvider | undefined;
|
|
121
|
+
options.configure?.(ctx);
|
|
122
|
+
|
|
123
|
+
return RequestContext.run(ctx, async (): Promise<Response> => {
|
|
124
|
+
const startedAtHighRes = performance.now();
|
|
125
|
+
const startedAtMs = Date.now();
|
|
126
|
+
beginHttp(); // in-flight gauge ++ (paired with endHttp in finally)
|
|
127
|
+
let response: Response;
|
|
128
|
+
let failure: unknown;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
if (providerHooks) await providerHooks.onRequestReceived(ctx);
|
|
132
|
+
|
|
133
|
+
const result = await options.execute(ctx);
|
|
134
|
+
|
|
135
|
+
if (providerHooks) await providerHooks.onRequestProcessed(ctx);
|
|
136
|
+
|
|
137
|
+
response = result ?? ctx.response ?? new Response("", { status: 204 });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
failure = error;
|
|
140
|
+
if (options.renderError) {
|
|
141
|
+
response = await options.renderError(error, ctx);
|
|
142
|
+
} else if (exceptionHandler) {
|
|
143
|
+
await exceptionHandler.report(error, ctx);
|
|
144
|
+
response = await exceptionHandler.render(error, ctx);
|
|
145
|
+
} else {
|
|
146
|
+
response = await ExceptionHandler.defaultRender(error, ctx);
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
endHttp(); // in-flight gauge -- (always, even if error rendering throws)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Last chance to touch the response. Runs for a rendered error exactly as
|
|
153
|
+
// it does for a success, which is what lets session state set before a
|
|
154
|
+
// throw (flashed validation errors, old input) reach the client on the
|
|
155
|
+
// redirect that was supposed to carry it.
|
|
156
|
+
for (const finalize of ctx._responseFinalizers) {
|
|
157
|
+
try {
|
|
158
|
+
await finalize(response!);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error("[Zerotal] response finalizer failed:", error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Framework instrumentation: emit on every outcome so subscribers
|
|
165
|
+
// (devtools, logger, telemetry) can finalise the request trace.
|
|
166
|
+
const durationMs = Math.round(performance.now() - startedAtHighRes);
|
|
167
|
+
if (failure !== undefined) {
|
|
168
|
+
FrameworkEvents.emit(
|
|
169
|
+
new RequestFailed(
|
|
170
|
+
ctx,
|
|
171
|
+
startedAtMs,
|
|
172
|
+
durationMs,
|
|
173
|
+
failure instanceof Error ? failure.message : String(failure),
|
|
174
|
+
response!.status,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
} else {
|
|
178
|
+
FrameworkEvents.emit(new RequestHandled(ctx, startedAtMs, durationMs));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// scheduleResponseSent fires for every request — success and error alike.
|
|
182
|
+
// Providers observing onResponseSent see all outcomes via ctx.response.
|
|
183
|
+
if (providerHooks) providerHooks.scheduleResponseSent(ctx);
|
|
184
|
+
|
|
185
|
+
// runScoped's finally block calls scoped.flush() — no manual flush needed.
|
|
186
|
+
if (ctx._afterResponseCallbacks.length > 0) {
|
|
187
|
+
void Promise.allSettled(ctx._afterResponseCallbacks.map((callback) => callback()));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
recordHttp(response!.status, performance.now() - startedAtHighRes);
|
|
192
|
+
} catch {
|
|
193
|
+
// Metrics are best-effort and must never break a request.
|
|
194
|
+
}
|
|
195
|
+
return response!;
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Whether a controller class declares constructor dependencies — via
|
|
202
|
+
* `@inject()` metadata or declared constructor parameters. Used to decide whether zero-arg construction is a
|
|
203
|
+
* safe fallback when container resolution fails.
|
|
204
|
+
*/
|
|
205
|
+
function _declaresConstructorDeps(ctor: RouteDefinition["controller"]): boolean {
|
|
206
|
+
const injected = injectRegistry.get(ctor);
|
|
207
|
+
if (injected && injected.length > 0) return true;
|
|
208
|
+
return ctor.length > 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Build a Bun-compatible request handler for a single route.
|
|
213
|
+
*
|
|
214
|
+
* The controller action is injected as the LAST pipe in the chain so that
|
|
215
|
+
* middleware finally-blocks (e.g. SessionMiddleware saving the cookie) execute
|
|
216
|
+
* after ctx.response has been set by the controller.
|
|
217
|
+
*
|
|
218
|
+
* Uses container.runScoped() so the scoped resolver is placed in AsyncLocalStorage,
|
|
219
|
+
* allowing container.make() for scoped bindings to work correctly inside matched routes.
|
|
220
|
+
*/
|
|
221
|
+
export function createRouteHandler(
|
|
222
|
+
definition: RouteDefinition,
|
|
223
|
+
container: Container,
|
|
224
|
+
globalMiddleware: MiddlewareClass[] = [],
|
|
225
|
+
exceptionHandler?: ExceptionHandler,
|
|
226
|
+
providerHooks?: ProviderHooks,
|
|
227
|
+
): (req: Request, server?: unknown) => Promise<Response> {
|
|
228
|
+
return (req: Request, server?: unknown): Promise<Response> =>
|
|
229
|
+
dispatchRequest(req, server, {
|
|
230
|
+
container,
|
|
231
|
+
exceptionHandler,
|
|
232
|
+
providerHooks,
|
|
233
|
+
configure(ctx) {
|
|
234
|
+
ctx.params = (req as { params?: Record<string, string> }).params ?? {};
|
|
235
|
+
ctx._routeDef = {
|
|
236
|
+
pattern: definition.path,
|
|
237
|
+
controller: definition.controller.name,
|
|
238
|
+
action: definition.action,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
async execute(ctx) {
|
|
242
|
+
// Resolve the controller before building the pipe chain so failures
|
|
243
|
+
// are caught by dispatchRequest and return a proper error response.
|
|
244
|
+
// Zero-arg construction is only a fallback for dependency-FREE
|
|
245
|
+
// controllers; a controller that declares dependencies must fail
|
|
246
|
+
// loudly here rather than run with `undefined` injections that
|
|
247
|
+
// explode far from the cause.
|
|
248
|
+
let controller: Record<string, unknown>;
|
|
249
|
+
try {
|
|
250
|
+
controller = (await container.make(definition.controller)) as Record<string, unknown>;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (_declaresConstructorDeps(definition.controller)) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Failed to resolve controller ${definition.controller.name} from the container: ` +
|
|
255
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
256
|
+
{ cause: error },
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
controller = new definition.controller() as Record<string, unknown>;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const action = controller[definition.action];
|
|
263
|
+
if (typeof action !== "function") {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Controller action "${definition.action}" not found on ${definition.controller.name}.`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Build a single-use Pipe that invokes the controller action.
|
|
270
|
+
// Placing it LAST in the chain means middleware finally-blocks run
|
|
271
|
+
// after ctx.response is set — required for session cookie persistence.
|
|
272
|
+
const boundAction = (
|
|
273
|
+
action as (ctx: HttpContext) => Promise<Response | void> | Response | void
|
|
274
|
+
).bind(controller);
|
|
275
|
+
|
|
276
|
+
const ControllerPipe = class implements Pipe<HttpContext> {
|
|
277
|
+
// Innermost pipe — runs the controller action and produces the response.
|
|
278
|
+
// It never calls next(); the pipeline terminal reads ctx.response.
|
|
279
|
+
// Route params and resolved model bindings are already on ctx.params.
|
|
280
|
+
async handle(context: HttpContext, _next: NextFn): Promise<Response | void> {
|
|
281
|
+
const result = await boundAction(context);
|
|
282
|
+
if (result instanceof Response) {
|
|
283
|
+
context.response = result;
|
|
284
|
+
}
|
|
285
|
+
return context.response;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// ── Route-model binding ────────────────────────────────────────────
|
|
290
|
+
// Second-innermost pipe: every middleware has run, the controller has
|
|
291
|
+
// not.
|
|
292
|
+
//
|
|
293
|
+
// This used to resolve before the pipeline, which put a database read
|
|
294
|
+
// and a 404 ahead of authentication: a protected route answered 404 for
|
|
295
|
+
// a missing id but 401 for one that existed, so anyone could enumerate
|
|
296
|
+
// which records exist without logging in. Middleware now decides
|
|
297
|
+
// whether the request may proceed at all before a binding is resolved.
|
|
298
|
+
//
|
|
299
|
+
// A throw here still renders correctly — `execute()` is wrapped by
|
|
300
|
+
// dispatchRequest's exception handler — and it now unwinds *through*
|
|
301
|
+
// the middleware, so their finally-blocks (e.g. SessionMiddleware
|
|
302
|
+
// persisting the cookie) run on a binding 404 as they do on any other
|
|
303
|
+
// failure.
|
|
304
|
+
const ModelBindingPipe = class implements Pipe<HttpContext> {
|
|
305
|
+
async handle(context: HttpContext, next: NextFn): Promise<Response | void> {
|
|
306
|
+
const params = context.params as Record<string, unknown>;
|
|
307
|
+
for (const [paramName, resolver] of definition.bindings) {
|
|
308
|
+
const paramValue = params[paramName] as string | undefined;
|
|
309
|
+
if (paramValue === undefined) continue;
|
|
310
|
+
const instance = await resolver(paramValue, context);
|
|
311
|
+
context._models.set(paramName, instance);
|
|
312
|
+
// Fold the resolved model onto params under its param name so handlers
|
|
313
|
+
// read it via `ctx.params.post` (raw string params stay as-is).
|
|
314
|
+
params[paramName] = instance;
|
|
315
|
+
}
|
|
316
|
+
return next();
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const allPipes = [
|
|
321
|
+
...globalMiddleware,
|
|
322
|
+
...definition.middleware,
|
|
323
|
+
...(definition.bindings.size > 0 ? [ModelBindingPipe as MiddlewareClass] : []),
|
|
324
|
+
ControllerPipe as MiddlewareClass,
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
const finalContext = await Pipeline.send<HttpContext>(ctx)
|
|
328
|
+
.through(allPipes)
|
|
329
|
+
.via(container)
|
|
330
|
+
.thenReturn();
|
|
331
|
+
|
|
332
|
+
return finalContext.response ?? undefined;
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|