@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,1256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module System
|
|
3
|
+
*
|
|
4
|
+
* NestJS-inspired module system with dependency injection,
|
|
5
|
+
* controllers, and provider management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Container,
|
|
10
|
+
type Provider,
|
|
11
|
+
type Token,
|
|
12
|
+
type ForwardRef,
|
|
13
|
+
forwardRef,
|
|
14
|
+
isForwardRef,
|
|
15
|
+
resolveForwardRef,
|
|
16
|
+
getInjectTokens,
|
|
17
|
+
} from "../container";
|
|
18
|
+
import {
|
|
19
|
+
type LazyModuleLoader,
|
|
20
|
+
type Constructor as LazyConstructor,
|
|
21
|
+
type ModuleLoaderFn,
|
|
22
|
+
type LazyModuleMetadata,
|
|
23
|
+
LazyModuleLoaderImpl,
|
|
24
|
+
LazyModule,
|
|
25
|
+
ModuleLoader,
|
|
26
|
+
MODULE_LOADER_TOKEN,
|
|
27
|
+
getLazyMetadata,
|
|
28
|
+
isLazyModule,
|
|
29
|
+
LazyModuleRegistry,
|
|
30
|
+
} from "./lazy";
|
|
31
|
+
import type { Context } from "../context";
|
|
32
|
+
import { Router } from "../router";
|
|
33
|
+
import type { RouteHandler } from "../types";
|
|
34
|
+
import {
|
|
35
|
+
LifecycleHookManager,
|
|
36
|
+
ShutdownSignalHandler,
|
|
37
|
+
type OnModuleInit,
|
|
38
|
+
type OnApplicationBootstrap,
|
|
39
|
+
type OnModuleDestroy,
|
|
40
|
+
type BeforeApplicationShutdown,
|
|
41
|
+
type OnApplicationShutdown,
|
|
42
|
+
type OnBeforeRequest,
|
|
43
|
+
type OnAfterRequest,
|
|
44
|
+
type OnRequestError,
|
|
45
|
+
type ApplicationLifecycle,
|
|
46
|
+
type RequestLifecycle,
|
|
47
|
+
type FullLifecycle,
|
|
48
|
+
isOnModuleInit,
|
|
49
|
+
isOnApplicationBootstrap,
|
|
50
|
+
isOnModuleDestroy,
|
|
51
|
+
isBeforeApplicationShutdown,
|
|
52
|
+
isOnApplicationShutdown,
|
|
53
|
+
isOnBeforeRequest,
|
|
54
|
+
isOnAfterRequest,
|
|
55
|
+
isOnRequestError,
|
|
56
|
+
} from "./lifecycle";
|
|
57
|
+
import {
|
|
58
|
+
type Guard,
|
|
59
|
+
type CanActivate,
|
|
60
|
+
type GuardFn,
|
|
61
|
+
getClassGuards,
|
|
62
|
+
getMethodGuards,
|
|
63
|
+
executeGuards,
|
|
64
|
+
createForbiddenResponse,
|
|
65
|
+
} from "./guards";
|
|
66
|
+
import {
|
|
67
|
+
type Interceptor,
|
|
68
|
+
type NestInterceptor,
|
|
69
|
+
type InterceptorFn,
|
|
70
|
+
type CallHandler,
|
|
71
|
+
getClassInterceptors,
|
|
72
|
+
getMethodInterceptors,
|
|
73
|
+
executeInterceptors,
|
|
74
|
+
isNestInterceptor,
|
|
75
|
+
isInterceptorFn,
|
|
76
|
+
} from "./interceptors";
|
|
77
|
+
import {
|
|
78
|
+
type Pipe,
|
|
79
|
+
type PipeTransform,
|
|
80
|
+
type PipeFn,
|
|
81
|
+
type PipeContext,
|
|
82
|
+
type ParameterPipeMetadata,
|
|
83
|
+
getMethodPipes,
|
|
84
|
+
executePipes,
|
|
85
|
+
extractParameterValue,
|
|
86
|
+
createBadRequestResponse,
|
|
87
|
+
} from "./pipes";
|
|
88
|
+
import {
|
|
89
|
+
type Filter,
|
|
90
|
+
type ExceptionFilter,
|
|
91
|
+
type FilterFn,
|
|
92
|
+
type ExecuteFiltersOptions,
|
|
93
|
+
UseFilters,
|
|
94
|
+
Catch,
|
|
95
|
+
getClassFilters,
|
|
96
|
+
getMethodFilters,
|
|
97
|
+
getCatchType,
|
|
98
|
+
canHandleException,
|
|
99
|
+
isExceptionFilter,
|
|
100
|
+
isFilterFn,
|
|
101
|
+
executeFilter,
|
|
102
|
+
findAndExecuteFilter,
|
|
103
|
+
HttpExceptionFilter,
|
|
104
|
+
ValidationFilter,
|
|
105
|
+
NotFoundFilter,
|
|
106
|
+
AllExceptionsFilter,
|
|
107
|
+
createDefaultErrorResponse,
|
|
108
|
+
createInternalErrorResponse,
|
|
109
|
+
} from "./filters";
|
|
110
|
+
|
|
111
|
+
// ============= Types =============
|
|
112
|
+
|
|
113
|
+
// Type alias for class constructors - re-exported from metadata.ts
|
|
114
|
+
export type Constructor = new (...args: unknown[]) => unknown;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @deprecated Use individual lifecycle hook interfaces instead.
|
|
118
|
+
* Kept for backward compatibility.
|
|
119
|
+
*/
|
|
120
|
+
export interface LifecycleHooks {
|
|
121
|
+
onModuleInit?(): void | Promise<void>;
|
|
122
|
+
onModuleDestroy?(): void | Promise<void>;
|
|
123
|
+
onApplicationBootstrap?(): void | Promise<void>;
|
|
124
|
+
onApplicationShutdown?(): void | Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============= Metadata Storage =============
|
|
128
|
+
// Import metadata storage and decorators from isolated module to avoid circular dependencies
|
|
129
|
+
import {
|
|
130
|
+
setMetadata,
|
|
131
|
+
getMetadata,
|
|
132
|
+
setPrototypeMetadata,
|
|
133
|
+
getPrototypeMetadata,
|
|
134
|
+
Injectable,
|
|
135
|
+
Controller,
|
|
136
|
+
Module,
|
|
137
|
+
type ModuleMetadata,
|
|
138
|
+
} from "./metadata";
|
|
139
|
+
|
|
140
|
+
// Re-export decorators and types for external use
|
|
141
|
+
export { Injectable, Controller, Module, type ModuleMetadata };
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Inject a dependency by token.
|
|
145
|
+
* Supports both Token and ForwardRef<Token> for circular dependency resolution.
|
|
146
|
+
*
|
|
147
|
+
* @param token - The injection token or a forward reference to the token
|
|
148
|
+
* @returns A parameter decorator that registers the injection metadata
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* // Regular injection
|
|
153
|
+
* constructor(@Inject(MY_TOKEN) private service: MyService) {}
|
|
154
|
+
*
|
|
155
|
+
* // Forward reference for circular dependency
|
|
156
|
+
* constructor(@Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB) {}
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export function Inject(token: Token | ForwardRef<Token>): ParameterDecorator {
|
|
160
|
+
return (
|
|
161
|
+
target: unknown,
|
|
162
|
+
propertyKey: string | symbol | undefined,
|
|
163
|
+
parameterIndex: number,
|
|
164
|
+
) => {
|
|
165
|
+
const targetObj = target as Constructor;
|
|
166
|
+
const existingTokens: Array<Token | ForwardRef<Token>> =
|
|
167
|
+
getMetadata(targetObj, "inject:tokens") ?? [];
|
|
168
|
+
existingTokens[parameterIndex] = token;
|
|
169
|
+
setMetadata(targetObj, "inject:tokens", existingTokens);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============= HTTP Method Decorators =============
|
|
174
|
+
|
|
175
|
+
function createMethodDecorator(
|
|
176
|
+
method: string,
|
|
177
|
+
): (path?: string) => MethodDecorator {
|
|
178
|
+
return (path = "") => {
|
|
179
|
+
return (
|
|
180
|
+
target: unknown,
|
|
181
|
+
propertyKey: string | symbol,
|
|
182
|
+
descriptor: PropertyDescriptor,
|
|
183
|
+
) => {
|
|
184
|
+
const targetObj = target as object;
|
|
185
|
+
const routes =
|
|
186
|
+
getPrototypeMetadata<
|
|
187
|
+
Array<{ method: string; path: string; handler: string | symbol }>
|
|
188
|
+
>(targetObj, "routes") ?? [];
|
|
189
|
+
routes.push({
|
|
190
|
+
method,
|
|
191
|
+
path,
|
|
192
|
+
handler: propertyKey,
|
|
193
|
+
});
|
|
194
|
+
setPrototypeMetadata(targetObj, "routes", routes);
|
|
195
|
+
return descriptor;
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const Get = createMethodDecorator("GET");
|
|
201
|
+
export const Post = createMethodDecorator("POST");
|
|
202
|
+
export const Put = createMethodDecorator("PUT");
|
|
203
|
+
export const Patch = createMethodDecorator("PATCH");
|
|
204
|
+
export const Delete = createMethodDecorator("DELETE");
|
|
205
|
+
export const Head = createMethodDecorator("HEAD");
|
|
206
|
+
export const Options = createMethodDecorator("OPTIONS");
|
|
207
|
+
|
|
208
|
+
// ============= AppModule Class =============
|
|
209
|
+
|
|
210
|
+
export class AppModule {
|
|
211
|
+
private moduleClass: Constructor;
|
|
212
|
+
private metadata: ModuleMetadata;
|
|
213
|
+
private providers: Provider[] = [];
|
|
214
|
+
private controllers: Constructor[] = [];
|
|
215
|
+
private visitedModules = new Set<Constructor>();
|
|
216
|
+
private lazyModules: Constructor[] = [];
|
|
217
|
+
|
|
218
|
+
constructor(moduleClass: Constructor) {
|
|
219
|
+
this.moduleClass = moduleClass;
|
|
220
|
+
this.metadata = getMetadata<ModuleMetadata>(moduleClass, "module") ?? {};
|
|
221
|
+
this.processModule(moduleClass);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Process module and its imports recursively
|
|
226
|
+
* Skips lazy modules during initial processing
|
|
227
|
+
*/
|
|
228
|
+
private processModule(moduleClass: Constructor): void {
|
|
229
|
+
if (this.visitedModules.has(moduleClass)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.visitedModules.add(moduleClass);
|
|
233
|
+
|
|
234
|
+
// Skip processing lazy modules at startup
|
|
235
|
+
if (isLazyModule(moduleClass)) {
|
|
236
|
+
this.lazyModules.push(moduleClass);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const metadata = getMetadata<ModuleMetadata>(moduleClass, "module") ?? {};
|
|
241
|
+
|
|
242
|
+
// Process imports first (so dependencies are available)
|
|
243
|
+
if (metadata.imports) {
|
|
244
|
+
for (const importedModule of metadata.imports) {
|
|
245
|
+
this.processModule(importedModule);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Add providers
|
|
250
|
+
if (metadata.providers) {
|
|
251
|
+
this.providers.push(...metadata.providers);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Add controllers
|
|
255
|
+
if (metadata.controllers) {
|
|
256
|
+
this.controllers.push(...metadata.controllers);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get all collected providers
|
|
262
|
+
*/
|
|
263
|
+
getProviders(): Provider[] {
|
|
264
|
+
return [...this.providers];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get all collected controllers
|
|
269
|
+
*/
|
|
270
|
+
getControllers(): Constructor[] {
|
|
271
|
+
return [...this.controllers];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get all lazy modules that were skipped during initial processing
|
|
276
|
+
*/
|
|
277
|
+
getLazyModules(): Constructor[] {
|
|
278
|
+
return [...this.lazyModules];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ============= Route Metadata for Guards and Pipes =============
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Metadata stored for each route to support guards
|
|
286
|
+
*/
|
|
287
|
+
interface RouteGuardMetadata {
|
|
288
|
+
controllerClass: Constructor;
|
|
289
|
+
handlerName: string | symbol;
|
|
290
|
+
classGuards: Guard[];
|
|
291
|
+
methodGuards: Guard[];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Metadata stored for each route to support pipes
|
|
296
|
+
*/
|
|
297
|
+
interface RoutePipeMetadata {
|
|
298
|
+
controllerClass: Constructor;
|
|
299
|
+
handlerName: string | symbol;
|
|
300
|
+
parameterPipes: ParameterPipeMetadata[];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Metadata stored for each route to support filters
|
|
305
|
+
*/
|
|
306
|
+
interface RouteFilterMetadata {
|
|
307
|
+
controllerClass: Constructor;
|
|
308
|
+
handlerName: string | symbol;
|
|
309
|
+
classFilters: Filter[];
|
|
310
|
+
methodFilters: Filter[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Metadata stored for each route to support interceptors
|
|
315
|
+
*/
|
|
316
|
+
interface RouteInterceptorMetadata {
|
|
317
|
+
controllerClass: Constructor;
|
|
318
|
+
handlerName: string | symbol;
|
|
319
|
+
classInterceptors: Interceptor[];
|
|
320
|
+
methodInterceptors: Interceptor[];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ============= Application Class =============
|
|
324
|
+
|
|
325
|
+
export class Application {
|
|
326
|
+
container: Container;
|
|
327
|
+
router: Router;
|
|
328
|
+
private appModule: AppModule;
|
|
329
|
+
private lifecycleManager: LifecycleHookManager;
|
|
330
|
+
private shutdownHandler: ShutdownSignalHandler;
|
|
331
|
+
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
332
|
+
private providerInstances: Map<Token, unknown> = new Map();
|
|
333
|
+
private controllerInstances: unknown[] = [];
|
|
334
|
+
private isInitialized = false;
|
|
335
|
+
private isShuttingDown = false;
|
|
336
|
+
private globalGuards: Guard[] = [];
|
|
337
|
+
private globalPipes: Pipe[] = [];
|
|
338
|
+
private globalFilters: Filter[] = [];
|
|
339
|
+
private globalInterceptors: Interceptor[] = [];
|
|
340
|
+
private routeGuardMetadata: Map<string, RouteGuardMetadata> = new Map();
|
|
341
|
+
private routePipeMetadata: Map<string, RoutePipeMetadata> = new Map();
|
|
342
|
+
private routeFilterMetadata: Map<string, RouteFilterMetadata> = new Map();
|
|
343
|
+
private routeInterceptorMetadata: Map<string, RouteInterceptorMetadata> = new Map();
|
|
344
|
+
private moduleLoader: ModuleLoader;
|
|
345
|
+
private loadedLazyModules = new Set<Constructor>();
|
|
346
|
+
|
|
347
|
+
constructor(moduleClass: Constructor) {
|
|
348
|
+
this.container = new Container();
|
|
349
|
+
this.router = new Router();
|
|
350
|
+
this.appModule = new AppModule(moduleClass);
|
|
351
|
+
this.lifecycleManager = new LifecycleHookManager();
|
|
352
|
+
this.shutdownHandler = new ShutdownSignalHandler();
|
|
353
|
+
this.moduleLoader = new ModuleLoader(this.container);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Add global guards that apply to all routes
|
|
358
|
+
* Global guards run before controller and method guards
|
|
359
|
+
*
|
|
360
|
+
* @param guards - Guards to add
|
|
361
|
+
* @returns this (for chaining)
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```typescript
|
|
365
|
+
* const app = createApp(AppModule);
|
|
366
|
+
* app.useGlobalGuards(AuthGuard);
|
|
367
|
+
* await app.listen(3000);
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
useGlobalGuards(...guards: Guard[]): this {
|
|
371
|
+
this.globalGuards.push(...guards);
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Add global pipes that apply to all parameters
|
|
377
|
+
* Global pipes run before parameter decorator pipes
|
|
378
|
+
*
|
|
379
|
+
* @param pipes - Pipes to add
|
|
380
|
+
* @returns this (for chaining)
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```typescript
|
|
384
|
+
* const app = createApp(AppModule);
|
|
385
|
+
* app.useGlobalPipes(new ValidationPipe());
|
|
386
|
+
* await app.listen(3000);
|
|
387
|
+
* ```
|
|
388
|
+
*/
|
|
389
|
+
useGlobalPipes(...pipes: Pipe[]): this {
|
|
390
|
+
this.globalPipes.push(...pipes);
|
|
391
|
+
return this;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Add global filters that apply to all routes
|
|
396
|
+
* Global filters run after controller and method filters
|
|
397
|
+
*
|
|
398
|
+
* @param filters - Filters to add
|
|
399
|
+
* @returns this (for chaining)
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* ```typescript
|
|
403
|
+
* const app = createApp(AppModule);
|
|
404
|
+
* app.useGlobalFilters(new AllExceptionsFilter());
|
|
405
|
+
* await app.listen(3000);
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
useGlobalFilters(...filters: Filter[]): this {
|
|
409
|
+
this.globalFilters.push(...filters);
|
|
410
|
+
return this;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Add global interceptors that apply to all routes
|
|
415
|
+
* Global interceptors run before controller and method interceptors
|
|
416
|
+
*
|
|
417
|
+
* @param interceptors - Interceptors to add
|
|
418
|
+
* @returns this (for chaining)
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```typescript
|
|
422
|
+
* const app = createApp(AppModule);
|
|
423
|
+
* app.useGlobalInterceptors(new LoggingInterceptor());
|
|
424
|
+
* await app.listen(3000);
|
|
425
|
+
* ```
|
|
426
|
+
*/
|
|
427
|
+
useGlobalInterceptors(...interceptors: Interceptor[]): this {
|
|
428
|
+
this.globalInterceptors.push(...interceptors);
|
|
429
|
+
return this;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Register all providers in the container
|
|
434
|
+
*/
|
|
435
|
+
private registerProviders(): void {
|
|
436
|
+
const providers = this.appModule.getProviders();
|
|
437
|
+
this.container.registerAll(providers);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Resolve all providers and register them with lifecycle manager
|
|
442
|
+
*/
|
|
443
|
+
private async resolveProviders(): Promise<void> {
|
|
444
|
+
const providers = this.appModule.getProviders();
|
|
445
|
+
|
|
446
|
+
for (const provider of providers) {
|
|
447
|
+
const token = provider.token;
|
|
448
|
+
const instance = this.container.resolve(token as import("../container").Token);
|
|
449
|
+
this.providerInstances.set(token as import("../container").Token, instance);
|
|
450
|
+
this.lifecycleManager.registerInstance(instance);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Register all controllers and their routes
|
|
456
|
+
*/
|
|
457
|
+
private registerControllers(): void {
|
|
458
|
+
const controllers = this.appModule.getControllers();
|
|
459
|
+
|
|
460
|
+
for (const controllerClass of controllers) {
|
|
461
|
+
const instance = this.registerController(controllerClass);
|
|
462
|
+
this.controllerInstances.push(instance);
|
|
463
|
+
this.lifecycleManager.registerInstance(instance);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Register a single controller and its routes
|
|
469
|
+
*/
|
|
470
|
+
private registerController(controllerClass: Constructor): unknown {
|
|
471
|
+
const basePath = getMetadata<string>(controllerClass, "path") ?? "";
|
|
472
|
+
const routes =
|
|
473
|
+
getPrototypeMetadata<
|
|
474
|
+
Array<{ method: string; path: string; handler: string | symbol }>
|
|
475
|
+
>(controllerClass.prototype, "routes") ?? [];
|
|
476
|
+
|
|
477
|
+
// Create controller instance
|
|
478
|
+
const injectTokens =
|
|
479
|
+
getInjectTokens<Array<Token | ForwardRef<Token>>>(controllerClass, "inject:tokens") ?? [];
|
|
480
|
+
const deps = injectTokens.map((tokenOrRef) => {
|
|
481
|
+
// Resolve forward reference if needed
|
|
482
|
+
const token = isForwardRef(tokenOrRef) ? resolveForwardRef(tokenOrRef) : tokenOrRef;
|
|
483
|
+
return this.container.resolve(token);
|
|
484
|
+
});
|
|
485
|
+
const instance = new controllerClass(...deps);
|
|
486
|
+
|
|
487
|
+
// Get class-level guards
|
|
488
|
+
const classGuards = getClassGuards(controllerClass) ?? [];
|
|
489
|
+
|
|
490
|
+
// Get class-level interceptors
|
|
491
|
+
const classInterceptors = getClassInterceptors(controllerClass) ?? [];
|
|
492
|
+
|
|
493
|
+
// Register routes
|
|
494
|
+
for (const route of routes) {
|
|
495
|
+
const fullPath = basePath + route.path;
|
|
496
|
+
const handler = (instance as Record<string | symbol, RouteHandler>)[
|
|
497
|
+
route.handler
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
if (typeof handler === "function") {
|
|
501
|
+
const method = route.method.toLowerCase();
|
|
502
|
+
const routerMethod = method as
|
|
503
|
+
| "get"
|
|
504
|
+
| "post"
|
|
505
|
+
| "put"
|
|
506
|
+
| "patch"
|
|
507
|
+
| "delete"
|
|
508
|
+
| "head"
|
|
509
|
+
| "options";
|
|
510
|
+
|
|
511
|
+
// Get method-level guards
|
|
512
|
+
const methodGuards = getMethodGuards(controllerClass.prototype, route.handler) ?? [];
|
|
513
|
+
|
|
514
|
+
// Get method-level pipes
|
|
515
|
+
const parameterPipes = getMethodPipes(controllerClass.prototype, route.handler) ?? [];
|
|
516
|
+
|
|
517
|
+
// Get class-level filters
|
|
518
|
+
const classFilters = getClassFilters(controllerClass) ?? [];
|
|
519
|
+
|
|
520
|
+
// Get method-level filters
|
|
521
|
+
const methodFilters = getMethodFilters(controllerClass.prototype, route.handler) ?? [];
|
|
522
|
+
|
|
523
|
+
// Get method-level interceptors
|
|
524
|
+
const methodInterceptors = getMethodInterceptors(controllerClass.prototype, route.handler) ?? [];
|
|
525
|
+
|
|
526
|
+
// Store guard metadata for this route
|
|
527
|
+
const routeKey = `${method.toUpperCase()}:${fullPath}`;
|
|
528
|
+
this.routeGuardMetadata.set(routeKey, {
|
|
529
|
+
controllerClass,
|
|
530
|
+
handlerName: route.handler,
|
|
531
|
+
classGuards,
|
|
532
|
+
methodGuards,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Store pipe metadata for this route
|
|
536
|
+
if (parameterPipes.length > 0) {
|
|
537
|
+
this.routePipeMetadata.set(routeKey, {
|
|
538
|
+
controllerClass,
|
|
539
|
+
handlerName: route.handler,
|
|
540
|
+
parameterPipes,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Store filter metadata for this route
|
|
545
|
+
if (classFilters.length > 0 || methodFilters.length > 0) {
|
|
546
|
+
this.routeFilterMetadata.set(routeKey, {
|
|
547
|
+
controllerClass,
|
|
548
|
+
handlerName: route.handler,
|
|
549
|
+
classFilters,
|
|
550
|
+
methodFilters,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Store interceptor metadata for this route
|
|
555
|
+
if (classInterceptors.length > 0 || methodInterceptors.length > 0) {
|
|
556
|
+
this.routeInterceptorMetadata.set(routeKey, {
|
|
557
|
+
controllerClass,
|
|
558
|
+
handlerName: route.handler,
|
|
559
|
+
classInterceptors,
|
|
560
|
+
methodInterceptors,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (
|
|
565
|
+
routerMethod in this.router &&
|
|
566
|
+
typeof (this.router as unknown as Record<string, unknown>)[
|
|
567
|
+
routerMethod
|
|
568
|
+
] === "function"
|
|
569
|
+
) {
|
|
570
|
+
(
|
|
571
|
+
this.router as unknown as Record<
|
|
572
|
+
string,
|
|
573
|
+
(p: string, h: RouteHandler) => void
|
|
574
|
+
>
|
|
575
|
+
)[routerMethod](fullPath, ((context: Context): unknown =>
|
|
576
|
+
handler.call(instance, context)) as RouteHandler);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return instance;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Initialize the application
|
|
586
|
+
*
|
|
587
|
+
* Execution order:
|
|
588
|
+
* 1. Module Registration
|
|
589
|
+
* 2. Provider Resolution
|
|
590
|
+
* 3. onModuleInit() for each provider
|
|
591
|
+
* 4. Controller Registration
|
|
592
|
+
* 5. onApplicationBootstrap() for each provider
|
|
593
|
+
*/
|
|
594
|
+
async init(): Promise<void> {
|
|
595
|
+
if (this.isInitialized) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// 1. Register providers in container
|
|
600
|
+
this.registerProviders();
|
|
601
|
+
|
|
602
|
+
// Register the ModuleLoader service
|
|
603
|
+
this.container.register({
|
|
604
|
+
token: MODULE_LOADER_TOKEN,
|
|
605
|
+
useValue: this.moduleLoader,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// 2. Resolve providers and register with lifecycle manager
|
|
609
|
+
await this.resolveProviders();
|
|
610
|
+
|
|
611
|
+
// 3. Execute onModuleInit hooks
|
|
612
|
+
await this.lifecycleManager.executeOnModuleInit();
|
|
613
|
+
|
|
614
|
+
// 4. Register controllers
|
|
615
|
+
this.registerControllers();
|
|
616
|
+
|
|
617
|
+
// 5. Execute onApplicationBootstrap hooks
|
|
618
|
+
await this.lifecycleManager.executeOnApplicationBootstrap();
|
|
619
|
+
|
|
620
|
+
// Setup callback for lazy module loading
|
|
621
|
+
this.moduleLoader.setOnModuleLoadCallback(
|
|
622
|
+
async (moduleClass: Constructor) => {
|
|
623
|
+
await this.loadLazyModule(moduleClass);
|
|
624
|
+
},
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
this.isInitialized = true;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Load a lazy module dynamically
|
|
632
|
+
* This is called by the ModuleLoader service when a lazy module is loaded
|
|
633
|
+
*/
|
|
634
|
+
private async loadLazyModule(moduleClass: Constructor): Promise<void> {
|
|
635
|
+
if (this.loadedLazyModules.has(moduleClass)) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const metadata = getMetadata<ModuleMetadata>(moduleClass, "module") ?? {};
|
|
640
|
+
|
|
641
|
+
// Register and resolve providers
|
|
642
|
+
if (metadata.providers) {
|
|
643
|
+
for (const provider of metadata.providers) {
|
|
644
|
+
this.container.register(provider);
|
|
645
|
+
const instance = this.container.resolve(provider.token as import("../container").Token);
|
|
646
|
+
this.providerInstances.set(provider.token as import("../container").Token, instance);
|
|
647
|
+
this.lifecycleManager.registerInstance(instance);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Execute onModuleInit for new providers
|
|
651
|
+
await this.lifecycleManager.executeOnModuleInit();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Register controllers
|
|
655
|
+
if (metadata.controllers) {
|
|
656
|
+
for (const controllerClass of metadata.controllers) {
|
|
657
|
+
const instance = this.registerController(controllerClass);
|
|
658
|
+
this.controllerInstances.push(instance);
|
|
659
|
+
this.lifecycleManager.registerInstance(instance);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Execute onApplicationBootstrap for new instances
|
|
664
|
+
await this.lifecycleManager.executeOnApplicationBootstrap();
|
|
665
|
+
|
|
666
|
+
this.loadedLazyModules.add(moduleClass);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Get the ModuleLoader service for loading lazy modules
|
|
671
|
+
*/
|
|
672
|
+
getModuleLoader(): ModuleLoader {
|
|
673
|
+
return this.moduleLoader;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Setup graceful shutdown handlers
|
|
678
|
+
*/
|
|
679
|
+
private setupShutdownHandlers(): void {
|
|
680
|
+
this.shutdownHandler.onSignal(async (signal) => {
|
|
681
|
+
await this.shutdown(signal);
|
|
682
|
+
});
|
|
683
|
+
this.shutdownHandler.startListening();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Perform graceful shutdown
|
|
688
|
+
*
|
|
689
|
+
* Execution order:
|
|
690
|
+
* 1. Stop accepting new requests
|
|
691
|
+
* 2. beforeApplicationShutdown(signal)
|
|
692
|
+
* 3. Drain existing requests
|
|
693
|
+
* 4. onModuleDestroy() for each provider
|
|
694
|
+
* 5. onApplicationShutdown(signal)
|
|
695
|
+
*/
|
|
696
|
+
async shutdown(signal?: string): Promise<void> {
|
|
697
|
+
if (this.isShuttingDown) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
this.isShuttingDown = true;
|
|
701
|
+
|
|
702
|
+
console.log("Starting graceful shutdown...");
|
|
703
|
+
|
|
704
|
+
// 1. Stop accepting new requests
|
|
705
|
+
if (this.server) {
|
|
706
|
+
this.server.stop();
|
|
707
|
+
console.log("Server stopped accepting new connections");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 2. Execute beforeApplicationShutdown hooks
|
|
711
|
+
await this.lifecycleManager.executeBeforeApplicationShutdown(signal);
|
|
712
|
+
|
|
713
|
+
// 3. Wait for existing requests to drain (with timeout)
|
|
714
|
+
// In a real implementation, you would track active requests
|
|
715
|
+
// For now, we just add a small delay
|
|
716
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
717
|
+
|
|
718
|
+
// 4. Execute onModuleDestroy hooks
|
|
719
|
+
await this.lifecycleManager.executeOnModuleDestroy();
|
|
720
|
+
|
|
721
|
+
// 5. Execute onApplicationShutdown hooks
|
|
722
|
+
await this.lifecycleManager.executeOnApplicationShutdown(signal);
|
|
723
|
+
|
|
724
|
+
console.log("Graceful shutdown complete");
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Start the HTTP server
|
|
729
|
+
*/
|
|
730
|
+
async listen(port = 3000, hostname = "localhost"): Promise<void> {
|
|
731
|
+
// Initialize if not already done
|
|
732
|
+
await this.init();
|
|
733
|
+
|
|
734
|
+
// Setup shutdown handlers
|
|
735
|
+
this.setupShutdownHandlers();
|
|
736
|
+
|
|
737
|
+
const { Context } = await import("../context");
|
|
738
|
+
const { compose } = await import("../middleware");
|
|
739
|
+
|
|
740
|
+
this.server = Bun.serve({
|
|
741
|
+
port,
|
|
742
|
+
hostname,
|
|
743
|
+
fetch: async (request: Request) => {
|
|
744
|
+
// Reject new requests during shutdown
|
|
745
|
+
if (this.isShuttingDown) {
|
|
746
|
+
return new Response("Service Unavailable", { status: 503 });
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const url = new URL(request.url);
|
|
750
|
+
const match = this.router.match(request.method as "GET", url.pathname);
|
|
751
|
+
|
|
752
|
+
if (!match) {
|
|
753
|
+
return new Response("Not Found", { status: 404 });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Create context
|
|
757
|
+
const context = new Context(request, match.params);
|
|
758
|
+
|
|
759
|
+
// Execute onBeforeRequest hooks
|
|
760
|
+
try {
|
|
761
|
+
await this.lifecycleManager.executeOnBeforeRequest(context);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
console.error("Error in onBeforeRequest hook:", error);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Execute guards (before interceptors and pipes)
|
|
767
|
+
const routeKey = `${request.method}:${url.pathname}`;
|
|
768
|
+
const guardMetadata = this.routeGuardMetadata.get(routeKey);
|
|
769
|
+
|
|
770
|
+
if (guardMetadata || this.globalGuards.length > 0) {
|
|
771
|
+
const guardsPassed = await executeGuards(context, {
|
|
772
|
+
globalGuards: this.globalGuards,
|
|
773
|
+
classGuards: guardMetadata?.classGuards ?? [],
|
|
774
|
+
methodGuards: guardMetadata?.methodGuards ?? [],
|
|
775
|
+
resolveGuard: (guard: Guard) => {
|
|
776
|
+
// Try to resolve from container if it's a token
|
|
777
|
+
if (typeof guard === "object" && guard !== null && !("canActivate" in guard)) {
|
|
778
|
+
try {
|
|
779
|
+
return this.container.resolve(guard as Token) as CanActivate;
|
|
780
|
+
} catch {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return null;
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
if (!guardsPassed) {
|
|
789
|
+
return createForbiddenResponse();
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Get interceptor metadata
|
|
794
|
+
const interceptorMetadata = this.routeInterceptorMetadata.get(routeKey);
|
|
795
|
+
|
|
796
|
+
// Create the handler function that executes pipes and middleware
|
|
797
|
+
const executeHandler = async (): Promise<Response> => {
|
|
798
|
+
// Execute pipes (after guards, before handler)
|
|
799
|
+
const pipeMetadata = this.routePipeMetadata.get(routeKey);
|
|
800
|
+
if (pipeMetadata || this.globalPipes.length > 0) {
|
|
801
|
+
try {
|
|
802
|
+
// Process each parameter with pipes
|
|
803
|
+
const params = pipeMetadata?.parameterPipes ?? [];
|
|
804
|
+
for (const paramMeta of params) {
|
|
805
|
+
// Extract the initial value for this parameter
|
|
806
|
+
const initialValue = await extractParameterValue(context, paramMeta);
|
|
807
|
+
|
|
808
|
+
// Create pipe context
|
|
809
|
+
const pipeContext: PipeContext = {
|
|
810
|
+
context,
|
|
811
|
+
metadata: {
|
|
812
|
+
index: paramMeta.index,
|
|
813
|
+
name: paramMeta.key,
|
|
814
|
+
decorator: paramMeta.decorator,
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// Execute pipes for this parameter
|
|
819
|
+
const transformedValue = await executePipes(initialValue, pipeContext, {
|
|
820
|
+
globalPipes: this.globalPipes,
|
|
821
|
+
parameterPipes: paramMeta.pipes,
|
|
822
|
+
resolvePipe: (pipe: Pipe) => {
|
|
823
|
+
// Try to resolve from container if it's a token
|
|
824
|
+
if (typeof pipe === "object" && pipe !== null && !("transform" in pipe)) {
|
|
825
|
+
try {
|
|
826
|
+
return this.container.resolve(pipe as Token) as PipeTransform;
|
|
827
|
+
} catch {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return null;
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// Store the transformed value in context for handler access
|
|
836
|
+
context.set(`pipe:param:${paramMeta.index}`, transformedValue);
|
|
837
|
+
}
|
|
838
|
+
} catch (error) {
|
|
839
|
+
// Pipe transformation failed - return 400 Bad Request
|
|
840
|
+
if (error instanceof Error) {
|
|
841
|
+
return createBadRequestResponse(error);
|
|
842
|
+
}
|
|
843
|
+
return createBadRequestResponse(new Error("Pipe transformation failed"));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Execute middleware and handler
|
|
848
|
+
const pipeline = compose((match.middleware ?? []) as import("../middleware").Middleware[]);
|
|
849
|
+
return pipeline(context, match.handler as import("../middleware").Handler);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// Execute interceptors wrapping around the handler
|
|
853
|
+
// Interceptors run after guards, before pipes
|
|
854
|
+
try {
|
|
855
|
+
const response = await executeInterceptors(context, executeHandler, {
|
|
856
|
+
globalInterceptors: this.globalInterceptors,
|
|
857
|
+
classInterceptors: interceptorMetadata?.classInterceptors ?? [],
|
|
858
|
+
methodInterceptors: interceptorMetadata?.methodInterceptors ?? [],
|
|
859
|
+
resolveInterceptor: (interceptor: Interceptor): NestInterceptor | InterceptorFn | null => {
|
|
860
|
+
// Try to resolve from container if it's a token
|
|
861
|
+
if (typeof interceptor === "object" && interceptor !== null && !isNestInterceptor(interceptor) && !isInterceptorFn(interceptor)) {
|
|
862
|
+
try {
|
|
863
|
+
return this.container.resolve(interceptor as Token) as NestInterceptor;
|
|
864
|
+
} catch {
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// Try to instantiate if it's a class constructor
|
|
869
|
+
if (typeof interceptor === "function" && !isInterceptorFn(interceptor)) {
|
|
870
|
+
try {
|
|
871
|
+
const Constructor = interceptor as new () => NestInterceptor;
|
|
872
|
+
const instance = new Constructor();
|
|
873
|
+
if (isNestInterceptor(instance)) {
|
|
874
|
+
return instance;
|
|
875
|
+
}
|
|
876
|
+
} catch {
|
|
877
|
+
// Cannot instantiate
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return null;
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// Execute onAfterRequest hooks
|
|
885
|
+
try {
|
|
886
|
+
await this.lifecycleManager.executeOnAfterRequest(context, response as Response);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
console.error("Error in onAfterRequest hook:", error);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return response as Response;
|
|
892
|
+
} catch (error) {
|
|
893
|
+
// Execute onRequestError hooks
|
|
894
|
+
try {
|
|
895
|
+
await this.lifecycleManager.executeOnRequestError(
|
|
896
|
+
context,
|
|
897
|
+
error as Error,
|
|
898
|
+
);
|
|
899
|
+
} catch (hookError) {
|
|
900
|
+
console.error("Error in onRequestError hook:", hookError);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Handle exception with filters
|
|
904
|
+
return this.handleException(error as Error, context, routeKey);
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
console.log(`Server running at http://${hostname}:${port}`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Handle request directly (for testing)
|
|
914
|
+
*/
|
|
915
|
+
async handle(request: Request): Promise<Response> {
|
|
916
|
+
const { Context } = await import("../context");
|
|
917
|
+
const { compose } = await import("../middleware");
|
|
918
|
+
|
|
919
|
+
const url = new URL(request.url);
|
|
920
|
+
const match = this.router.match(request.method as "GET", url.pathname);
|
|
921
|
+
|
|
922
|
+
if (!match) {
|
|
923
|
+
return new Response("Not Found", { status: 404 });
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const context = new Context(request, match.params);
|
|
927
|
+
|
|
928
|
+
// Execute guards (before interceptors and pipes)
|
|
929
|
+
const routeKey = `${request.method}:${url.pathname}`;
|
|
930
|
+
const guardMetadata = this.routeGuardMetadata.get(routeKey);
|
|
931
|
+
|
|
932
|
+
if (guardMetadata || this.globalGuards.length > 0) {
|
|
933
|
+
const guardsPassed = await executeGuards(context, {
|
|
934
|
+
globalGuards: this.globalGuards,
|
|
935
|
+
classGuards: guardMetadata?.classGuards ?? [],
|
|
936
|
+
methodGuards: guardMetadata?.methodGuards ?? [],
|
|
937
|
+
resolveGuard: (guard: Guard) => {
|
|
938
|
+
// Try to resolve from container if it's a token
|
|
939
|
+
if (typeof guard === "object" && guard !== null && !("canActivate" in guard)) {
|
|
940
|
+
try {
|
|
941
|
+
return this.container.resolve(guard as Token) as CanActivate;
|
|
942
|
+
} catch {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return null;
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
if (!guardsPassed) {
|
|
951
|
+
return createForbiddenResponse();
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Get interceptor metadata
|
|
956
|
+
const interceptorMetadata = this.routeInterceptorMetadata.get(routeKey);
|
|
957
|
+
|
|
958
|
+
// Create the handler function that executes pipes and middleware
|
|
959
|
+
const executeHandler = async (): Promise<Response> => {
|
|
960
|
+
// Execute pipes (after guards, before handler)
|
|
961
|
+
const pipeMetadata = this.routePipeMetadata.get(routeKey);
|
|
962
|
+
if (pipeMetadata || this.globalPipes.length > 0) {
|
|
963
|
+
try {
|
|
964
|
+
// Process each parameter with pipes
|
|
965
|
+
const params = pipeMetadata?.parameterPipes ?? [];
|
|
966
|
+
for (const paramMeta of params) {
|
|
967
|
+
// Extract the initial value for this parameter
|
|
968
|
+
const initialValue = await extractParameterValue(context, paramMeta);
|
|
969
|
+
|
|
970
|
+
// Create pipe context
|
|
971
|
+
const pipeContext: PipeContext = {
|
|
972
|
+
context,
|
|
973
|
+
metadata: {
|
|
974
|
+
index: paramMeta.index,
|
|
975
|
+
name: paramMeta.key,
|
|
976
|
+
decorator: paramMeta.decorator,
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
// Execute pipes for this parameter
|
|
981
|
+
const transformedValue = await executePipes(initialValue, pipeContext, {
|
|
982
|
+
globalPipes: this.globalPipes,
|
|
983
|
+
parameterPipes: paramMeta.pipes,
|
|
984
|
+
resolvePipe: (pipe: Pipe) => {
|
|
985
|
+
// Try to resolve from container if it's a token
|
|
986
|
+
if (typeof pipe === "object" && pipe !== null && !("transform" in pipe)) {
|
|
987
|
+
try {
|
|
988
|
+
return this.container.resolve(pipe as Token) as PipeTransform;
|
|
989
|
+
} catch {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// Store the transformed value in context for handler access
|
|
998
|
+
context.set(`pipe:param:${paramMeta.index}`, transformedValue);
|
|
999
|
+
}
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
// Pipe transformation failed - return 400 Bad Request
|
|
1002
|
+
if (error instanceof Error) {
|
|
1003
|
+
return createBadRequestResponse(error);
|
|
1004
|
+
}
|
|
1005
|
+
return createBadRequestResponse(new Error("Pipe transformation failed"));
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Execute middleware and handler
|
|
1010
|
+
const pipeline = compose((match.middleware ?? []) as import("../middleware").Middleware[]);
|
|
1011
|
+
return pipeline(context, match.handler as import("../middleware").Handler);
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
// Execute interceptors wrapping around the handler
|
|
1015
|
+
// Interceptors run after guards, before pipes
|
|
1016
|
+
try {
|
|
1017
|
+
const response = await executeInterceptors(context, executeHandler, {
|
|
1018
|
+
globalInterceptors: this.globalInterceptors,
|
|
1019
|
+
classInterceptors: interceptorMetadata?.classInterceptors ?? [],
|
|
1020
|
+
methodInterceptors: interceptorMetadata?.methodInterceptors ?? [],
|
|
1021
|
+
resolveInterceptor: (interceptor: Interceptor): NestInterceptor | InterceptorFn | null => {
|
|
1022
|
+
// Try to resolve from container if it's a token
|
|
1023
|
+
if (typeof interceptor === "object" && interceptor !== null && !isNestInterceptor(interceptor) && !isInterceptorFn(interceptor)) {
|
|
1024
|
+
try {
|
|
1025
|
+
return this.container.resolve(interceptor as Token) as NestInterceptor;
|
|
1026
|
+
} catch {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
// Try to instantiate if it's a class constructor
|
|
1031
|
+
if (typeof interceptor === "function" && !isInterceptorFn(interceptor)) {
|
|
1032
|
+
try {
|
|
1033
|
+
const Constructor = interceptor as new () => NestInterceptor;
|
|
1034
|
+
const instance = new Constructor();
|
|
1035
|
+
if (isNestInterceptor(instance)) {
|
|
1036
|
+
return instance;
|
|
1037
|
+
}
|
|
1038
|
+
} catch {
|
|
1039
|
+
// Cannot instantiate
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return null;
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
return response as Response;
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
// Handle exception with filters
|
|
1049
|
+
return this.handleException(error as Error, context, routeKey);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Get the lifecycle manager for this application
|
|
1055
|
+
*/
|
|
1056
|
+
getLifecycleManager(): LifecycleHookManager {
|
|
1057
|
+
return this.lifecycleManager;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Check if the application is shutting down
|
|
1062
|
+
*/
|
|
1063
|
+
isShuttingDownNow(): boolean {
|
|
1064
|
+
return this.isShuttingDown;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Handle an exception using the filters system
|
|
1069
|
+
* Filters are checked in order: method → class → global
|
|
1070
|
+
*/
|
|
1071
|
+
private async handleException(
|
|
1072
|
+
exception: Error,
|
|
1073
|
+
context: Context,
|
|
1074
|
+
routeKey: string,
|
|
1075
|
+
): Promise<Response> {
|
|
1076
|
+
const filterMetadata = this.routeFilterMetadata.get(routeKey);
|
|
1077
|
+
|
|
1078
|
+
return findAndExecuteFilter(exception, context, {
|
|
1079
|
+
globalFilters: this.globalFilters,
|
|
1080
|
+
classFilters: filterMetadata?.classFilters ?? [],
|
|
1081
|
+
methodFilters: filterMetadata?.methodFilters ?? [],
|
|
1082
|
+
resolveFilter: (filter: Filter): ExceptionFilter | null => {
|
|
1083
|
+
// Try to resolve from container if it's a token
|
|
1084
|
+
if (typeof filter === "object" && filter !== null && !isExceptionFilter(filter)) {
|
|
1085
|
+
try {
|
|
1086
|
+
return this.container.resolve(filter as Token) as ExceptionFilter;
|
|
1087
|
+
} catch {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
// Try to instantiate if it's a class constructor
|
|
1092
|
+
if (typeof filter === "function" && !isFilterFn(filter)) {
|
|
1093
|
+
try {
|
|
1094
|
+
const Constructor = filter as new () => ExceptionFilter;
|
|
1095
|
+
const instance = new Constructor();
|
|
1096
|
+
if (isExceptionFilter(instance)) {
|
|
1097
|
+
return instance;
|
|
1098
|
+
}
|
|
1099
|
+
} catch {
|
|
1100
|
+
// Cannot instantiate
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return null;
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Create an application from a module
|
|
1111
|
+
*/
|
|
1112
|
+
export function createApp(moduleClass: Constructor): Application {
|
|
1113
|
+
return new Application(moduleClass);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Re-export lifecycle types and utilities
|
|
1117
|
+
export {
|
|
1118
|
+
LifecycleHookManager,
|
|
1119
|
+
ShutdownSignalHandler,
|
|
1120
|
+
OnModuleInit,
|
|
1121
|
+
OnApplicationBootstrap,
|
|
1122
|
+
OnModuleDestroy,
|
|
1123
|
+
BeforeApplicationShutdown,
|
|
1124
|
+
OnApplicationShutdown,
|
|
1125
|
+
OnBeforeRequest,
|
|
1126
|
+
OnAfterRequest,
|
|
1127
|
+
OnRequestError,
|
|
1128
|
+
ApplicationLifecycle,
|
|
1129
|
+
RequestLifecycle,
|
|
1130
|
+
FullLifecycle,
|
|
1131
|
+
isOnModuleInit,
|
|
1132
|
+
isOnApplicationBootstrap,
|
|
1133
|
+
isOnModuleDestroy,
|
|
1134
|
+
isBeforeApplicationShutdown,
|
|
1135
|
+
isOnApplicationShutdown,
|
|
1136
|
+
isOnBeforeRequest,
|
|
1137
|
+
isOnAfterRequest,
|
|
1138
|
+
isOnRequestError,
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
// Re-export guards types and utilities
|
|
1142
|
+
export {
|
|
1143
|
+
type CanActivate,
|
|
1144
|
+
type GuardFn,
|
|
1145
|
+
type Guard,
|
|
1146
|
+
type User,
|
|
1147
|
+
UseGuards,
|
|
1148
|
+
AuthGuard,
|
|
1149
|
+
RolesGuard,
|
|
1150
|
+
Roles,
|
|
1151
|
+
getClassGuards,
|
|
1152
|
+
getMethodGuards,
|
|
1153
|
+
getMethodRoles,
|
|
1154
|
+
executeGuards,
|
|
1155
|
+
createForbiddenResponse,
|
|
1156
|
+
} from "./guards";
|
|
1157
|
+
|
|
1158
|
+
// Re-export pipes types and utilities
|
|
1159
|
+
export {
|
|
1160
|
+
type PipeTransform,
|
|
1161
|
+
type PipeFn,
|
|
1162
|
+
type Pipe,
|
|
1163
|
+
type PipeContext,
|
|
1164
|
+
type ParameterMetadata,
|
|
1165
|
+
type ParameterPipeMetadata,
|
|
1166
|
+
UsePipes,
|
|
1167
|
+
Body,
|
|
1168
|
+
Query,
|
|
1169
|
+
Param,
|
|
1170
|
+
ValidationPipe,
|
|
1171
|
+
ParseIntPipe,
|
|
1172
|
+
ParseFloatPipe,
|
|
1173
|
+
ParseBoolPipe,
|
|
1174
|
+
DefaultValuePipe,
|
|
1175
|
+
TrimPipe,
|
|
1176
|
+
ParseJsonPipe,
|
|
1177
|
+
ParseArrayPipe,
|
|
1178
|
+
getMethodPipes,
|
|
1179
|
+
executePipes,
|
|
1180
|
+
extractParameterValue,
|
|
1181
|
+
createBadRequestResponse,
|
|
1182
|
+
isPipeTransform,
|
|
1183
|
+
isPipeFn,
|
|
1184
|
+
} from "./pipes";
|
|
1185
|
+
|
|
1186
|
+
// Re-export filters types and utilities
|
|
1187
|
+
export {
|
|
1188
|
+
type ExceptionFilter,
|
|
1189
|
+
type FilterFn,
|
|
1190
|
+
type Filter,
|
|
1191
|
+
type ExecuteFiltersOptions,
|
|
1192
|
+
UseFilters,
|
|
1193
|
+
Catch,
|
|
1194
|
+
getClassFilters,
|
|
1195
|
+
getMethodFilters,
|
|
1196
|
+
getCatchType,
|
|
1197
|
+
canHandleException,
|
|
1198
|
+
isExceptionFilter,
|
|
1199
|
+
isFilterFn,
|
|
1200
|
+
executeFilter,
|
|
1201
|
+
findAndExecuteFilter,
|
|
1202
|
+
HttpExceptionFilter,
|
|
1203
|
+
ValidationFilter,
|
|
1204
|
+
NotFoundFilter,
|
|
1205
|
+
AllExceptionsFilter,
|
|
1206
|
+
createDefaultErrorResponse,
|
|
1207
|
+
createInternalErrorResponse,
|
|
1208
|
+
} from "./filters";
|
|
1209
|
+
|
|
1210
|
+
// Re-export interceptors types and utilities
|
|
1211
|
+
export {
|
|
1212
|
+
type NestInterceptor,
|
|
1213
|
+
type InterceptorFn,
|
|
1214
|
+
type Interceptor,
|
|
1215
|
+
type CallHandler,
|
|
1216
|
+
type TransformResponse,
|
|
1217
|
+
type InterceptorExecutorOptions,
|
|
1218
|
+
UseInterceptors,
|
|
1219
|
+
LoggingInterceptor,
|
|
1220
|
+
TransformInterceptor,
|
|
1221
|
+
TimeoutInterceptor,
|
|
1222
|
+
CacheInterceptor,
|
|
1223
|
+
HeaderInterceptor,
|
|
1224
|
+
getClassInterceptors,
|
|
1225
|
+
getMethodInterceptors,
|
|
1226
|
+
executeInterceptors,
|
|
1227
|
+
isNestInterceptor,
|
|
1228
|
+
isInterceptorFn,
|
|
1229
|
+
} from "./interceptors";
|
|
1230
|
+
|
|
1231
|
+
// Re-export lazy loading types and utilities
|
|
1232
|
+
export {
|
|
1233
|
+
type LazyModuleLoader,
|
|
1234
|
+
type Constructor as LazyModuleConstructor,
|
|
1235
|
+
type ModuleLoaderFn,
|
|
1236
|
+
type LazyModuleMetadata,
|
|
1237
|
+
type LoadModuleOptions,
|
|
1238
|
+
LazyModuleLoaderImpl,
|
|
1239
|
+
LazyModule,
|
|
1240
|
+
ModuleLoader,
|
|
1241
|
+
MODULE_LOADER_TOKEN,
|
|
1242
|
+
getLazyMetadata,
|
|
1243
|
+
isLazyModule,
|
|
1244
|
+
LazyModuleRegistry,
|
|
1245
|
+
createLazyLoader,
|
|
1246
|
+
areAllLazyModulesLoaded,
|
|
1247
|
+
getUnloadedLazyModules,
|
|
1248
|
+
} from "./lazy";
|
|
1249
|
+
|
|
1250
|
+
// Re-export forward reference utilities for circular dependencies
|
|
1251
|
+
export {
|
|
1252
|
+
type ForwardRef,
|
|
1253
|
+
forwardRef,
|
|
1254
|
+
isForwardRef,
|
|
1255
|
+
resolveForwardRef,
|
|
1256
|
+
} from "../container";
|