@onebun/core 0.1.23 → 0.2.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/package.json +8 -8
- package/src/application/application.test.ts +519 -32
- package/src/application/application.ts +342 -377
- package/src/decorators/decorators.test.ts +47 -33
- package/src/decorators/decorators.ts +12 -0
- package/src/docs-examples.test.ts +300 -1
- package/src/index.ts +2 -0
- package/src/module/controller.ts +7 -3
- package/src/queue/docs-examples.test.ts +86 -0
- package/src/service-client/service-client.test.ts +1 -1
- package/src/types.ts +18 -2
- package/src/validation/schemas.test.ts +0 -2
- package/src/websocket/ws-base-gateway.ts +2 -2
- package/src/websocket/ws-handler.ts +4 -3
- package/src/websocket/ws.types.ts +1 -1
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
type ApplicationOptions,
|
|
48
48
|
type HttpMethod,
|
|
49
49
|
type ModuleInstance,
|
|
50
|
+
type OneBunRequest,
|
|
50
51
|
type ParamMetadata,
|
|
51
52
|
ParamType,
|
|
52
53
|
type RouteMetadata,
|
|
@@ -113,6 +114,41 @@ function normalizePath(path: string): string {
|
|
|
113
114
|
return path.endsWith('/') ? path.slice(0, -1) : path;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Extract query parameters from URL, handling array notation and repeated keys.
|
|
119
|
+
*
|
|
120
|
+
* @param url - The URL to extract query parameters from
|
|
121
|
+
* @returns Record of parameter names to values (string for single, string[] for repeated/array notation)
|
|
122
|
+
* @example
|
|
123
|
+
* extractQueryParams(new URL('http://x.com/?a=1&b=2')) // { a: '1', b: '2' }
|
|
124
|
+
* extractQueryParams(new URL('http://x.com/?tag=a&tag=b')) // { tag: ['a', 'b'] }
|
|
125
|
+
* extractQueryParams(new URL('http://x.com/?tag[]=a')) // { tag: ['a'] }
|
|
126
|
+
*/
|
|
127
|
+
function extractQueryParams(url: URL): Record<string, string | string[]> {
|
|
128
|
+
const queryParams: Record<string, string | string[]> = {};
|
|
129
|
+
|
|
130
|
+
for (const [rawKey, value] of url.searchParams.entries()) {
|
|
131
|
+
// Handle array notation: tag[] -> tag (as array)
|
|
132
|
+
const isArrayNotation = rawKey.endsWith('[]');
|
|
133
|
+
const key = isArrayNotation ? rawKey.replace('[]', '') : rawKey;
|
|
134
|
+
|
|
135
|
+
const existing = queryParams[key];
|
|
136
|
+
if (existing !== undefined) {
|
|
137
|
+
// Handle multiple values with same key (e.g., ?tag=a&tag=b or ?tag[]=a&tag[]=b)
|
|
138
|
+
queryParams[key] = Array.isArray(existing)
|
|
139
|
+
? [...existing, value]
|
|
140
|
+
: [existing, value];
|
|
141
|
+
} else if (isArrayNotation) {
|
|
142
|
+
// Array notation always creates an array, even with single value
|
|
143
|
+
queryParams[key] = [value];
|
|
144
|
+
} else {
|
|
145
|
+
queryParams[key] = value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return queryParams;
|
|
150
|
+
}
|
|
151
|
+
|
|
116
152
|
/**
|
|
117
153
|
* Resolve port from options, environment variable, or default.
|
|
118
154
|
* Priority: explicit option > PORT env > default (3000)
|
|
@@ -152,12 +188,14 @@ function resolveHost(explicitHost: string | undefined): string {
|
|
|
152
188
|
* OneBun Application
|
|
153
189
|
*/
|
|
154
190
|
export class OneBunApplication {
|
|
155
|
-
private rootModule: ModuleInstance;
|
|
191
|
+
private rootModule: ModuleInstance | null = null;
|
|
156
192
|
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
157
193
|
private options: ApplicationOptions;
|
|
158
194
|
private logger: SyncLogger;
|
|
159
195
|
private config: IConfig<OneBunAppConfig>;
|
|
160
196
|
private configService: ConfigServiceImpl | null = null;
|
|
197
|
+
private moduleClass: new (...args: unknown[]) => object;
|
|
198
|
+
private loggerLayer: Layer.Layer<never, never, unknown>;
|
|
161
199
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
162
200
|
private metricsService: any = null;
|
|
163
201
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -176,6 +214,8 @@ export class OneBunApplication {
|
|
|
176
214
|
moduleClass: new (...args: unknown[]) => object,
|
|
177
215
|
options?: Partial<ApplicationOptions>,
|
|
178
216
|
) {
|
|
217
|
+
this.moduleClass = moduleClass;
|
|
218
|
+
|
|
179
219
|
// Resolve port and host with priority: explicit > env > default
|
|
180
220
|
this.options = {
|
|
181
221
|
port: resolvePort(options?.port),
|
|
@@ -194,7 +234,7 @@ export class OneBunApplication {
|
|
|
194
234
|
|
|
195
235
|
// Use provided logger layer, or create from options, or use default
|
|
196
236
|
// Priority: loggerLayer > loggerOptions > env variables > NODE_ENV defaults
|
|
197
|
-
|
|
237
|
+
this.loggerLayer = this.options.loggerLayer
|
|
198
238
|
?? (this.options.loggerOptions
|
|
199
239
|
? makeLoggerFromOptions(this.options.loggerOptions)
|
|
200
240
|
: makeLogger());
|
|
@@ -205,13 +245,14 @@ export class OneBunApplication {
|
|
|
205
245
|
Effect.map(LoggerService, (logger: Logger) =>
|
|
206
246
|
logger.child({ className: 'OneBunApplication' }),
|
|
207
247
|
),
|
|
208
|
-
loggerLayer,
|
|
209
|
-
),
|
|
248
|
+
this.loggerLayer,
|
|
249
|
+
) as Effect.Effect<Logger, never, never>,
|
|
210
250
|
) as Logger;
|
|
211
251
|
this.logger = createSyncLogger(effectLogger);
|
|
212
252
|
|
|
213
|
-
// Create configuration service if config
|
|
214
|
-
|
|
253
|
+
// Create configuration service eagerly if config exists (it stores a reference,
|
|
254
|
+
// doesn't call config.get(), so safe before initialization)
|
|
255
|
+
if (!(this.config instanceof NotInitializedConfig)) {
|
|
215
256
|
this.configService = new ConfigServiceImpl(this.logger, this.config);
|
|
216
257
|
}
|
|
217
258
|
|
|
@@ -274,8 +315,8 @@ export class OneBunApplication {
|
|
|
274
315
|
}
|
|
275
316
|
}
|
|
276
317
|
|
|
277
|
-
//
|
|
278
|
-
|
|
318
|
+
// Note: root module creation is deferred to start() to ensure
|
|
319
|
+
// config is fully initialized before services are created.
|
|
279
320
|
}
|
|
280
321
|
|
|
281
322
|
/**
|
|
@@ -323,11 +364,23 @@ export class OneBunApplication {
|
|
|
323
364
|
return this.getConfig().get(path);
|
|
324
365
|
}
|
|
325
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Ensure root module is created (i.e., start() has been called).
|
|
369
|
+
* Throws if called before start().
|
|
370
|
+
*/
|
|
371
|
+
private ensureModule(): ModuleInstance {
|
|
372
|
+
if (!this.rootModule) {
|
|
373
|
+
throw new Error('Application not started. Call start() before accessing the module.');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return this.rootModule;
|
|
377
|
+
}
|
|
378
|
+
|
|
326
379
|
/**
|
|
327
380
|
* Get root module layer
|
|
328
381
|
*/
|
|
329
382
|
getLayer(): Layer.Layer<never, never, unknown> {
|
|
330
|
-
return this.
|
|
383
|
+
return this.ensureModule().getLayer();
|
|
331
384
|
}
|
|
332
385
|
|
|
333
386
|
/**
|
|
@@ -377,6 +430,10 @@ export class OneBunApplication {
|
|
|
377
430
|
this.logger.info('Application configuration initialized');
|
|
378
431
|
}
|
|
379
432
|
|
|
433
|
+
// Create the root module AFTER config is initialized,
|
|
434
|
+
// so services can safely use this.config.get() in their constructors
|
|
435
|
+
this.rootModule = OneBunModule.create(this.moduleClass, this.loggerLayer, this.config);
|
|
436
|
+
|
|
380
437
|
// Start metrics collection if enabled
|
|
381
438
|
if (this.metricsService && this.metricsService.startSystemMetricsCollection) {
|
|
382
439
|
this.metricsService.startSystemMetricsCollection();
|
|
@@ -384,10 +441,10 @@ export class OneBunApplication {
|
|
|
384
441
|
}
|
|
385
442
|
|
|
386
443
|
// Setup the module and create controller instances
|
|
387
|
-
await Effect.runPromise(this.
|
|
444
|
+
await Effect.runPromise(this.ensureModule().setup() as Effect.Effect<unknown, never, never>);
|
|
388
445
|
|
|
389
446
|
// Get all controllers from the root module
|
|
390
|
-
const controllers = this.
|
|
447
|
+
const controllers = this.ensureModule().getControllers();
|
|
391
448
|
this.logger.debug(`Loaded ${controllers.length} controllers`);
|
|
392
449
|
|
|
393
450
|
// Initialize WebSocket handler and detect gateways
|
|
@@ -396,7 +453,7 @@ export class OneBunApplication {
|
|
|
396
453
|
// Register WebSocket gateways (they are in controllers array but decorated with @WebSocketGateway)
|
|
397
454
|
for (const controllerClass of controllers) {
|
|
398
455
|
if (isWebSocketGateway(controllerClass)) {
|
|
399
|
-
const instance = this.
|
|
456
|
+
const instance = this.ensureModule().getControllerInstance?.(controllerClass);
|
|
400
457
|
if (instance) {
|
|
401
458
|
this.wsHandler.registerGateway(controllerClass, instance as import('../websocket/ws-base-gateway').BaseWebSocketGateway);
|
|
402
459
|
this.logger.info(`Registered WebSocket gateway: ${controllerClass.name}`);
|
|
@@ -410,163 +467,34 @@ export class OneBunApplication {
|
|
|
410
467
|
// Initialize Docs (OpenAPI/Swagger) if enabled and available
|
|
411
468
|
await this.initializeDocs(controllers);
|
|
412
469
|
|
|
413
|
-
// Create
|
|
414
|
-
const
|
|
415
|
-
string,
|
|
416
|
-
{
|
|
417
|
-
method: string;
|
|
418
|
-
handler: Function;
|
|
419
|
-
handlerName: string;
|
|
420
|
-
controller: Controller;
|
|
421
|
-
params?: ParamMetadata[];
|
|
422
|
-
middleware?: Function[];
|
|
423
|
-
pathPattern?: RegExp;
|
|
424
|
-
pathParams?: string[];
|
|
425
|
-
responseSchemas?: RouteMetadata['responseSchemas'];
|
|
426
|
-
}
|
|
427
|
-
>();
|
|
428
|
-
|
|
429
|
-
// Build application-level path prefix from options
|
|
430
|
-
const appPrefix = this.buildPathPrefix();
|
|
431
|
-
|
|
432
|
-
// Add routes from controllers
|
|
433
|
-
for (const controllerClass of controllers) {
|
|
434
|
-
const controllerMetadata = getControllerMetadata(controllerClass);
|
|
435
|
-
if (!controllerMetadata) {
|
|
436
|
-
this.logger.warn(`No metadata found for controller: ${controllerClass.name}`);
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Get controller instance from module
|
|
441
|
-
if (!this.rootModule.getControllerInstance) {
|
|
442
|
-
this.logger.warn(
|
|
443
|
-
`Module does not support getControllerInstance for ${controllerClass.name}`,
|
|
444
|
-
);
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const controller = this.rootModule.getControllerInstance(controllerClass) as Controller;
|
|
449
|
-
if (!controller) {
|
|
450
|
-
this.logger.warn(`Controller instance not found for ${controllerClass.name}`);
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const controllerPath = controllerMetadata.path;
|
|
455
|
-
|
|
456
|
-
for (const route of controllerMetadata.routes) {
|
|
457
|
-
// Combine: appPrefix + controllerPath + routePath
|
|
458
|
-
// Normalize to ensure consistent matching (e.g., '/api/users/' -> '/api/users')
|
|
459
|
-
const fullPath = normalizePath(`${appPrefix}${controllerPath}${route.path}`);
|
|
460
|
-
const method = this.mapHttpMethod(route.method);
|
|
461
|
-
const handler = (controller as unknown as Record<string, Function>)[route.handler].bind(
|
|
462
|
-
controller,
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
// Process path parameters
|
|
466
|
-
const pathParams: string[] = [];
|
|
467
|
-
let pathPattern: RegExp | undefined;
|
|
468
|
-
|
|
469
|
-
// Check if path contains parameters like :id
|
|
470
|
-
if (fullPath.includes(':')) {
|
|
471
|
-
// Convert path to regex pattern
|
|
472
|
-
const pattern = fullPath.replace(/:([^/]+)/g, (_, paramName) => {
|
|
473
|
-
pathParams.push(paramName);
|
|
474
|
-
|
|
475
|
-
return '([^/]+)';
|
|
476
|
-
});
|
|
477
|
-
pathPattern = new RegExp(`^${pattern}$`);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Use method and path as key to avoid conflicts between different HTTP methods
|
|
481
|
-
const _routeKey = `${method}:${fullPath}`;
|
|
482
|
-
routes.set(_routeKey, {
|
|
483
|
-
method,
|
|
484
|
-
handler,
|
|
485
|
-
handlerName: route.handler,
|
|
486
|
-
controller,
|
|
487
|
-
params: route.params,
|
|
488
|
-
middleware: route.middleware,
|
|
489
|
-
pathPattern,
|
|
490
|
-
pathParams,
|
|
491
|
-
responseSchemas: route.responseSchemas,
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Log all routes
|
|
497
|
-
for (const controllerClass of controllers) {
|
|
498
|
-
const metadata = getControllerMetadata(controllerClass);
|
|
499
|
-
if (!metadata) {
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
for (const route of metadata.routes) {
|
|
504
|
-
const fullPath = normalizePath(`${appPrefix}${metadata.path}${route.path}`);
|
|
505
|
-
const method = this.mapHttpMethod(route.method);
|
|
506
|
-
this.logger.info(`Mapped {${method}} route: ${fullPath}`);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Call onApplicationInit lifecycle hook for all services and controllers
|
|
511
|
-
if (this.rootModule.callOnApplicationInit) {
|
|
512
|
-
await this.rootModule.callOnApplicationInit();
|
|
513
|
-
this.logger.debug('Application initialization hooks completed');
|
|
514
|
-
}
|
|
470
|
+
// Create server context binding (used by route handlers and executeHandler)
|
|
471
|
+
const app = this;
|
|
515
472
|
|
|
516
|
-
//
|
|
473
|
+
// Path constants for framework endpoints
|
|
517
474
|
const metricsPath = this.options.metrics?.path || '/metrics';
|
|
518
|
-
|
|
519
|
-
// Get docs paths
|
|
520
475
|
const docsPath = this.options.docs?.path || '/docs';
|
|
521
476
|
const openApiPath = this.options.docs?.jsonPath || '/openapi.json';
|
|
522
477
|
|
|
523
|
-
//
|
|
524
|
-
const
|
|
525
|
-
const hasWebSocketGateways = this.wsHandler?.hasGateways() ?? false;
|
|
526
|
-
|
|
527
|
-
// Prepare WebSocket handlers if gateways exist
|
|
528
|
-
// When no gateways, use no-op handlers (required by Bun.serve)
|
|
529
|
-
const wsHandlers = hasWebSocketGateways ? this.wsHandler!.createWebSocketHandlers() : {
|
|
530
|
-
|
|
531
|
-
open() { /* no-op */ },
|
|
532
|
-
|
|
533
|
-
message() { /* no-op */ },
|
|
534
|
-
|
|
535
|
-
close() { /* no-op */ },
|
|
536
|
-
|
|
537
|
-
drain() { /* no-op */ },
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
this.server = Bun.serve<WsClientData>({
|
|
541
|
-
port: this.options.port,
|
|
542
|
-
hostname: this.options.host,
|
|
543
|
-
// WebSocket handlers
|
|
544
|
-
websocket: wsHandlers,
|
|
545
|
-
async fetch(req, server) {
|
|
546
|
-
const url = new URL(req.url);
|
|
547
|
-
const rawPath = url.pathname;
|
|
548
|
-
// Normalize path to ensure consistent routing and metrics
|
|
549
|
-
// (removes trailing slash except for root path)
|
|
550
|
-
const path = normalizePath(rawPath);
|
|
551
|
-
const method = req.method;
|
|
552
|
-
const startTime = Date.now();
|
|
553
|
-
|
|
554
|
-
// Handle WebSocket upgrade if gateways exist
|
|
555
|
-
if (hasWebSocketGateways && app.wsHandler) {
|
|
556
|
-
const upgradeHeader = req.headers.get('upgrade')?.toLowerCase();
|
|
557
|
-
const socketioEnabled = app.options.websocket?.socketio?.enabled ?? false;
|
|
558
|
-
const socketioPath = app.options.websocket?.socketio?.path ?? '/socket.io';
|
|
559
|
-
|
|
560
|
-
const isSocketIoPath = socketioEnabled && path.startsWith(socketioPath);
|
|
561
|
-
if (upgradeHeader === 'websocket' || isSocketIoPath) {
|
|
562
|
-
const response = await app.wsHandler.handleUpgrade(req, server);
|
|
563
|
-
if (response === undefined) {
|
|
564
|
-
return undefined; // Successfully upgraded
|
|
565
|
-
}
|
|
478
|
+
// Build application-level path prefix from options
|
|
479
|
+
const appPrefix = this.buildPathPrefix();
|
|
566
480
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
481
|
+
// Build Bun routes object: { "/path": { GET: handler, POST: handler } }
|
|
482
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
483
|
+
const bunRoutes: Record<string, any> = {};
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Create a route handler with the full OneBun request lifecycle:
|
|
487
|
+
* tracing setup → middleware chain → executeHandler → metrics → tracing end
|
|
488
|
+
*/
|
|
489
|
+
function createRouteHandler(
|
|
490
|
+
routeMeta: RouteMetadata,
|
|
491
|
+
boundHandler: Function,
|
|
492
|
+
controller: Controller,
|
|
493
|
+
fullPath: string,
|
|
494
|
+
method: string,
|
|
495
|
+
): (req: OneBunRequest) => Promise<Response> {
|
|
496
|
+
return async (req) => {
|
|
497
|
+
const startTime = Date.now();
|
|
570
498
|
|
|
571
499
|
// Setup tracing context if available and enabled
|
|
572
500
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -605,7 +533,7 @@ export class OneBunApplication {
|
|
|
605
533
|
const httpData = {
|
|
606
534
|
method,
|
|
607
535
|
url: req.url,
|
|
608
|
-
route:
|
|
536
|
+
route: fullPath,
|
|
609
537
|
userAgent: headers['user-agent'],
|
|
610
538
|
remoteAddr: headers['x-forwarded-for'] || headers['x-real-ip'],
|
|
611
539
|
requestSize: headers['content-length']
|
|
@@ -627,9 +555,6 @@ export class OneBunApplication {
|
|
|
627
555
|
(globalThis as Record<string, unknown>).__onebunCurrentTraceContext =
|
|
628
556
|
globalCurrentTraceContext;
|
|
629
557
|
}
|
|
630
|
-
|
|
631
|
-
// Propagate trace context to logger
|
|
632
|
-
// Note: FiberRef.set should be used within Effect context
|
|
633
558
|
} catch (error) {
|
|
634
559
|
app.logger.error(
|
|
635
560
|
'Failed to setup tracing:',
|
|
@@ -638,232 +563,89 @@ export class OneBunApplication {
|
|
|
638
563
|
}
|
|
639
564
|
}
|
|
640
565
|
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (path === docsPath && method === 'GET' && app.swaggerHtml) {
|
|
645
|
-
return new Response(app.swaggerHtml, {
|
|
646
|
-
headers: {
|
|
647
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
648
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
649
|
-
},
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Serve OpenAPI JSON spec
|
|
654
|
-
if (path === openApiPath && method === 'GET') {
|
|
655
|
-
return new Response(JSON.stringify(app.openApiSpec, null, 2), {
|
|
656
|
-
headers: {
|
|
657
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
658
|
-
'Content-Type': 'application/json',
|
|
659
|
-
},
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
}
|
|
566
|
+
// Extract query parameters from URL
|
|
567
|
+
const url = new URL(req.url);
|
|
568
|
+
const queryParams = extractQueryParams(url);
|
|
663
569
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
try {
|
|
667
|
-
const metrics = await app.metricsService.getMetrics();
|
|
570
|
+
try {
|
|
571
|
+
let response: Response;
|
|
668
572
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
573
|
+
// Execute middleware chain if any, then handler
|
|
574
|
+
if (routeMeta.middleware && routeMeta.middleware.length > 0) {
|
|
575
|
+
const next = async (index: number): Promise<Response> => {
|
|
576
|
+
if (index >= routeMeta.middleware!.length) {
|
|
577
|
+
return await executeHandler(
|
|
578
|
+
{
|
|
579
|
+
handler: boundHandler,
|
|
580
|
+
handlerName: routeMeta.handler,
|
|
581
|
+
controller,
|
|
582
|
+
params: routeMeta.params,
|
|
583
|
+
responseSchemas: routeMeta.responseSchemas,
|
|
584
|
+
},
|
|
585
|
+
req,
|
|
586
|
+
queryParams,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
680
589
|
|
|
681
|
-
|
|
682
|
-
status: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
}
|
|
590
|
+
const middleware = routeMeta.middleware![index];
|
|
686
591
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
let route = routes.get(exactRouteKey);
|
|
690
|
-
const paramValues: Record<string, string | string[]> = {};
|
|
592
|
+
return await middleware(req, () => next(index + 1));
|
|
593
|
+
};
|
|
691
594
|
|
|
692
|
-
|
|
693
|
-
for (const [rawKey, value] of url.searchParams.entries()) {
|
|
694
|
-
// Handle array notation: tag[] -> tag (as array)
|
|
695
|
-
const isArrayNotation = rawKey.endsWith('[]');
|
|
696
|
-
const key = isArrayNotation ? rawKey.replace('[]', '') : rawKey;
|
|
697
|
-
|
|
698
|
-
const existing = paramValues[key];
|
|
699
|
-
if (existing !== undefined) {
|
|
700
|
-
// Handle multiple values with same key (e.g., ?tag=a&tag=b or ?tag[]=a&tag[]=b)
|
|
701
|
-
paramValues[key] = Array.isArray(existing)
|
|
702
|
-
? [...existing, value]
|
|
703
|
-
: [existing, value];
|
|
704
|
-
} else if (isArrayNotation) {
|
|
705
|
-
// Array notation always creates an array, even with single value
|
|
706
|
-
paramValues[key] = [value];
|
|
595
|
+
response = await next(0);
|
|
707
596
|
} else {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
route = routeData;
|
|
720
|
-
// Extract parameter values
|
|
721
|
-
for (let i = 0; i < routeData.pathParams!.length; i++) {
|
|
722
|
-
paramValues[routeData.pathParams![i]] = match[i + 1];
|
|
723
|
-
}
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
597
|
+
response = await executeHandler(
|
|
598
|
+
{
|
|
599
|
+
handler: boundHandler,
|
|
600
|
+
handlerName: routeMeta.handler,
|
|
601
|
+
controller,
|
|
602
|
+
params: routeMeta.params,
|
|
603
|
+
responseSchemas: routeMeta.responseSchemas,
|
|
604
|
+
},
|
|
605
|
+
req,
|
|
606
|
+
queryParams,
|
|
607
|
+
);
|
|
727
608
|
}
|
|
728
|
-
}
|
|
729
609
|
|
|
730
|
-
if (!route) {
|
|
731
|
-
const response = new Response('Not Found', {
|
|
732
|
-
status: HttpStatusCode.NOT_FOUND,
|
|
733
|
-
});
|
|
734
610
|
const duration = Date.now() - startTime;
|
|
735
611
|
|
|
736
|
-
// Record metrics
|
|
612
|
+
// Record metrics
|
|
737
613
|
if (app.metricsService && app.metricsService.recordHttpRequest) {
|
|
738
614
|
const durationSeconds = duration / 1000;
|
|
739
615
|
app.metricsService.recordHttpRequest({
|
|
740
616
|
method,
|
|
741
|
-
route:
|
|
742
|
-
statusCode: HttpStatusCode.
|
|
617
|
+
route: fullPath,
|
|
618
|
+
statusCode: response?.status || HttpStatusCode.OK,
|
|
743
619
|
duration: durationSeconds,
|
|
744
|
-
controller:
|
|
620
|
+
controller: controller.constructor.name,
|
|
745
621
|
action: 'unknown',
|
|
746
622
|
});
|
|
747
623
|
}
|
|
748
624
|
|
|
749
|
-
// End trace
|
|
625
|
+
// End trace
|
|
750
626
|
if (traceSpan && app.traceService) {
|
|
751
627
|
try {
|
|
752
628
|
await Effect.runPromise(
|
|
753
629
|
app.traceService.endHttpTrace(traceSpan, {
|
|
754
|
-
statusCode: HttpStatusCode.
|
|
630
|
+
statusCode: response?.status || HttpStatusCode.OK,
|
|
631
|
+
responseSize: response?.headers?.get('content-length')
|
|
632
|
+
? parseInt(response.headers.get('content-length')!, 10)
|
|
633
|
+
: undefined,
|
|
755
634
|
duration,
|
|
756
635
|
}),
|
|
757
636
|
);
|
|
758
637
|
} catch (traceError) {
|
|
759
638
|
app.logger.error(
|
|
760
|
-
'Failed to end trace
|
|
639
|
+
'Failed to end trace:',
|
|
761
640
|
traceError instanceof Error ? traceError : new Error(String(traceError)),
|
|
762
641
|
);
|
|
763
642
|
}
|
|
764
643
|
}
|
|
765
644
|
|
|
766
|
-
// Clear trace context after
|
|
645
|
+
// Clear trace context after request
|
|
767
646
|
clearGlobalTraceContext();
|
|
768
647
|
|
|
769
648
|
return response;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
try {
|
|
773
|
-
// Execute middleware if any
|
|
774
|
-
if (route.middleware && route.middleware.length > 0) {
|
|
775
|
-
const next = async (index: number): Promise<Response> => {
|
|
776
|
-
if (index >= route!.middleware!.length) {
|
|
777
|
-
return await executeHandler(route!, req, paramValues);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const middleware = route!.middleware![index];
|
|
781
|
-
|
|
782
|
-
return await middleware(req, () => next(index + 1));
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
const response = await next(0);
|
|
786
|
-
const duration = Date.now() - startTime;
|
|
787
|
-
|
|
788
|
-
// Record metrics
|
|
789
|
-
if (app.metricsService && app.metricsService.recordHttpRequest) {
|
|
790
|
-
const durationSeconds = duration / 1000;
|
|
791
|
-
app.metricsService.recordHttpRequest({
|
|
792
|
-
method,
|
|
793
|
-
route: path,
|
|
794
|
-
statusCode: response?.status || HttpStatusCode.OK,
|
|
795
|
-
duration: durationSeconds,
|
|
796
|
-
controller: route.controller.constructor.name,
|
|
797
|
-
action: 'unknown',
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// End trace
|
|
802
|
-
if (traceSpan && app.traceService) {
|
|
803
|
-
try {
|
|
804
|
-
await Effect.runPromise(
|
|
805
|
-
app.traceService.endHttpTrace(traceSpan, {
|
|
806
|
-
statusCode: response?.status || HttpStatusCode.OK,
|
|
807
|
-
responseSize: response?.headers?.get('content-length')
|
|
808
|
-
? parseInt(response.headers.get('content-length')!, 10)
|
|
809
|
-
: undefined,
|
|
810
|
-
duration,
|
|
811
|
-
}),
|
|
812
|
-
);
|
|
813
|
-
} catch (traceError) {
|
|
814
|
-
app.logger.error(
|
|
815
|
-
'Failed to end trace:',
|
|
816
|
-
traceError instanceof Error ? traceError : new Error(String(traceError)),
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Clear trace context after request
|
|
822
|
-
clearGlobalTraceContext();
|
|
823
|
-
|
|
824
|
-
return response;
|
|
825
|
-
} else {
|
|
826
|
-
const response = await executeHandler(route, req, paramValues);
|
|
827
|
-
const duration = Date.now() - startTime;
|
|
828
|
-
|
|
829
|
-
// Record metrics
|
|
830
|
-
if (app.metricsService && app.metricsService.recordHttpRequest) {
|
|
831
|
-
const durationSeconds = duration / 1000;
|
|
832
|
-
app.metricsService.recordHttpRequest({
|
|
833
|
-
method,
|
|
834
|
-
route: path,
|
|
835
|
-
statusCode: response?.status || HttpStatusCode.OK,
|
|
836
|
-
duration: durationSeconds,
|
|
837
|
-
controller: route.controller.constructor.name,
|
|
838
|
-
action: 'unknown',
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// End trace
|
|
843
|
-
if (traceSpan && app.traceService) {
|
|
844
|
-
try {
|
|
845
|
-
await Effect.runPromise(
|
|
846
|
-
app.traceService.endHttpTrace(traceSpan, {
|
|
847
|
-
statusCode: response?.status || HttpStatusCode.OK,
|
|
848
|
-
responseSize: response?.headers?.get('content-length')
|
|
849
|
-
? parseInt(response.headers.get('content-length')!, 10)
|
|
850
|
-
: undefined,
|
|
851
|
-
duration,
|
|
852
|
-
}),
|
|
853
|
-
);
|
|
854
|
-
} catch (traceError) {
|
|
855
|
-
app.logger.error(
|
|
856
|
-
'Failed to end trace:',
|
|
857
|
-
traceError instanceof Error ? traceError : new Error(String(traceError)),
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// Clear trace context after request
|
|
863
|
-
clearGlobalTraceContext();
|
|
864
|
-
|
|
865
|
-
return response;
|
|
866
|
-
}
|
|
867
649
|
} catch (error) {
|
|
868
650
|
app.logger.error(
|
|
869
651
|
'Request handling error:',
|
|
@@ -879,10 +661,10 @@ export class OneBunApplication {
|
|
|
879
661
|
const durationSeconds = duration / 1000;
|
|
880
662
|
app.metricsService.recordHttpRequest({
|
|
881
663
|
method,
|
|
882
|
-
route:
|
|
664
|
+
route: fullPath,
|
|
883
665
|
statusCode: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
884
666
|
duration: durationSeconds,
|
|
885
|
-
controller:
|
|
667
|
+
controller: controller.constructor.name,
|
|
886
668
|
action: 'unknown',
|
|
887
669
|
});
|
|
888
670
|
}
|
|
@@ -915,6 +697,179 @@ export class OneBunApplication {
|
|
|
915
697
|
|
|
916
698
|
return response;
|
|
917
699
|
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Add routes from controllers
|
|
704
|
+
for (const controllerClass of controllers) {
|
|
705
|
+
const controllerMetadata = getControllerMetadata(controllerClass);
|
|
706
|
+
if (!controllerMetadata) {
|
|
707
|
+
this.logger.warn(`No metadata found for controller: ${controllerClass.name}`);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Get controller instance from module
|
|
712
|
+
if (!this.ensureModule().getControllerInstance) {
|
|
713
|
+
this.logger.warn(
|
|
714
|
+
`Module does not support getControllerInstance for ${controllerClass.name}`,
|
|
715
|
+
);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const controller = this.ensureModule().getControllerInstance!(controllerClass) as Controller;
|
|
720
|
+
if (!controller) {
|
|
721
|
+
this.logger.warn(`Controller instance not found for ${controllerClass.name}`);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const controllerPath = controllerMetadata.path;
|
|
726
|
+
|
|
727
|
+
for (const route of controllerMetadata.routes) {
|
|
728
|
+
// Combine: appPrefix + controllerPath + routePath
|
|
729
|
+
// Normalize to ensure consistent matching (e.g., '/api/users/' -> '/api/users')
|
|
730
|
+
const fullPath = normalizePath(`${appPrefix}${controllerPath}${route.path}`);
|
|
731
|
+
const method = this.mapHttpMethod(route.method);
|
|
732
|
+
const handler = (controller as unknown as Record<string, Function>)[route.handler].bind(
|
|
733
|
+
controller,
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
// Create wrapped handler with full OneBun lifecycle (tracing, metrics, middleware)
|
|
737
|
+
const wrappedHandler = createRouteHandler(route, handler, controller, fullPath, method);
|
|
738
|
+
|
|
739
|
+
// Add to bunRoutes grouped by path and method
|
|
740
|
+
if (!bunRoutes[fullPath]) {
|
|
741
|
+
bunRoutes[fullPath] = {};
|
|
742
|
+
}
|
|
743
|
+
bunRoutes[fullPath][method] = wrappedHandler;
|
|
744
|
+
|
|
745
|
+
// Register trailing slash variant for consistent matching
|
|
746
|
+
// (e.g., /api/users and /api/users/ both map to the same handler)
|
|
747
|
+
if (fullPath.length > 1 && !fullPath.endsWith('/')) {
|
|
748
|
+
const trailingPath = fullPath + '/';
|
|
749
|
+
if (!bunRoutes[trailingPath]) {
|
|
750
|
+
bunRoutes[trailingPath] = {};
|
|
751
|
+
}
|
|
752
|
+
bunRoutes[trailingPath][method] = wrappedHandler;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Add framework endpoints to routes (docs, metrics)
|
|
758
|
+
if (app.options.docs?.enabled !== false && app.openApiSpec) {
|
|
759
|
+
if (app.swaggerHtml) {
|
|
760
|
+
bunRoutes[docsPath] = {
|
|
761
|
+
|
|
762
|
+
GET: () => new Response(app.swaggerHtml!, {
|
|
763
|
+
headers: {
|
|
764
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
765
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
766
|
+
},
|
|
767
|
+
}),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
bunRoutes[openApiPath] = {
|
|
771
|
+
|
|
772
|
+
GET: () => new Response(JSON.stringify(app.openApiSpec, null, 2), {
|
|
773
|
+
headers: {
|
|
774
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
775
|
+
'Content-Type': 'application/json',
|
|
776
|
+
},
|
|
777
|
+
}),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (app.metricsService) {
|
|
782
|
+
bunRoutes[metricsPath] = {
|
|
783
|
+
|
|
784
|
+
async GET() {
|
|
785
|
+
try {
|
|
786
|
+
const metrics = await app.metricsService.getMetrics();
|
|
787
|
+
|
|
788
|
+
return new Response(metrics, {
|
|
789
|
+
headers: {
|
|
790
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
791
|
+
'Content-Type': app.metricsService.getContentType(),
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
} catch (error) {
|
|
795
|
+
app.logger.error(
|
|
796
|
+
'Failed to get metrics:',
|
|
797
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
return new Response('Internal Server Error', {
|
|
801
|
+
status: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Log all routes
|
|
809
|
+
for (const controllerClass of controllers) {
|
|
810
|
+
const metadata = getControllerMetadata(controllerClass);
|
|
811
|
+
if (!metadata) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
for (const route of metadata.routes) {
|
|
816
|
+
const fullPath = normalizePath(`${appPrefix}${metadata.path}${route.path}`);
|
|
817
|
+
const method = this.mapHttpMethod(route.method);
|
|
818
|
+
this.logger.info(`Mapped {${method}} route: ${fullPath}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Call onApplicationInit lifecycle hook for all services and controllers
|
|
823
|
+
if (this.ensureModule().callOnApplicationInit) {
|
|
824
|
+
await this.ensureModule().callOnApplicationInit!();
|
|
825
|
+
this.logger.debug('Application initialization hooks completed');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const hasWebSocketGateways = this.wsHandler?.hasGateways() ?? false;
|
|
829
|
+
|
|
830
|
+
// Prepare WebSocket handlers if gateways exist
|
|
831
|
+
// When no gateways, use no-op handlers (required by Bun.serve)
|
|
832
|
+
const wsHandlers = hasWebSocketGateways ? this.wsHandler!.createWebSocketHandlers() : {
|
|
833
|
+
|
|
834
|
+
open() { /* no-op */ },
|
|
835
|
+
|
|
836
|
+
message() { /* no-op */ },
|
|
837
|
+
|
|
838
|
+
close() { /* no-op */ },
|
|
839
|
+
|
|
840
|
+
drain() { /* no-op */ },
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
this.server = Bun.serve<WsClientData>({
|
|
844
|
+
port: this.options.port,
|
|
845
|
+
hostname: this.options.host,
|
|
846
|
+
// WebSocket handlers
|
|
847
|
+
websocket: wsHandlers,
|
|
848
|
+
// Bun routes API: all endpoints are handled here
|
|
849
|
+
routes: bunRoutes,
|
|
850
|
+
// Fallback: only WebSocket upgrade and 404
|
|
851
|
+
async fetch(req, server) {
|
|
852
|
+
// Handle WebSocket upgrade if gateways exist
|
|
853
|
+
if (hasWebSocketGateways && app.wsHandler) {
|
|
854
|
+
const upgradeHeader = req.headers.get('upgrade')?.toLowerCase();
|
|
855
|
+
const socketioEnabled = app.options.websocket?.socketio?.enabled ?? false;
|
|
856
|
+
const socketioPath = app.options.websocket?.socketio?.path ?? '/socket.io';
|
|
857
|
+
|
|
858
|
+
const url = new URL(req.url);
|
|
859
|
+
const path = normalizePath(url.pathname);
|
|
860
|
+
const isSocketIoPath = socketioEnabled && path.startsWith(socketioPath);
|
|
861
|
+
if (upgradeHeader === 'websocket' || isSocketIoPath) {
|
|
862
|
+
const response = await app.wsHandler.handleUpgrade(req, server);
|
|
863
|
+
if (response === undefined) {
|
|
864
|
+
return undefined; // Successfully upgraded
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return response;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// 404 for everything not matched by routes
|
|
872
|
+
return new Response('Not Found', { status: HttpStatusCode.NOT_FOUND });
|
|
918
873
|
},
|
|
919
874
|
});
|
|
920
875
|
|
|
@@ -957,7 +912,9 @@ export class OneBunApplication {
|
|
|
957
912
|
}
|
|
958
913
|
|
|
959
914
|
/**
|
|
960
|
-
* Execute route handler with parameter injection and validation
|
|
915
|
+
* Execute route handler with parameter injection and validation.
|
|
916
|
+
* Path parameters come from BunRequest.params (populated by Bun routes API).
|
|
917
|
+
* Query parameters are extracted separately from the URL.
|
|
961
918
|
*/
|
|
962
919
|
async function executeHandler(
|
|
963
920
|
route: {
|
|
@@ -967,8 +924,8 @@ export class OneBunApplication {
|
|
|
967
924
|
params?: ParamMetadata[];
|
|
968
925
|
responseSchemas?: RouteMetadata['responseSchemas'];
|
|
969
926
|
},
|
|
970
|
-
req:
|
|
971
|
-
|
|
927
|
+
req: OneBunRequest,
|
|
928
|
+
queryParams: Record<string, string | string[]>,
|
|
972
929
|
): Promise<Response> {
|
|
973
930
|
// Check if this is an SSE endpoint
|
|
974
931
|
let sseOptions: SseDecoratorOptions | undefined;
|
|
@@ -997,11 +954,14 @@ export class OneBunApplication {
|
|
|
997
954
|
for (const param of sortedParams) {
|
|
998
955
|
switch (param.type) {
|
|
999
956
|
case ParamType.PATH:
|
|
1000
|
-
|
|
957
|
+
// Use req.params from BunRequest (natively populated by Bun routes API)
|
|
958
|
+
args[param.index] = param.name
|
|
959
|
+
? (req.params as Record<string, string>)[param.name]
|
|
960
|
+
: undefined;
|
|
1001
961
|
break;
|
|
1002
962
|
|
|
1003
963
|
case ParamType.QUERY:
|
|
1004
|
-
args[param.index] = param.name ?
|
|
964
|
+
args[param.index] = param.name ? queryParams[param.name] : undefined;
|
|
1005
965
|
break;
|
|
1006
966
|
|
|
1007
967
|
case ParamType.BODY:
|
|
@@ -1016,6 +976,10 @@ export class OneBunApplication {
|
|
|
1016
976
|
args[param.index] = param.name ? req.headers.get(param.name) : undefined;
|
|
1017
977
|
break;
|
|
1018
978
|
|
|
979
|
+
case ParamType.COOKIE:
|
|
980
|
+
args[param.index] = param.name ? req.cookies.get(param.name) ?? undefined : undefined;
|
|
981
|
+
break;
|
|
982
|
+
|
|
1019
983
|
case ParamType.REQUEST:
|
|
1020
984
|
args[param.index] = req;
|
|
1021
985
|
break;
|
|
@@ -1064,7 +1028,6 @@ export class OneBunApplication {
|
|
|
1064
1028
|
// If the result is already a Response object, extract body and validate it
|
|
1065
1029
|
if (result instanceof Response) {
|
|
1066
1030
|
responseStatusCode = result.status;
|
|
1067
|
-
const responseHeaders = Object.fromEntries(result.headers.entries());
|
|
1068
1031
|
|
|
1069
1032
|
// Extract and parse response body for validation
|
|
1070
1033
|
const contentType = result.headers.get('content-type') || '';
|
|
@@ -1098,14 +1061,16 @@ export class OneBunApplication {
|
|
|
1098
1061
|
validatedResult = bodyData;
|
|
1099
1062
|
}
|
|
1100
1063
|
|
|
1064
|
+
// Preserve all original headers (including multiple Set-Cookie)
|
|
1065
|
+
// using new Headers() constructor instead of Object.fromEntries()
|
|
1066
|
+
// which would lose duplicate header keys
|
|
1067
|
+
const newHeaders = new Headers(result.headers);
|
|
1068
|
+
newHeaders.set('Content-Type', 'application/json');
|
|
1069
|
+
|
|
1101
1070
|
// Create new Response with validated data
|
|
1102
1071
|
return new Response(JSON.stringify(validatedResult), {
|
|
1103
1072
|
status: responseStatusCode,
|
|
1104
|
-
headers:
|
|
1105
|
-
...responseHeaders,
|
|
1106
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
1107
|
-
'Content-Type': 'application/json',
|
|
1108
|
-
},
|
|
1073
|
+
headers: newHeaders,
|
|
1109
1074
|
});
|
|
1110
1075
|
} catch {
|
|
1111
1076
|
// If parsing fails, return original response
|
|
@@ -1242,7 +1207,7 @@ export class OneBunApplication {
|
|
|
1242
1207
|
this.logger.info('Stopping OneBun application...');
|
|
1243
1208
|
|
|
1244
1209
|
// Call beforeApplicationDestroy lifecycle hook
|
|
1245
|
-
if (this.rootModule
|
|
1210
|
+
if (this.rootModule?.callBeforeApplicationDestroy) {
|
|
1246
1211
|
this.logger.debug('Calling beforeApplicationDestroy hooks');
|
|
1247
1212
|
await this.rootModule.callBeforeApplicationDestroy(signal);
|
|
1248
1213
|
}
|
|
@@ -1276,7 +1241,7 @@ export class OneBunApplication {
|
|
|
1276
1241
|
}
|
|
1277
1242
|
|
|
1278
1243
|
// Call onModuleDestroy lifecycle hook
|
|
1279
|
-
if (this.rootModule
|
|
1244
|
+
if (this.rootModule?.callOnModuleDestroy) {
|
|
1280
1245
|
this.logger.debug('Calling onModuleDestroy hooks');
|
|
1281
1246
|
await this.rootModule.callOnModuleDestroy();
|
|
1282
1247
|
}
|
|
@@ -1288,7 +1253,7 @@ export class OneBunApplication {
|
|
|
1288
1253
|
}
|
|
1289
1254
|
|
|
1290
1255
|
// Call onApplicationDestroy lifecycle hook
|
|
1291
|
-
if (this.rootModule
|
|
1256
|
+
if (this.rootModule?.callOnApplicationDestroy) {
|
|
1292
1257
|
this.logger.debug('Calling onApplicationDestroy hooks');
|
|
1293
1258
|
await this.rootModule.callOnApplicationDestroy(signal);
|
|
1294
1259
|
}
|
|
@@ -1304,7 +1269,7 @@ export class OneBunApplication {
|
|
|
1304
1269
|
|
|
1305
1270
|
// Check if any controller has queue-related decorators
|
|
1306
1271
|
const hasQueueHandlers = controllers.some(controller => {
|
|
1307
|
-
const instance = this.
|
|
1272
|
+
const instance = this.ensureModule().getControllerInstance?.(controller);
|
|
1308
1273
|
if (!instance) {
|
|
1309
1274
|
return false;
|
|
1310
1275
|
}
|
|
@@ -1364,7 +1329,7 @@ export class OneBunApplication {
|
|
|
1364
1329
|
|
|
1365
1330
|
// Register handlers from controllers using registerService
|
|
1366
1331
|
for (const controllerClass of controllers) {
|
|
1367
|
-
const instance = this.
|
|
1332
|
+
const instance = this.ensureModule().getControllerInstance?.(controllerClass);
|
|
1368
1333
|
if (!instance) {
|
|
1369
1334
|
continue;
|
|
1370
1335
|
}
|
|
@@ -1528,11 +1493,11 @@ export class OneBunApplication {
|
|
|
1528
1493
|
* ```
|
|
1529
1494
|
*/
|
|
1530
1495
|
getService<T>(serviceClass: new (...args: unknown[]) => T): T {
|
|
1531
|
-
if (!this.
|
|
1496
|
+
if (!this.ensureModule().getServiceByClass) {
|
|
1532
1497
|
throw new Error('Module does not support getServiceByClass');
|
|
1533
1498
|
}
|
|
1534
1499
|
|
|
1535
|
-
const service = this.
|
|
1500
|
+
const service = this.ensureModule().getServiceByClass!(serviceClass);
|
|
1536
1501
|
if (!service) {
|
|
1537
1502
|
throw new Error(
|
|
1538
1503
|
`Service ${serviceClass.name} not found. Make sure it's registered in the module's providers.`,
|