@asenajs/ergenecore 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -17
- package/dist/lib/Ergenecore.d.ts +15 -2
- package/dist/lib/Ergenecore.js +152 -80
- package/dist/lib/Ergenecore.js.map +1 -1
- package/dist/lib/ErgenecoreContextWrapper.d.ts +44 -4
- package/dist/lib/ErgenecoreContextWrapper.js +115 -7
- package/dist/lib/ErgenecoreContextWrapper.js.map +1 -1
- package/dist/lib/ErgenecoreWebsocketAdapter.d.ts +2 -16
- package/dist/lib/ErgenecoreWebsocketAdapter.js +24 -52
- 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/MiddlewareService.js.map +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 +2 -2
- package/dist/lib/middlewares/CorsMiddleware.js +2 -2
- package/dist/lib/middlewares/CorsMiddleware.js.map +1 -1
- package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +2 -2
- package/dist/lib/middlewares/RateLimiterMiddleware.js +9 -6
- 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 +19 -10
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,11 @@ 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;
|
|
59
64
|
private options;
|
|
60
65
|
/**
|
|
61
66
|
* Creates a new CoreAdapter instance
|
|
@@ -82,7 +87,7 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
|
|
|
82
87
|
* });
|
|
83
88
|
* ```
|
|
84
89
|
*/
|
|
85
|
-
registerWebsocketRoute(params: WebsocketRouteParams<Context>): void
|
|
90
|
+
registerWebsocketRoute(params: WebsocketRouteParams<Context>): Promise<void>;
|
|
86
91
|
/**
|
|
87
92
|
* Sets the server port
|
|
88
93
|
*
|
|
@@ -107,6 +112,14 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
|
|
|
107
112
|
* });
|
|
108
113
|
* ```
|
|
109
114
|
*/
|
|
115
|
+
/**
|
|
116
|
+
* Registers an HTML route for FrontendController pages.
|
|
117
|
+
* HTML routes bypass the middleware chain and are served directly by Bun.serve().
|
|
118
|
+
*
|
|
119
|
+
* @param path - Full URL path (e.g., '/ui/home')
|
|
120
|
+
* @param htmlBundle - The HTML bundle returned by importing an .html file
|
|
121
|
+
*/
|
|
122
|
+
registerHTMLRoute(path: string, htmlBundle: unknown): void;
|
|
110
123
|
use(middleware: BaseMiddleware<Context>, config?: GlobalMiddlewareConfig['routes']): void;
|
|
111
124
|
/**
|
|
112
125
|
* Registers a route (deferred registration)
|
|
@@ -143,7 +156,7 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
|
|
|
143
156
|
* @param port - Optional port to override default
|
|
144
157
|
* @returns Bun server instance
|
|
145
158
|
*/
|
|
146
|
-
start(port?: number): Server<any
|
|
159
|
+
start(port?: number): Promise<Server<any>>;
|
|
147
160
|
/**
|
|
148
161
|
* Stops the server
|
|
149
162
|
*
|
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/utils/patternMatcher';
|
|
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,11 @@ 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();
|
|
66
71
|
options = {};
|
|
67
72
|
/**
|
|
68
73
|
* Creates a new CoreAdapter instance
|
|
@@ -92,12 +97,12 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
92
97
|
* });
|
|
93
98
|
* ```
|
|
94
99
|
*/
|
|
95
|
-
registerWebsocketRoute(params) {
|
|
100
|
+
async registerWebsocketRoute(params) {
|
|
96
101
|
// Queue WebSocket route for building during start()
|
|
97
102
|
this.wsRouteQueue.push(params);
|
|
98
103
|
// Register WebSocket service with adapter using the route path (not namespace)
|
|
99
104
|
if (this.websocketAdapter && params.websocketService) {
|
|
100
|
-
this.websocketAdapter.registerWebSocket(params.websocketService);
|
|
105
|
+
await this.websocketAdapter.registerWebSocket(params.websocketService);
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
/**
|
|
@@ -126,6 +131,27 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
126
131
|
* });
|
|
127
132
|
* ```
|
|
128
133
|
*/
|
|
134
|
+
/**
|
|
135
|
+
* Registers an HTML route for FrontendController pages.
|
|
136
|
+
* HTML routes bypass the middleware chain and are served directly by Bun.serve().
|
|
137
|
+
*
|
|
138
|
+
* @param path - Full URL path (e.g., '/ui/home')
|
|
139
|
+
* @param htmlBundle - The HTML bundle returned by importing an .html file
|
|
140
|
+
*/
|
|
141
|
+
registerHTMLRoute(path, htmlBundle) {
|
|
142
|
+
if (this.htmlRoutes.has(path)) {
|
|
143
|
+
throw new Error(`Duplicate HTML route: "${path}" is already registered.`);
|
|
144
|
+
}
|
|
145
|
+
this.htmlRoutes.set(path, htmlBundle);
|
|
146
|
+
// Register trailing slash variant for consistent routing
|
|
147
|
+
// e.g., /ui → also register /ui/ (or vice versa)
|
|
148
|
+
if (path !== '/' && !path.endsWith('/')) {
|
|
149
|
+
this.htmlRoutes.set(`${path}/`, htmlBundle);
|
|
150
|
+
}
|
|
151
|
+
else if (path !== '/' && path.endsWith('/')) {
|
|
152
|
+
this.htmlRoutes.set(path.slice(0, -1), htmlBundle);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
129
155
|
use(middleware, config) {
|
|
130
156
|
this.globalMiddlewares.push({ middleware, config });
|
|
131
157
|
}
|
|
@@ -166,7 +192,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
166
192
|
* @param port - Optional port to override default
|
|
167
193
|
* @returns Bun server instance
|
|
168
194
|
*/
|
|
169
|
-
start(port) {
|
|
195
|
+
async start(port) {
|
|
170
196
|
// Build routes if not built yet
|
|
171
197
|
const serverHostname = this._hostname;
|
|
172
198
|
if (!this.routesBuilt) {
|
|
@@ -178,8 +204,15 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
178
204
|
this.checkPathCollisions(httpRoutes, wsRoutes);
|
|
179
205
|
// 4. Merge routes
|
|
180
206
|
const finalRoutes = this.mergeRoutes(httpRoutes, wsRoutes);
|
|
207
|
+
// 4.5. Merge HTML routes (FrontendController pages)
|
|
208
|
+
for (const [htmlPath, htmlBundle] of this.htmlRoutes) {
|
|
209
|
+
if (finalRoutes[htmlPath]) {
|
|
210
|
+
throw new Error(`HTML route collision at "${htmlPath}": path already registered as an API or WebSocket route.`);
|
|
211
|
+
}
|
|
212
|
+
finalRoutes[htmlPath] = htmlBundle;
|
|
213
|
+
}
|
|
181
214
|
// 5. Prepare WebSocket before starting server
|
|
182
|
-
this.websocketAdapter.prepareWebSocket(this.options.wsOptions);
|
|
215
|
+
await this.websocketAdapter.prepareWebSocket(this.options.wsOptions);
|
|
183
216
|
const serverPort = port ?? this.port;
|
|
184
217
|
// 6. Start Bun server with merged routes
|
|
185
218
|
this.server = Bun.serve({
|
|
@@ -190,7 +223,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
190
223
|
websocket: this.websocketAdapter.websocket,
|
|
191
224
|
});
|
|
192
225
|
// Start WebSocket server (initializes AsenaWebSocketServer for each namespace)
|
|
193
|
-
this.websocketAdapter.startWebsocket(this.server);
|
|
226
|
+
await this.websocketAdapter.startWebsocket(this.server);
|
|
194
227
|
this.routesBuilt = true;
|
|
195
228
|
// Log controller summary first
|
|
196
229
|
if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0) {
|
|
@@ -275,7 +308,15 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
275
308
|
}
|
|
276
309
|
// Build Bun router object for each path
|
|
277
310
|
for (const [path, pathRoutes] of routesByPath) {
|
|
278
|
-
|
|
311
|
+
// Normalize trailing slash to support both variants
|
|
312
|
+
// This ensures /users and /users/ both work with query params
|
|
313
|
+
const normalizedPath = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
|
|
314
|
+
const pathWithSlash = normalizedPath === '/' ? '/' : `${normalizedPath}/`;
|
|
315
|
+
// Initialize both path variants (skip duplicate for root path)
|
|
316
|
+
routes[normalizedPath] = routes[normalizedPath] || {};
|
|
317
|
+
if (normalizedPath !== '/') {
|
|
318
|
+
routes[pathWithSlash] = routes[pathWithSlash] || {};
|
|
319
|
+
}
|
|
279
320
|
for (const route of pathRoutes) {
|
|
280
321
|
const method = route.method.toUpperCase();
|
|
281
322
|
// Fast Path Optimization with Pattern Matching
|
|
@@ -286,19 +327,48 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
286
327
|
(!route.middlewares || route.middlewares.length === 0) &&
|
|
287
328
|
!route.validator &&
|
|
288
329
|
!route.staticServe;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
330
|
+
// Create handler based on route complexity
|
|
331
|
+
const handler = isSimpleRoute
|
|
332
|
+
? this.createFastPathHandler(route)
|
|
333
|
+
: this.createRouteHandler(route, commonMiddlewares);
|
|
334
|
+
// Register handler to both path variants for trailing slash support
|
|
335
|
+
if (method === 'ALL') {
|
|
336
|
+
// Bun native: direct handler function matches all HTTP methods
|
|
337
|
+
routes[normalizedPath] = handler;
|
|
338
|
+
if (normalizedPath !== '/') {
|
|
339
|
+
routes[pathWithSlash] = handler;
|
|
340
|
+
}
|
|
292
341
|
}
|
|
293
342
|
else {
|
|
294
|
-
|
|
295
|
-
|
|
343
|
+
routes[normalizedPath][method] = handler;
|
|
344
|
+
if (normalizedPath !== '/') {
|
|
345
|
+
routes[pathWithSlash][method] = handler;
|
|
346
|
+
}
|
|
296
347
|
}
|
|
297
348
|
}
|
|
298
349
|
}
|
|
299
350
|
}
|
|
300
|
-
//
|
|
301
|
-
|
|
351
|
+
// Catch-all handler for unmatched routes (including OPTIONS preflight)
|
|
352
|
+
// Runs global middlewares so CORS and other cross-cutting concerns work for all requests
|
|
353
|
+
routes['/*'] = async (req) => {
|
|
354
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
355
|
+
const requestPath = new URL(req.url).pathname;
|
|
356
|
+
// Filter and run applicable global middlewares
|
|
357
|
+
const applicableMiddlewares = this.globalMiddlewares
|
|
358
|
+
.filter(({ config }) => shouldApplyMiddleware(requestPath, config))
|
|
359
|
+
.map(({ middleware }) => middleware);
|
|
360
|
+
if (applicableMiddlewares.length > 0) {
|
|
361
|
+
const result = await this.executeMiddlewares(context, applicableMiddlewares);
|
|
362
|
+
if (result instanceof Response)
|
|
363
|
+
return result;
|
|
364
|
+
if (result === false) {
|
|
365
|
+
return new Response('Forbidden', { status: 403 });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// No middleware handled the request → error handler or 404
|
|
369
|
+
if (this.errorHandler) {
|
|
370
|
+
return this.errorHandler(new Error('Not Found'), context);
|
|
371
|
+
}
|
|
302
372
|
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
|
303
373
|
status: 404,
|
|
304
374
|
headers: STATIC_JSON_HEADERS,
|
|
@@ -333,7 +403,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
333
403
|
// Initialize path object
|
|
334
404
|
routes[path] = routes[path] || {};
|
|
335
405
|
// Add GET handler for WebSocket upgrade (with middleware chain)
|
|
336
|
-
routes[path]
|
|
406
|
+
routes[path].GET = this.createWebSocketUpgradeHandler(wsRoute);
|
|
337
407
|
}
|
|
338
408
|
return routes;
|
|
339
409
|
}
|
|
@@ -363,15 +433,24 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
363
433
|
* }
|
|
364
434
|
* ```
|
|
365
435
|
*/
|
|
366
|
-
async executeMiddlewares(context, middlewares, index = 0) {
|
|
436
|
+
async executeMiddlewares(context, middlewares, index = 0, onComplete) {
|
|
367
437
|
// Base case: all middlewares executed successfully
|
|
368
438
|
if (index >= middlewares.length) {
|
|
439
|
+
if (onComplete) {
|
|
440
|
+
const result = await onComplete();
|
|
441
|
+
if (result instanceof Response)
|
|
442
|
+
return result;
|
|
443
|
+
if (result === false)
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
369
446
|
return true;
|
|
370
447
|
}
|
|
371
448
|
const middleware = middlewares[index];
|
|
449
|
+
let nextCalled = false;
|
|
372
450
|
// Create next() function that executes the next middleware in chain
|
|
373
451
|
const next = async () => {
|
|
374
|
-
|
|
452
|
+
nextCalled = true;
|
|
453
|
+
const result = await this.executeMiddlewares(context, middlewares, index + 1, onComplete);
|
|
375
454
|
// If next middleware returned Response, throw MiddlewareResponseError to propagate it
|
|
376
455
|
if (result instanceof Response) {
|
|
377
456
|
throw new MiddlewareResponseError(result);
|
|
@@ -392,7 +471,11 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
392
471
|
if (result === false) {
|
|
393
472
|
return false;
|
|
394
473
|
}
|
|
395
|
-
//
|
|
474
|
+
// If middleware returned true/void without calling next(), auto-continue the chain
|
|
475
|
+
if (!nextCalled) {
|
|
476
|
+
return await this.executeMiddlewares(context, middlewares, index + 1, onComplete);
|
|
477
|
+
}
|
|
478
|
+
// Middleware called next() and completed successfully
|
|
396
479
|
return true;
|
|
397
480
|
}
|
|
398
481
|
catch (error) {
|
|
@@ -442,7 +525,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
442
525
|
return async (req) => {
|
|
443
526
|
try {
|
|
444
527
|
// Create context wrapper
|
|
445
|
-
const context = new ErgenecoreContextWrapper(req);
|
|
528
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
446
529
|
// Execute filtered global middlewares with real next() chain
|
|
447
530
|
if (applicableGlobalMiddlewares.length > 0) {
|
|
448
531
|
const result = await this.executeMiddlewares(context, applicableGlobalMiddlewares);
|
|
@@ -522,7 +605,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
522
605
|
// Check if HTTP routes have the same path
|
|
523
606
|
if (httpRoutes[wsPath]) {
|
|
524
607
|
// Check if HTTP route has GET method (collision with WebSocket GET)
|
|
525
|
-
if (httpRoutes[wsPath]
|
|
608
|
+
if (httpRoutes[wsPath].GET) {
|
|
526
609
|
throw new Error(`Route collision detected at path "${wsPath}": Both HTTP and WebSocket routes define GET method. ` +
|
|
527
610
|
`WebSocket routes use GET for upgrade handshake, so HTTP GET cannot be registered on the same path.`);
|
|
528
611
|
}
|
|
@@ -583,14 +666,13 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
583
666
|
// If no error handler is set, use ultra-minimal version with default error handling
|
|
584
667
|
if (!this.errorHandler) {
|
|
585
668
|
return async (req) => {
|
|
586
|
-
const context = new ErgenecoreContextWrapper(req);
|
|
669
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
587
670
|
try {
|
|
588
671
|
// Inject Bun's native route params if present
|
|
589
|
-
// @ts-
|
|
672
|
+
// @ts-expect-error - Bun adds params to Request
|
|
590
673
|
if (req.params) {
|
|
591
|
-
// @ts-
|
|
674
|
+
// @ts-expect-error - Bun adds params to Request
|
|
592
675
|
const params = req.params;
|
|
593
|
-
// eslint-disable-next-line guard-for-in
|
|
594
676
|
for (const key in params) {
|
|
595
677
|
context.setValue(`param:${key}`, params[key]);
|
|
596
678
|
}
|
|
@@ -625,14 +707,13 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
625
707
|
}
|
|
626
708
|
// With error handler (minimal try-catch)
|
|
627
709
|
return async (req) => {
|
|
628
|
-
const context = new ErgenecoreContextWrapper(req);
|
|
710
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
629
711
|
try {
|
|
630
712
|
// Inject Bun's native route params if present
|
|
631
|
-
// @ts-
|
|
713
|
+
// @ts-expect-error - Bun adds params to Request
|
|
632
714
|
if (req.params) {
|
|
633
|
-
// @ts-
|
|
715
|
+
// @ts-expect-error - Bun adds params to Request
|
|
634
716
|
const params = req.params;
|
|
635
|
-
// eslint-disable-next-line guard-for-in
|
|
636
717
|
for (const key in params) {
|
|
637
718
|
context.setValue(`param:${key}`, params[key]);
|
|
638
719
|
}
|
|
@@ -678,65 +759,59 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
678
759
|
const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
|
|
679
760
|
return async (req) => {
|
|
680
761
|
// Create context wrapper outside try block so it's accessible in catch
|
|
681
|
-
const context = new ErgenecoreContextWrapper(req);
|
|
762
|
+
const context = new ErgenecoreContextWrapper(req, this.server);
|
|
682
763
|
// Inject Bun's native route params
|
|
683
|
-
// @ts-
|
|
764
|
+
// @ts-expect-error - Bun adds params to Request
|
|
684
765
|
if (req.params) {
|
|
685
|
-
// @ts-
|
|
766
|
+
// @ts-expect-error - Bun adds params to Request
|
|
686
767
|
const params = req.params;
|
|
687
|
-
// eslint-disable-next-line guard-for-in
|
|
688
768
|
for (const key in params) {
|
|
689
769
|
context.setValue(`param:${key}`, params[key]);
|
|
690
770
|
}
|
|
691
771
|
}
|
|
692
772
|
try {
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
773
|
+
// Combine global + route middlewares into a single chain
|
|
774
|
+
const allMiddlewares = [
|
|
775
|
+
...applicableGlobalMiddlewares,
|
|
776
|
+
...(route.middlewares || []),
|
|
777
|
+
];
|
|
778
|
+
// Execute middleware chain with handler as onComplete callback
|
|
779
|
+
// This ensures the handler runs INSIDE the middleware async context
|
|
780
|
+
const result = await this.executeMiddlewares(context, allMiddlewares, 0, async () => {
|
|
781
|
+
// Execute validation
|
|
782
|
+
if (route.validator) {
|
|
783
|
+
const validationResult = await this.validateRequest(context, route.validator);
|
|
784
|
+
if (validationResult)
|
|
785
|
+
return validationResult;
|
|
703
786
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (result instanceof Response) {
|
|
710
|
-
return result;
|
|
787
|
+
// Handle static file serving
|
|
788
|
+
if (route.staticServe) {
|
|
789
|
+
const staticResponse = await this.serveStaticFile(req, context, route.staticServe);
|
|
790
|
+
if (staticResponse)
|
|
791
|
+
return staticResponse;
|
|
711
792
|
}
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
793
|
+
// Execute route handler
|
|
794
|
+
const response = await route.handler(context);
|
|
795
|
+
// If handler returns Response, return it directly
|
|
796
|
+
if (response instanceof Response) {
|
|
797
|
+
return response;
|
|
715
798
|
}
|
|
799
|
+
// Otherwise, wrap in Response
|
|
800
|
+
return new Response(JSON.stringify(response), {
|
|
801
|
+
status: 200,
|
|
802
|
+
headers: { 'Content-Type': 'application/json' },
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
// If middleware chain returned a custom response, return it
|
|
806
|
+
if (result instanceof Response) {
|
|
807
|
+
return result;
|
|
716
808
|
}
|
|
717
|
-
//
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
if (validationResult)
|
|
721
|
-
return validationResult;
|
|
722
|
-
}
|
|
723
|
-
// Handle static file serving
|
|
724
|
-
if (route.staticServe) {
|
|
725
|
-
const staticResponse = await this.serveStaticFile(req, context, route.staticServe);
|
|
726
|
-
if (staticResponse)
|
|
727
|
-
return staticResponse;
|
|
809
|
+
// If middleware chain returned false (stopped), return 403
|
|
810
|
+
if (result === false) {
|
|
811
|
+
return new Response('Forbidden', { status: 403 });
|
|
728
812
|
}
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
// If handler returns Response, return it directly
|
|
732
|
-
if (response instanceof Response) {
|
|
733
|
-
return response;
|
|
734
|
-
}
|
|
735
|
-
// Otherwise, wrap in Response
|
|
736
|
-
return new Response(JSON.stringify(response), {
|
|
737
|
-
status: 200,
|
|
738
|
-
headers: { 'Content-Type': 'application/json' },
|
|
739
|
-
});
|
|
813
|
+
// Default: result is true (should not happen with onComplete, but just in case)
|
|
814
|
+
return new Response(null, { status: 204 });
|
|
740
815
|
}
|
|
741
816
|
catch (error) {
|
|
742
817
|
// If handler or middleware threw HttpException, convert to Response
|
|
@@ -746,7 +821,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
746
821
|
// Handle other errors with custom error handler if available
|
|
747
822
|
if (this.errorHandler) {
|
|
748
823
|
// Pass the original context with params already injected
|
|
749
|
-
return
|
|
824
|
+
return this.errorHandler(error, context);
|
|
750
825
|
}
|
|
751
826
|
// Default error response
|
|
752
827
|
this.logger.error('Route handler error:', error);
|
|
@@ -1057,7 +1132,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
1057
1132
|
*/
|
|
1058
1133
|
extractBasePath(path) {
|
|
1059
1134
|
// Remove trailing slash (except for root)
|
|
1060
|
-
|
|
1135
|
+
const normalized = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
|
|
1061
1136
|
// Split path into segments
|
|
1062
1137
|
const segments = normalized.split('/');
|
|
1063
1138
|
const baseSegments = [];
|
|
@@ -1355,10 +1430,7 @@ export class Ergenecore extends AsenaAdapter {
|
|
|
1355
1430
|
for (const [controllerName, group] of mixedControllers) {
|
|
1356
1431
|
// Merge WebSocket routes into this controller
|
|
1357
1432
|
const wsGroup = wsGroups.get(controllerName);
|
|
1358
|
-
const allRoutes = [
|
|
1359
|
-
...group.routes,
|
|
1360
|
-
...wsGroup.routes.map((r) => ({ method: 'WS', path: r.path })),
|
|
1361
|
-
];
|
|
1433
|
+
const allRoutes = [...group.routes, ...wsGroup.routes.map((r) => ({ method: 'WS', path: r.path }))];
|
|
1362
1434
|
lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
|
|
1363
1435
|
// Sort routes: GET first, then POST, PUT, PATCH, DELETE, WS
|
|
1364
1436
|
const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'WS'];
|