@asenajs/ergenecore 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -17
- package/dist/lib/Ergenecore.d.ts +21 -0
- package/dist/lib/Ergenecore.js +178 -59
- package/dist/lib/Ergenecore.js.map +1 -1
- package/dist/lib/ErgenecoreContextWrapper.d.ts +45 -4
- package/dist/lib/ErgenecoreContextWrapper.js +122 -7
- package/dist/lib/ErgenecoreContextWrapper.js.map +1 -1
- package/dist/lib/ErgenecoreWebsocketAdapter.d.ts +2 -16
- package/dist/lib/ErgenecoreWebsocketAdapter.js +20 -48
- package/dist/lib/ErgenecoreWebsocketAdapter.js.map +1 -1
- package/dist/lib/defaults/ConfigService.d.ts +5 -2
- package/dist/lib/defaults/ConfigService.js +1 -1
- package/dist/lib/defaults/ConfigService.js.map +1 -1
- package/dist/lib/defaults/MiddlewareService.d.ts +1 -1
- package/dist/lib/defaults/MiddlewareService.js +1 -1
- package/dist/lib/defaults/StaticServeService.d.ts +1 -1
- package/dist/lib/defaults/StaticServeService.js +1 -1
- package/dist/lib/defaults/ValidationService.d.ts +1 -1
- package/dist/lib/defaults/ValidationService.js +1 -1
- package/dist/lib/errors.d.ts +24 -5
- package/dist/lib/errors.js +15 -4
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/index.d.ts +5 -1
- package/dist/lib/index.js +6 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/middlewares/CorsMiddleware.d.ts +1 -1
- package/dist/lib/middlewares/CorsMiddleware.js +1 -1
- package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +1 -1
- package/dist/lib/middlewares/RateLimiterMiddleware.js +5 -2
- package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -1
- package/dist/lib/stream/SSEStreamWriter.d.ts +9 -0
- package/dist/lib/stream/SSEStreamWriter.js +18 -0
- package/dist/lib/stream/SSEStreamWriter.js.map +1 -0
- package/dist/lib/stream/StreamWriter.d.ts +24 -0
- package/dist/lib/stream/StreamWriter.js +72 -0
- package/dist/lib/stream/StreamWriter.js.map +1 -0
- package/dist/lib/stream/index.d.ts +2 -0
- package/dist/lib/stream/index.js +3 -0
- package/dist/lib/stream/index.js.map +1 -0
- package/dist/lib/types/SendPingStrategy.d.ts +4 -0
- package/dist/lib/types/SendPingStrategy.js +2 -0
- package/dist/lib/types/SendPingStrategy.js.map +1 -0
- package/dist/lib/types/Validation.d.ts +3 -3
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +2 -0
- package/dist/lib/types/index.js.map +1 -1
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -70,8 +70,8 @@ await server.start();
|
|
|
70
70
|
### Controller Example
|
|
71
71
|
|
|
72
72
|
```typescript
|
|
73
|
-
import { Controller } from '@asenajs/asena/
|
|
74
|
-
import { Get, Post, Put, Delete } from '@asenajs/asena/
|
|
73
|
+
import { Controller } from '@asenajs/asena/decorators';
|
|
74
|
+
import { Get, Post, Put, Delete } from '@asenajs/asena/decorators/http';
|
|
75
75
|
import type { Context } from '@asenajs/ergenecore/types';
|
|
76
76
|
|
|
77
77
|
@Controller('/users')
|
|
@@ -225,9 +225,9 @@ const session = context.getCookie('session');
|
|
|
225
225
|
#### Custom Middleware
|
|
226
226
|
|
|
227
227
|
```typescript
|
|
228
|
-
import { Middleware } from '@asenajs/asena/
|
|
229
|
-
import { Controller } from '@asenajs/asena/
|
|
230
|
-
import { Get } from '@asenajs/asena/
|
|
228
|
+
import { Middleware } from '@asenajs/asena/decorators';
|
|
229
|
+
import { Controller } from '@asenajs/asena/decorators';
|
|
230
|
+
import { Get } from '@asenajs/asena/decorators/http';
|
|
231
231
|
import { MiddlewareService, type Context } from '@asenajs/ergenecore';
|
|
232
232
|
|
|
233
233
|
@Middleware()
|
|
@@ -262,7 +262,7 @@ export class AdminController {
|
|
|
262
262
|
Ergenecore comes with a built-in CORS middleware for handling Cross-Origin Resource Sharing:
|
|
263
263
|
|
|
264
264
|
```typescript
|
|
265
|
-
import { Middleware } from '@asenajs/asena/
|
|
265
|
+
import { Middleware } from '@asenajs/asena/decorators';
|
|
266
266
|
import { CorsMiddleware } from '@asenajs/ergenecore';
|
|
267
267
|
|
|
268
268
|
// Allow all origins (default)
|
|
@@ -303,7 +303,7 @@ export class DynamicCors extends CorsMiddleware {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
// Use globally in server config
|
|
306
|
-
import { Config } from '@asenajs/asena/
|
|
306
|
+
import { Config } from '@asenajs/asena/decorators';
|
|
307
307
|
import { ConfigService } from '@asenajs/ergenecore';
|
|
308
308
|
|
|
309
309
|
@Config()
|
|
@@ -326,7 +326,7 @@ export class ApiController {
|
|
|
326
326
|
Ergenecore includes a Token Bucket-based rate limiter for controlling request rates and preventing abuse:
|
|
327
327
|
|
|
328
328
|
```typescript
|
|
329
|
-
import { Middleware } from '@asenajs/asena/
|
|
329
|
+
import { Middleware } from '@asenajs/asena/decorators';
|
|
330
330
|
import { RateLimiterMiddleware } from '@asenajs/ergenecore';
|
|
331
331
|
|
|
332
332
|
// General API rate limiter (100 requests per minute)
|
|
@@ -381,7 +381,7 @@ export class CustomRateLimiter extends RateLimiterMiddleware {
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
// Use globally
|
|
384
|
-
import { Config } from '@asenajs/asena/
|
|
384
|
+
import { Config } from '@asenajs/asena/decorators';
|
|
385
385
|
import { ConfigService } from '@asenajs/ergenecore';
|
|
386
386
|
|
|
387
387
|
@Config()
|
|
@@ -432,8 +432,8 @@ The middleware automatically sets standard rate limit headers:
|
|
|
432
432
|
### Validation with Zod
|
|
433
433
|
|
|
434
434
|
```typescript
|
|
435
|
-
import { Controller, Middleware } from '@asenajs/asena/
|
|
436
|
-
import { Post } from '@asenajs/asena/
|
|
435
|
+
import { Controller, Middleware } from '@asenajs/asena/decorators';
|
|
436
|
+
import { Post } from '@asenajs/asena/decorators/http';
|
|
437
437
|
import { ValidationService, type Context, type ValidationSchema, type ValidationSchemaWithHook } from '@asenajs/ergenecore';
|
|
438
438
|
import { z } from 'zod';
|
|
439
439
|
|
|
@@ -471,8 +471,8 @@ async json(): Promise<ValidationSchema | ValidationSchemaWithHook> {
|
|
|
471
471
|
### Combining Middleware and Validation
|
|
472
472
|
|
|
473
473
|
```typescript
|
|
474
|
-
import { Controller, Middleware } from '@asenajs/asena/
|
|
475
|
-
import { Post } from '@asenajs/asena/
|
|
474
|
+
import { Controller, Middleware } from '@asenajs/asena/decorators';
|
|
475
|
+
import { Post } from '@asenajs/asena/decorators/http';
|
|
476
476
|
import {
|
|
477
477
|
MiddlewareService,
|
|
478
478
|
ValidationService,
|
|
@@ -556,8 +556,8 @@ export class ChatSocket extends AsenaWebSocketService<void> {
|
|
|
556
556
|
### Static File Serving
|
|
557
557
|
|
|
558
558
|
```typescript
|
|
559
|
-
import { Controller } from '@asenajs/asena/
|
|
560
|
-
import { Get } from '@asenajs/asena/
|
|
559
|
+
import { Controller } from '@asenajs/asena/decorators';
|
|
560
|
+
import { Get } from '@asenajs/asena/decorators/http';
|
|
561
561
|
import { StaticServe, StaticServeService } from '@asenajs/asena/static';
|
|
562
562
|
import type { Context } from '@asenajs/ergenecore/types';
|
|
563
563
|
|
|
@@ -587,8 +587,8 @@ export class StaticController {
|
|
|
587
587
|
### Error Handling
|
|
588
588
|
|
|
589
589
|
```typescript
|
|
590
|
-
import { Config } from '@asenajs/asena/
|
|
591
|
-
import { Inject } from '@asenajs/asena/ioc';
|
|
590
|
+
import { Config } from '@asenajs/asena/decorators';
|
|
591
|
+
import { Inject } from '@asenajs/asena/decorators/ioc';
|
|
592
592
|
import { ConfigService, type Context } from '@asenajs/ergenecore';
|
|
593
593
|
|
|
594
594
|
@Config()
|
package/dist/lib/Ergenecore.d.ts
CHANGED
|
@@ -56,6 +56,15 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
|
|
|
56
56
|
* Config is optional - if not provided, middleware applies to all routes
|
|
57
57
|
*/
|
|
58
58
|
private globalMiddlewares;
|
|
59
|
+
/**
|
|
60
|
+
* HTML routes for FrontendController pages
|
|
61
|
+
* Stored separately and merged into Bun.serve() routes at start time
|
|
62
|
+
*/
|
|
63
|
+
private htmlRoutes;
|
|
64
|
+
/**
|
|
65
|
+
* Queue of FrontendController route metadata for deferred logging at start time
|
|
66
|
+
*/
|
|
67
|
+
private frontEndRouteQueue;
|
|
59
68
|
private options;
|
|
60
69
|
/**
|
|
61
70
|
* Creates a new CoreAdapter instance
|
|
@@ -107,6 +116,14 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
|
|
|
107
116
|
* });
|
|
108
117
|
* ```
|
|
109
118
|
*/
|
|
119
|
+
/**
|
|
120
|
+
* Registers an HTML route for FrontendController pages.
|
|
121
|
+
* HTML routes bypass the middleware chain and are served directly by Bun.serve().
|
|
122
|
+
*
|
|
123
|
+
* @param path - Full URL path (e.g., '/ui/home')
|
|
124
|
+
* @param htmlBundle - The HTML bundle returned by importing an .html file
|
|
125
|
+
*/
|
|
126
|
+
registerHTMLRoute(path: string, htmlBundle: unknown, controllerName: string, controllerBasePath: string): void;
|
|
110
127
|
use(middleware: BaseMiddleware<Context>, config?: GlobalMiddlewareConfig['routes']): void;
|
|
111
128
|
/**
|
|
112
129
|
* Registers a route (deferred registration)
|
|
@@ -595,4 +612,8 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
|
|
|
595
612
|
* ```
|
|
596
613
|
*/
|
|
597
614
|
private buildControllerBasedLog;
|
|
615
|
+
/**
|
|
616
|
+
* Groups FrontendController routes by controller name for logging
|
|
617
|
+
*/
|
|
618
|
+
private groupFrontEndRoutesByController;
|
|
598
619
|
}
|
package/dist/lib/Ergenecore.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { AsenaAdapter, VALIDATOR_METHODS, } from '@asenajs/asena/adapter';
|
|
2
2
|
import { blue, green, red, yellow } from '@asenajs/asena/logger';
|
|
3
|
-
import { shouldApplyMiddleware } from '@asenajs/asena/utlis';
|
|
4
3
|
import { ErgenecoreWebsocketAdapter } from './ErgenecoreWebsocketAdapter';
|
|
5
4
|
import { ErgenecoreContextWrapper } from './ErgenecoreContextWrapper';
|
|
6
5
|
import * as Bun from 'bun';
|
|
7
6
|
import * as path from 'path';
|
|
8
7
|
import { HttpException, MiddlewareResponseError } from './errors';
|
|
8
|
+
import { shouldApplyMiddleware } from '@asenajs/asena/utils';
|
|
9
9
|
/**
|
|
10
10
|
* Static response headers for performance
|
|
11
11
|
*
|
|
@@ -63,6 +63,15 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
63
63
|
* Config is optional - if not provided, middleware applies to all routes
|
|
64
64
|
*/
|
|
65
65
|
globalMiddlewares = [];
|
|
66
|
+
/**
|
|
67
|
+
* HTML routes for FrontendController pages
|
|
68
|
+
* Stored separately and merged into Bun.serve() routes at start time
|
|
69
|
+
*/
|
|
70
|
+
htmlRoutes = new Map();
|
|
71
|
+
/**
|
|
72
|
+
* Queue of FrontendController route metadata for deferred logging at start time
|
|
73
|
+
*/
|
|
74
|
+
frontEndRouteQueue = [];
|
|
66
75
|
options = {};
|
|
67
76
|
/**
|
|
68
77
|
* Creates a new CoreAdapter instance
|
|
@@ -126,6 +135,28 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
126
135
|
* });
|
|
127
136
|
* ```
|
|
128
137
|
*/
|
|
138
|
+
/**
|
|
139
|
+
* Registers an HTML route for FrontendController pages.
|
|
140
|
+
* HTML routes bypass the middleware chain and are served directly by Bun.serve().
|
|
141
|
+
*
|
|
142
|
+
* @param path - Full URL path (e.g., '/ui/home')
|
|
143
|
+
* @param htmlBundle - The HTML bundle returned by importing an .html file
|
|
144
|
+
*/
|
|
145
|
+
registerHTMLRoute(path, htmlBundle, controllerName, controllerBasePath) {
|
|
146
|
+
if (this.htmlRoutes.has(path)) {
|
|
147
|
+
throw new Error(`Duplicate HTML route: "${path}" is already registered.`);
|
|
148
|
+
}
|
|
149
|
+
this.htmlRoutes.set(path, htmlBundle);
|
|
150
|
+
this.frontEndRouteQueue.push({ path, controllerName, controllerBasePath });
|
|
151
|
+
// Register trailing slash variant for consistent routing
|
|
152
|
+
// e.g., /ui → also register /ui/ (or vice versa)
|
|
153
|
+
if (path !== '/' && !path.endsWith('/')) {
|
|
154
|
+
this.htmlRoutes.set(`${path}/`, htmlBundle);
|
|
155
|
+
}
|
|
156
|
+
else if (path !== '/' && path.endsWith('/')) {
|
|
157
|
+
this.htmlRoutes.set(path.slice(0, -1), htmlBundle);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
129
160
|
use(middleware, config) {
|
|
130
161
|
this.globalMiddlewares.push({ middleware, config });
|
|
131
162
|
}
|
|
@@ -178,6 +209,13 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
178
209
|
this.checkPathCollisions(httpRoutes, wsRoutes);
|
|
179
210
|
// 4. Merge routes
|
|
180
211
|
const finalRoutes = this.mergeRoutes(httpRoutes, wsRoutes);
|
|
212
|
+
// 4.5. Merge HTML routes (FrontendController pages)
|
|
213
|
+
for (const [htmlPath, htmlBundle] of this.htmlRoutes) {
|
|
214
|
+
if (finalRoutes[htmlPath]) {
|
|
215
|
+
throw new Error(`HTML route collision at "${htmlPath}": path already registered as an API or WebSocket route.`);
|
|
216
|
+
}
|
|
217
|
+
finalRoutes[htmlPath] = htmlBundle;
|
|
218
|
+
}
|
|
181
219
|
// 5. Prepare WebSocket before starting server
|
|
182
220
|
await this.websocketAdapter.prepareWebSocket(this.options.wsOptions);
|
|
183
221
|
const serverPort = port ?? this.port;
|
|
@@ -193,7 +231,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
193
231
|
await this.websocketAdapter.startWebsocket(this.server);
|
|
194
232
|
this.routesBuilt = true;
|
|
195
233
|
// Log controller summary first
|
|
196
|
-
if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0) {
|
|
234
|
+
if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0 || this.frontEndRouteQueue.length > 0) {
|
|
197
235
|
this.logControllerSummary();
|
|
198
236
|
// Then log detailed route list
|
|
199
237
|
this.logger.info(this.buildControllerBasedLog());
|
|
@@ -275,7 +313,15 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
275
313
|
}
|
|
276
314
|
// Build Bun router object for each path
|
|
277
315
|
for (const [path, pathRoutes] of routesByPath) {
|
|
278
|
-
|
|
316
|
+
// Normalize trailing slash to support both variants
|
|
317
|
+
// This ensures /users and /users/ both work with query params
|
|
318
|
+
const normalizedPath = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
|
|
319
|
+
const pathWithSlash = normalizedPath === '/' ? '/' : `${normalizedPath}/`;
|
|
320
|
+
// Initialize both path variants (skip duplicate for root path)
|
|
321
|
+
routes[normalizedPath] = routes[normalizedPath] || {};
|
|
322
|
+
if (normalizedPath !== '/') {
|
|
323
|
+
routes[pathWithSlash] = routes[pathWithSlash] || {};
|
|
324
|
+
}
|
|
279
325
|
for (const route of pathRoutes) {
|
|
280
326
|
const method = route.method.toUpperCase();
|
|
281
327
|
// Fast Path Optimization with Pattern Matching
|
|
@@ -286,19 +332,48 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
286
332
|
(!route.middlewares || route.middlewares.length === 0) &&
|
|
287
333
|
!route.validator &&
|
|
288
334
|
!route.staticServe;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
335
|
+
// Create handler based on route complexity
|
|
336
|
+
const handler = isSimpleRoute
|
|
337
|
+
? this.createFastPathHandler(route)
|
|
338
|
+
: this.createRouteHandler(route, commonMiddlewares);
|
|
339
|
+
// Register handler to both path variants for trailing slash support
|
|
340
|
+
if (method === 'ALL') {
|
|
341
|
+
// Bun native: direct handler function matches all HTTP methods
|
|
342
|
+
routes[normalizedPath] = handler;
|
|
343
|
+
if (normalizedPath !== '/') {
|
|
344
|
+
routes[pathWithSlash] = handler;
|
|
345
|
+
}
|
|
292
346
|
}
|
|
293
347
|
else {
|
|
294
|
-
|
|
295
|
-
|
|
348
|
+
routes[normalizedPath][method] = handler;
|
|
349
|
+
if (normalizedPath !== '/') {
|
|
350
|
+
routes[pathWithSlash][method] = handler;
|
|
351
|
+
}
|
|
296
352
|
}
|
|
297
353
|
}
|
|
298
354
|
}
|
|
299
355
|
}
|
|
300
|
-
//
|
|
301
|
-
|
|
356
|
+
// Catch-all handler for unmatched routes (including OPTIONS preflight)
|
|
357
|
+
// Runs global middlewares so CORS and other cross-cutting concerns work for all requests
|
|
358
|
+
routes['/*'] = async (req) => {
|
|
359
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
360
|
+
const requestPath = new URL(req.url).pathname;
|
|
361
|
+
// Filter and run applicable global middlewares
|
|
362
|
+
const applicableMiddlewares = this.globalMiddlewares
|
|
363
|
+
.filter(({ config }) => shouldApplyMiddleware(requestPath, config))
|
|
364
|
+
.map(({ middleware }) => middleware);
|
|
365
|
+
if (applicableMiddlewares.length > 0) {
|
|
366
|
+
const result = await this.executeMiddlewares(context, applicableMiddlewares);
|
|
367
|
+
if (result instanceof Response)
|
|
368
|
+
return result;
|
|
369
|
+
if (result === false) {
|
|
370
|
+
return new Response('Forbidden', { status: 403 });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// No middleware handled the request → error handler or 404
|
|
374
|
+
if (this.errorHandler) {
|
|
375
|
+
return this.errorHandler(new Error('Not Found'), context);
|
|
376
|
+
}
|
|
302
377
|
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
|
303
378
|
status: 404,
|
|
304
379
|
headers: STATIC_JSON_HEADERS,
|
|
@@ -363,15 +438,24 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
363
438
|
* }
|
|
364
439
|
* ```
|
|
365
440
|
*/
|
|
366
|
-
async executeMiddlewares(context, middlewares, index = 0) {
|
|
441
|
+
async executeMiddlewares(context, middlewares, index = 0, onComplete) {
|
|
367
442
|
// Base case: all middlewares executed successfully
|
|
368
443
|
if (index >= middlewares.length) {
|
|
444
|
+
if (onComplete) {
|
|
445
|
+
const result = await onComplete();
|
|
446
|
+
if (result instanceof Response)
|
|
447
|
+
return result;
|
|
448
|
+
if (result === false)
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
369
451
|
return true;
|
|
370
452
|
}
|
|
371
453
|
const middleware = middlewares[index];
|
|
454
|
+
let nextCalled = false;
|
|
372
455
|
// Create next() function that executes the next middleware in chain
|
|
373
456
|
const next = async () => {
|
|
374
|
-
|
|
457
|
+
nextCalled = true;
|
|
458
|
+
const result = await this.executeMiddlewares(context, middlewares, index + 1, onComplete);
|
|
375
459
|
// If next middleware returned Response, throw MiddlewareResponseError to propagate it
|
|
376
460
|
if (result instanceof Response) {
|
|
377
461
|
throw new MiddlewareResponseError(result);
|
|
@@ -392,7 +476,11 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
392
476
|
if (result === false) {
|
|
393
477
|
return false;
|
|
394
478
|
}
|
|
395
|
-
//
|
|
479
|
+
// If middleware returned true/void without calling next(), auto-continue the chain
|
|
480
|
+
if (!nextCalled) {
|
|
481
|
+
return await this.executeMiddlewares(context, middlewares, index + 1, onComplete);
|
|
482
|
+
}
|
|
483
|
+
// Middleware called next() and completed successfully
|
|
396
484
|
return true;
|
|
397
485
|
}
|
|
398
486
|
catch (error) {
|
|
@@ -442,7 +530,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
442
530
|
return async (req) => {
|
|
443
531
|
try {
|
|
444
532
|
// Create context wrapper
|
|
445
|
-
const context = new ErgenecoreContextWrapper(req);
|
|
533
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
446
534
|
// Execute filtered global middlewares with real next() chain
|
|
447
535
|
if (applicableGlobalMiddlewares.length > 0) {
|
|
448
536
|
const result = await this.executeMiddlewares(context, applicableGlobalMiddlewares);
|
|
@@ -583,7 +671,8 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
583
671
|
// If no error handler is set, use ultra-minimal version with default error handling
|
|
584
672
|
if (!this.errorHandler) {
|
|
585
673
|
return async (req) => {
|
|
586
|
-
const context = new ErgenecoreContextWrapper(req);
|
|
674
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
675
|
+
context.routePattern = route.path;
|
|
587
676
|
try {
|
|
588
677
|
// Inject Bun's native route params if present
|
|
589
678
|
// @ts-expect-error - Bun adds params to Request
|
|
@@ -624,7 +713,8 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
624
713
|
}
|
|
625
714
|
// With error handler (minimal try-catch)
|
|
626
715
|
return async (req) => {
|
|
627
|
-
const context = new ErgenecoreContextWrapper(req);
|
|
716
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
717
|
+
context.routePattern = route.path;
|
|
628
718
|
try {
|
|
629
719
|
// Inject Bun's native route params if present
|
|
630
720
|
// @ts-expect-error - Bun adds params to Request
|
|
@@ -676,7 +766,8 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
676
766
|
const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
|
|
677
767
|
return async (req) => {
|
|
678
768
|
// Create context wrapper outside try block so it's accessible in catch
|
|
679
|
-
const context = new ErgenecoreContextWrapper(req);
|
|
769
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
770
|
+
context.routePattern = route.path;
|
|
680
771
|
// Inject Bun's native route params
|
|
681
772
|
// @ts-expect-error - Bun adds params to Request
|
|
682
773
|
if (req.params) {
|
|
@@ -687,53 +778,48 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
687
778
|
}
|
|
688
779
|
}
|
|
689
780
|
try {
|
|
690
|
-
//
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
781
|
+
// Combine global + route middlewares into a single chain
|
|
782
|
+
const allMiddlewares = [
|
|
783
|
+
...applicableGlobalMiddlewares,
|
|
784
|
+
...(route.middlewares || []),
|
|
785
|
+
];
|
|
786
|
+
// Execute middleware chain with handler as onComplete callback
|
|
787
|
+
// This ensures the handler runs INSIDE the middleware async context
|
|
788
|
+
const result = await this.executeMiddlewares(context, allMiddlewares, 0, async () => {
|
|
789
|
+
// Execute validation
|
|
790
|
+
if (route.validator) {
|
|
791
|
+
const validationResult = await this.validateRequest(context, route.validator);
|
|
792
|
+
if (validationResult)
|
|
793
|
+
return validationResult;
|
|
696
794
|
}
|
|
697
|
-
//
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// Execute route middlewares with real next() chain
|
|
703
|
-
if (route.middlewares && route.middlewares.length > 0) {
|
|
704
|
-
const result = await this.executeMiddlewares(context, route.middlewares);
|
|
705
|
-
// If middleware returned a custom response, return it
|
|
706
|
-
if (result instanceof Response) {
|
|
707
|
-
return result;
|
|
795
|
+
// Handle static file serving
|
|
796
|
+
if (route.staticServe) {
|
|
797
|
+
const staticResponse = await this.serveStaticFile(req, context, route.staticServe);
|
|
798
|
+
if (staticResponse)
|
|
799
|
+
return staticResponse;
|
|
708
800
|
}
|
|
709
|
-
//
|
|
710
|
-
|
|
711
|
-
|
|
801
|
+
// Execute route handler
|
|
802
|
+
const response = await route.handler(context);
|
|
803
|
+
// If handler returns Response, return it directly
|
|
804
|
+
if (response instanceof Response) {
|
|
805
|
+
return response;
|
|
712
806
|
}
|
|
807
|
+
// Otherwise, wrap in Response
|
|
808
|
+
return new Response(JSON.stringify(response), {
|
|
809
|
+
status: 200,
|
|
810
|
+
headers: { 'Content-Type': 'application/json' },
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
// If middleware chain returned a custom response, return it
|
|
814
|
+
if (result instanceof Response) {
|
|
815
|
+
return result;
|
|
713
816
|
}
|
|
714
|
-
//
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
if (validationResult)
|
|
718
|
-
return validationResult;
|
|
719
|
-
}
|
|
720
|
-
// Handle static file serving
|
|
721
|
-
if (route.staticServe) {
|
|
722
|
-
const staticResponse = await this.serveStaticFile(req, context, route.staticServe);
|
|
723
|
-
if (staticResponse)
|
|
724
|
-
return staticResponse;
|
|
725
|
-
}
|
|
726
|
-
// Execute route handler
|
|
727
|
-
const response = await route.handler(context);
|
|
728
|
-
// If handler returns Response, return it directly
|
|
729
|
-
if (response instanceof Response) {
|
|
730
|
-
return response;
|
|
817
|
+
// If middleware chain returned false (stopped), return 403
|
|
818
|
+
if (result === false) {
|
|
819
|
+
return new Response('Forbidden', { status: 403 });
|
|
731
820
|
}
|
|
732
|
-
//
|
|
733
|
-
return new Response(
|
|
734
|
-
status: 200,
|
|
735
|
-
headers: { 'Content-Type': 'application/json' },
|
|
736
|
-
});
|
|
821
|
+
// Default: result is true (should not happen with onComplete, but just in case)
|
|
822
|
+
return new Response(null, { status: 204 });
|
|
737
823
|
}
|
|
738
824
|
catch (error) {
|
|
739
825
|
// If handler or middleware threw HttpException, convert to Response
|
|
@@ -1278,6 +1364,13 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
1278
1364
|
this.logger.info(`${green('✓')} Successfully registered ${yellow('WEBSOCKET')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
|
|
1279
1365
|
}
|
|
1280
1366
|
}
|
|
1367
|
+
// Log FrontendControllers
|
|
1368
|
+
const frontEndGroups = this.groupFrontEndRoutesByController();
|
|
1369
|
+
for (const [controllerName, group] of frontEndGroups) {
|
|
1370
|
+
const routeCount = group.routes.length;
|
|
1371
|
+
const routeText = routeCount === 1 ? 'route' : 'routes';
|
|
1372
|
+
this.logger.info(`${green('✓')} Successfully registered ${yellow('FRONTEND')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
|
|
1373
|
+
}
|
|
1281
1374
|
}
|
|
1282
1375
|
/**
|
|
1283
1376
|
* Builds controller-based log output with colors
|
|
@@ -1394,7 +1487,33 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
1394
1487
|
}
|
|
1395
1488
|
lines.push(''); // Empty line between namespaces
|
|
1396
1489
|
}
|
|
1490
|
+
// 4. Frontend controllers (HTML routes)
|
|
1491
|
+
const frontEndGroups = this.groupFrontEndRoutesByController();
|
|
1492
|
+
const sortedFrontEnd = Array.from(frontEndGroups.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
1493
|
+
for (const [controllerName, group] of sortedFrontEnd) {
|
|
1494
|
+
lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
|
|
1495
|
+
for (const route of group.routes) {
|
|
1496
|
+
lines.push(` ${green('HTML')} ${route.path}`);
|
|
1497
|
+
}
|
|
1498
|
+
lines.push('');
|
|
1499
|
+
}
|
|
1397
1500
|
return lines.join('\n');
|
|
1398
1501
|
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Groups FrontendController routes by controller name for logging
|
|
1504
|
+
*/
|
|
1505
|
+
groupFrontEndRoutesByController() {
|
|
1506
|
+
const groups = new Map();
|
|
1507
|
+
for (const route of this.frontEndRouteQueue) {
|
|
1508
|
+
if (!groups.has(route.controllerName)) {
|
|
1509
|
+
groups.set(route.controllerName, {
|
|
1510
|
+
basePath: route.controllerBasePath,
|
|
1511
|
+
routes: [],
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
groups.get(route.controllerName).routes.push({ path: route.path });
|
|
1515
|
+
}
|
|
1516
|
+
return groups;
|
|
1517
|
+
}
|
|
1399
1518
|
}
|
|
1400
1519
|
//# sourceMappingURL=Ergenecore.js.map
|