@martel/calyx 1.2.3 → 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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/cli/index.ts +3 -2
- package/src/http/application.ts +249 -14
- package/src/http/index.ts +1 -0
- package/src/http/middleware.ts +82 -0
- package/src/http/router.ts +127 -42
- package/src/lifecycle/context.ts +44 -4
- package/tests/middleware.test.ts +160 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/bmartel/calyx/compare/v1.2.4...v1.3.0) (2026-07-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **http:** implement middleware support, JIT radix router compilation, and context pooling ([f0cf712](https://github.com/bmartel/calyx/commit/f0cf7125181749728eda29261e8423cd467b4977))
|
|
7
|
+
|
|
8
|
+
## [1.2.4](https://github.com/bmartel/calyx/compare/v1.2.3...v1.2.4) (2026-07-01)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* make scaffolded project port dynamically read process.env.PORT ([3fbf8a6](https://github.com/bmartel/calyx/commit/3fbf8a6cb4a6be7a0bf8056bbe5fc391d9f77e97))
|
|
14
|
+
|
|
1
15
|
## [1.2.3](https://github.com/bmartel/calyx/compare/v1.2.2...v1.2.3) (2026-07-01)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -247,8 +247,9 @@ import { AppModule } from './app.module';
|
|
|
247
247
|
|
|
248
248
|
async function bootstrap() {
|
|
249
249
|
const app = await CalyxFactory.create(AppModule);
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
const port = process.env.PORT || 3000;
|
|
251
|
+
await app.listen(port);
|
|
252
|
+
console.log(\`Application is running on http://localhost:\${port}\`);
|
|
252
253
|
}
|
|
253
254
|
bootstrap();
|
|
254
255
|
`;
|
package/src/http/application.ts
CHANGED
|
@@ -5,6 +5,20 @@ import { ParameterConfig } from './decorators.ts';
|
|
|
5
5
|
import { HttpException, NotFoundException } from './exceptions.ts';
|
|
6
6
|
import { ArgumentsHost, ExecutionContext } from '../lifecycle/interfaces.ts';
|
|
7
7
|
import { CalyxArgumentsHost, CalyxExecutionContext } from '../lifecycle/context.ts';
|
|
8
|
+
import { CalyxMiddlewareConsumer, MiddlewareConfiguration, RequestMethodMap, CompiledLifecycleItem as CompiledMiddlewareItem } from './middleware.ts';
|
|
9
|
+
|
|
10
|
+
class ObjectPool<T> {
|
|
11
|
+
private pool: T[] = [];
|
|
12
|
+
constructor(private readonly factory: () => T) {}
|
|
13
|
+
|
|
14
|
+
acquire(): T {
|
|
15
|
+
return this.pool.pop() ?? this.factory();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
release(item: T) {
|
|
19
|
+
this.pool.push(item);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
export class CalyxResponse {
|
|
10
24
|
statusCode = 200;
|
|
@@ -65,6 +79,7 @@ interface HandlerConfig {
|
|
|
65
79
|
guardsList: CompiledLifecycleItem[];
|
|
66
80
|
interceptorsList: CompiledLifecycleItem[];
|
|
67
81
|
filtersList: CompiledLifecycleItem[];
|
|
82
|
+
middlewaresList: CompiledLifecycleItem[];
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
export class CalyxApplication {
|
|
@@ -76,6 +91,15 @@ export class CalyxApplication {
|
|
|
76
91
|
private globalInterceptors: any[] = [];
|
|
77
92
|
private globalPipes: any[] = [];
|
|
78
93
|
private globalFilters: any[] = [];
|
|
94
|
+
private globalMiddlewares: any[] = [];
|
|
95
|
+
private middlewareConfigurations: MiddlewareConfiguration[] = [];
|
|
96
|
+
private hostPool = new ObjectPool<CalyxArgumentsHost>(() => new CalyxArgumentsHost());
|
|
97
|
+
private contextPool = new ObjectPool<CalyxExecutionContext>(() => new CalyxExecutionContext());
|
|
98
|
+
|
|
99
|
+
use(...middlewares: any[]) {
|
|
100
|
+
this.globalMiddlewares.push(...middlewares);
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
79
103
|
|
|
80
104
|
useGlobalGuards(...guards: any[]) {
|
|
81
105
|
this.globalGuards.push(...guards);
|
|
@@ -99,6 +123,9 @@ export class CalyxApplication {
|
|
|
99
123
|
// Bootstrap the dependency injection container
|
|
100
124
|
this.container.bootstrap(this.rootModule);
|
|
101
125
|
|
|
126
|
+
// Resolve registered modules middlewares
|
|
127
|
+
this.resolveMiddleware();
|
|
128
|
+
|
|
102
129
|
// Build the routing table from registered controllers
|
|
103
130
|
this.buildRoutes();
|
|
104
131
|
|
|
@@ -116,8 +143,8 @@ export class CalyxApplication {
|
|
|
116
143
|
const prefix = Reflect.getMetadata(METADATA_KEYS.CONTROLLER, controllerClass) ?? '';
|
|
117
144
|
const methods = this.getMethods(controllerClass.prototype);
|
|
118
145
|
const controllerScope = this.container.getControllerScope(controllerClass);
|
|
119
|
-
const
|
|
120
|
-
const
|
|
146
|
+
const isControllerRequestScoped = controllerScope === Scope.REQUEST;
|
|
147
|
+
const singletonInstance = isControllerRequestScoped ? null : record.instances.get(controllerClass);
|
|
121
148
|
|
|
122
149
|
for (const method of methods) {
|
|
123
150
|
const routeMeta = Reflect.getMetadata(METADATA_KEYS.HTTP_METHOD, controllerClass.prototype, method);
|
|
@@ -155,6 +182,8 @@ export class CalyxApplication {
|
|
|
155
182
|
const methodFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, controllerClass.prototype, method) || [];
|
|
156
183
|
const filtersList = this.compileLifecycleItems(moduleClass, [...this.globalFilters, ...classFilters, ...methodFilters]);
|
|
157
184
|
|
|
185
|
+
const middlewaresList = this.getMiddlewaresForRoute(routeMeta.method, fullPath, controllerClass, moduleClass);
|
|
186
|
+
|
|
158
187
|
const paramTypes = Reflect.getMetadata('design:paramtypes', controllerClass.prototype, method) || [];
|
|
159
188
|
const compiledParamsConfig: CompiledParameterConfig[] = paramsConfig.map((p) => {
|
|
160
189
|
const classPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, controllerClass) || [];
|
|
@@ -172,12 +201,27 @@ export class CalyxApplication {
|
|
|
172
201
|
guardsList.length > 0 ||
|
|
173
202
|
interceptorsList.length > 0 ||
|
|
174
203
|
filtersList.length > 0 ||
|
|
204
|
+
middlewaresList.length > 0 ||
|
|
175
205
|
compiledParamsConfig.some((p) => p.pipesList.length > 0);
|
|
176
206
|
|
|
207
|
+
const hasReqScopedGuard = guardsList.some((g) => g.isReqScoped);
|
|
208
|
+
const hasReqScopedInterceptor = interceptorsList.some((i) => i.isReqScoped);
|
|
209
|
+
const hasReqScopedFilter = filtersList.some((f) => f.isReqScoped);
|
|
210
|
+
const hasReqScopedPipe = compiledParamsConfig.some((p) => p.pipesList.some((pi) => pi.isReqScoped));
|
|
211
|
+
const hasReqScopedMiddleware = middlewaresList.some((m) => m.isReqScoped);
|
|
212
|
+
|
|
213
|
+
const isRouteRequestScoped =
|
|
214
|
+
isControllerRequestScoped ||
|
|
215
|
+
hasReqScopedGuard ||
|
|
216
|
+
hasReqScopedInterceptor ||
|
|
217
|
+
hasReqScopedFilter ||
|
|
218
|
+
hasReqScopedPipe ||
|
|
219
|
+
hasReqScopedMiddleware;
|
|
220
|
+
|
|
177
221
|
this.router.insert(routeMeta.method, fullPath, {
|
|
178
222
|
controllerClass,
|
|
179
223
|
moduleClass,
|
|
180
|
-
instance,
|
|
224
|
+
instance: singletonInstance,
|
|
181
225
|
methodName: method,
|
|
182
226
|
paramsConfig: compiledParamsConfig,
|
|
183
227
|
httpCode,
|
|
@@ -187,17 +231,108 @@ export class CalyxApplication {
|
|
|
187
231
|
hasQueryParam,
|
|
188
232
|
hasResParam,
|
|
189
233
|
isPassthrough,
|
|
190
|
-
isRequestScoped,
|
|
234
|
+
isRequestScoped: isRouteRequestScoped,
|
|
191
235
|
hasLifecycleMetadata,
|
|
192
236
|
guardsList,
|
|
193
237
|
interceptorsList,
|
|
194
238
|
filtersList,
|
|
239
|
+
middlewaresList,
|
|
195
240
|
});
|
|
196
241
|
}
|
|
197
242
|
}
|
|
198
243
|
}
|
|
199
244
|
}
|
|
200
245
|
|
|
246
|
+
private resolveMiddleware() {
|
|
247
|
+
const consumer = new CalyxMiddlewareConsumer();
|
|
248
|
+
const modules = this.container.getModules();
|
|
249
|
+
for (const [moduleClass, record] of modules.entries()) {
|
|
250
|
+
const instance = record.instances.get(moduleClass);
|
|
251
|
+
if (instance && typeof instance.configure === 'function') {
|
|
252
|
+
instance.configure(consumer);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.middlewareConfigurations = consumer.getConfigurations();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private getMiddlewaresForRoute(
|
|
259
|
+
method: string,
|
|
260
|
+
routePath: string,
|
|
261
|
+
controllerClass: any,
|
|
262
|
+
moduleClass: any
|
|
263
|
+
): CompiledLifecycleItem[] {
|
|
264
|
+
const matchedMiddlewares: any[] = [...this.globalMiddlewares];
|
|
265
|
+
|
|
266
|
+
for (const config of this.middlewareConfigurations) {
|
|
267
|
+
// 1. Check if excluded
|
|
268
|
+
let isExcluded = false;
|
|
269
|
+
for (const ex of config.exclude) {
|
|
270
|
+
if (typeof ex === 'string') {
|
|
271
|
+
if (this.routePathMatches(routePath, ex)) {
|
|
272
|
+
isExcluded = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// RouteInfo
|
|
277
|
+
const exMethod = RequestMethodMap[ex.method];
|
|
278
|
+
if ((exMethod === 'ALL' || exMethod === method.toUpperCase()) && this.routePathMatches(routePath, ex.path)) {
|
|
279
|
+
isExcluded = true;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (isExcluded) continue;
|
|
286
|
+
|
|
287
|
+
// 2. Check if matched in forRoutes
|
|
288
|
+
let isMatched = false;
|
|
289
|
+
for (const rule of config.forRoutes) {
|
|
290
|
+
if (typeof rule === 'string') {
|
|
291
|
+
if (rule === '*' || this.routePathMatches(routePath, rule)) {
|
|
292
|
+
isMatched = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
} else if (typeof rule === 'function') {
|
|
296
|
+
// It's a controller class
|
|
297
|
+
if (rule === controllerClass) {
|
|
298
|
+
isMatched = true;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
// RouteInfo
|
|
303
|
+
const ruleMethod = RequestMethodMap[rule.method];
|
|
304
|
+
if ((ruleMethod === 'ALL' || ruleMethod === method.toUpperCase()) && this.routePathMatches(routePath, rule.path)) {
|
|
305
|
+
isMatched = true;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (isMatched) {
|
|
312
|
+
matchedMiddlewares.push(...config.middleware);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return this.compileLifecycleItems(moduleClass, matchedMiddlewares);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private routePathMatches(routePath: string, pattern: string): boolean {
|
|
320
|
+
const rPath = routePath.startsWith('/') ? routePath : '/' + routePath;
|
|
321
|
+
const pPath = pattern.startsWith('/') ? pattern : '/' + pattern;
|
|
322
|
+
|
|
323
|
+
if (pPath === '/*' || pPath === '*' || pPath === '/**') {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const regexStr = pPath
|
|
328
|
+
.replace(/\?/g, '\\?')
|
|
329
|
+
.replace(/\/\*/g, '(/.*)?')
|
|
330
|
+
.replace(/:[a-zA-Z0-9_]+/g, '[^/]+');
|
|
331
|
+
|
|
332
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
333
|
+
return regex.test(rPath);
|
|
334
|
+
}
|
|
335
|
+
|
|
201
336
|
private getMethods(obj: any): string[] {
|
|
202
337
|
const properties = new Set<string>();
|
|
203
338
|
let currentObj = obj;
|
|
@@ -238,6 +373,9 @@ export class CalyxApplication {
|
|
|
238
373
|
|
|
239
374
|
const matched = this.router.match(req.method, pathname);
|
|
240
375
|
if (!matched) {
|
|
376
|
+
if (this.globalMiddlewares.length > 0) {
|
|
377
|
+
return this.handleGlobalMiddlewaresAnd404(req, pathname);
|
|
378
|
+
}
|
|
241
379
|
throw new NotFoundException(`Cannot ${req.method} ${pathname}`);
|
|
242
380
|
}
|
|
243
381
|
|
|
@@ -249,13 +387,18 @@ export class CalyxApplication {
|
|
|
249
387
|
if (handler.isRequestScoped) {
|
|
250
388
|
requestContext = new Map<any, any>();
|
|
251
389
|
requestContext.set(REQUEST, req);
|
|
252
|
-
|
|
390
|
+
const controllerScope = this.container.getControllerScope(handler.controllerClass);
|
|
391
|
+
if (controllerScope === Scope.REQUEST) {
|
|
392
|
+
instance = this.container.resolveControllerInRequestContext(handler.moduleClass, handler.controllerClass, requestContext);
|
|
393
|
+
}
|
|
253
394
|
}
|
|
254
395
|
|
|
255
396
|
// Check if we need to run lifecycle hooks
|
|
256
397
|
const hasLifecycle = handler.hasLifecycleMetadata || this.hasGlobalLifecycle();
|
|
257
398
|
if (hasLifecycle) {
|
|
258
|
-
|
|
399
|
+
const needsBodyParsing = (handler.hasBodyParam || handler.middlewaresList.length > 0) &&
|
|
400
|
+
req.body && req.method !== 'GET' && req.method !== 'HEAD';
|
|
401
|
+
if (needsBodyParsing) {
|
|
259
402
|
return this.handleRequestPipelineAsync(req, handler, params, search, instance, requestContext);
|
|
260
403
|
}
|
|
261
404
|
return this.handleRequestPipeline(req, handler, params, search, null, instance, requestContext);
|
|
@@ -304,14 +447,19 @@ export class CalyxApplication {
|
|
|
304
447
|
}
|
|
305
448
|
}
|
|
306
449
|
|
|
307
|
-
return this.handleRequestPipeline(req, handler, params, search, body, instance, requestContext);
|
|
450
|
+
return await this.handleRequestPipeline(req, handler, params, search, body, instance, requestContext);
|
|
308
451
|
} catch (err: any) {
|
|
309
452
|
const resWrapper = new CalyxResponse();
|
|
310
|
-
const host =
|
|
311
|
-
|
|
453
|
+
const host = this.hostPool.acquire();
|
|
454
|
+
host.reset(req, resWrapper);
|
|
455
|
+
try {
|
|
456
|
+
return await this.handleLifecycleError(err, host, handler, requestContext);
|
|
457
|
+
} finally {
|
|
458
|
+
host.clear();
|
|
459
|
+
this.hostPool.release(host);
|
|
460
|
+
}
|
|
312
461
|
}
|
|
313
462
|
}
|
|
314
|
-
|
|
315
463
|
private async handleRequestPipeline(
|
|
316
464
|
req: Request,
|
|
317
465
|
handler: HandlerConfig,
|
|
@@ -325,10 +473,43 @@ export class CalyxApplication {
|
|
|
325
473
|
const controllerClass = handler.controllerClass;
|
|
326
474
|
const moduleClass = handler.moduleClass;
|
|
327
475
|
const resWrapper = new CalyxResponse();
|
|
328
|
-
const host =
|
|
329
|
-
|
|
476
|
+
const host = this.hostPool.acquire();
|
|
477
|
+
host.reset(req, resWrapper);
|
|
478
|
+
const context = this.contextPool.acquire();
|
|
479
|
+
context.resetContext(req, resWrapper, controllerClass, instance[methodName]);
|
|
330
480
|
|
|
331
481
|
try {
|
|
482
|
+
// 0. Run Middleware
|
|
483
|
+
if (handler.middlewaresList.length > 0) {
|
|
484
|
+
let index = 0;
|
|
485
|
+
const next = async (err?: any): Promise<void> => {
|
|
486
|
+
if (err) throw err;
|
|
487
|
+
if (index < handler.middlewaresList.length) {
|
|
488
|
+
const item = handler.middlewaresList[index++];
|
|
489
|
+
const mwInstance = item.isReqScoped
|
|
490
|
+
? this.container.resolveTokenInModuleContext(moduleClass, item.token, requestContext)
|
|
491
|
+
: item.instance;
|
|
492
|
+
|
|
493
|
+
if (typeof mwInstance.use === 'function') {
|
|
494
|
+
await mwInstance.use(req, resWrapper, next);
|
|
495
|
+
} else if (typeof mwInstance === 'function') {
|
|
496
|
+
await mwInstance(req, resWrapper, next);
|
|
497
|
+
} else {
|
|
498
|
+
await next();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
await next();
|
|
503
|
+
|
|
504
|
+
// If middleware sent a response, return it directly
|
|
505
|
+
if (resWrapper.sent) {
|
|
506
|
+
return new Response(resWrapper.body, {
|
|
507
|
+
status: resWrapper.statusCode,
|
|
508
|
+
headers: resWrapper.headers,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
332
513
|
// 1. Run Guards
|
|
333
514
|
for (const guard of handler.guardsList) {
|
|
334
515
|
const guardInstance = guard.isReqScoped
|
|
@@ -411,7 +592,45 @@ export class CalyxApplication {
|
|
|
411
592
|
|
|
412
593
|
return this.processResult(req, result, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam);
|
|
413
594
|
} catch (err: any) {
|
|
414
|
-
return this.handleLifecycleError(err, host, handler, requestContext);
|
|
595
|
+
return await this.handleLifecycleError(err, host, handler, requestContext);
|
|
596
|
+
} finally {
|
|
597
|
+
host.clear();
|
|
598
|
+
context.clearContext();
|
|
599
|
+
this.hostPool.release(host);
|
|
600
|
+
this.contextPool.release(context);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private async handleGlobalMiddlewaresAnd404(req: Request, pathname: string): Promise<Response> {
|
|
605
|
+
const resWrapper = new CalyxResponse();
|
|
606
|
+
try {
|
|
607
|
+
const compiled = this.compileLifecycleItems(this.rootModule, this.globalMiddlewares);
|
|
608
|
+
let index = 0;
|
|
609
|
+
const next = async (err?: any): Promise<void> => {
|
|
610
|
+
if (err) throw err;
|
|
611
|
+
if (index < compiled.length) {
|
|
612
|
+
const item = compiled[index++];
|
|
613
|
+
const mwInstance = item.instance;
|
|
614
|
+
if (typeof mwInstance.use === 'function') {
|
|
615
|
+
await mwInstance.use(req, resWrapper, next);
|
|
616
|
+
} else if (typeof mwInstance === 'function') {
|
|
617
|
+
await mwInstance(req, resWrapper, next);
|
|
618
|
+
} else {
|
|
619
|
+
await next();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
await next();
|
|
624
|
+
|
|
625
|
+
if (resWrapper.sent) {
|
|
626
|
+
return new Response(resWrapper.body, {
|
|
627
|
+
status: resWrapper.statusCode,
|
|
628
|
+
headers: resWrapper.headers,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
throw new NotFoundException(`Cannot ${req.method} ${pathname}`);
|
|
632
|
+
} catch (err: any) {
|
|
633
|
+
return this.handleError(err);
|
|
415
634
|
}
|
|
416
635
|
}
|
|
417
636
|
|
|
@@ -454,6 +673,22 @@ export class CalyxApplication {
|
|
|
454
673
|
private compileLifecycleItems(moduleClass: any, items: any[]): CompiledLifecycleItem[] {
|
|
455
674
|
return items.map((item) => {
|
|
456
675
|
if (typeof item === 'function') {
|
|
676
|
+
const isClass =
|
|
677
|
+
item.prototype && (
|
|
678
|
+
typeof item.prototype.canActivate === 'function' ||
|
|
679
|
+
typeof item.prototype.intercept === 'function' ||
|
|
680
|
+
typeof item.prototype.transform === 'function' ||
|
|
681
|
+
typeof item.prototype.catch === 'function' ||
|
|
682
|
+
typeof item.prototype.use === 'function' ||
|
|
683
|
+
Reflect.hasMetadata(METADATA_KEYS.INJECTABLE, item) ||
|
|
684
|
+
Reflect.hasMetadata(METADATA_KEYS.CONTROLLER, item) ||
|
|
685
|
+
Reflect.hasMetadata(METADATA_KEYS.MODULE, item)
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
if (!isClass) {
|
|
689
|
+
return { isReqScoped: false, token: item, instance: item };
|
|
690
|
+
}
|
|
691
|
+
|
|
457
692
|
const scope = this.container.getProviderScope(item);
|
|
458
693
|
if (scope === Scope.REQUEST) {
|
|
459
694
|
return { isReqScoped: true, token: item, instance: null };
|
|
@@ -609,7 +844,7 @@ export class CalyxApplication {
|
|
|
609
844
|
: (httpCode ?? (req.method === 'POST' ? 201 : 200));
|
|
610
845
|
|
|
611
846
|
// Build headers as a plain object Record<string, string>
|
|
612
|
-
const responseHeaders: Record<string, string> = resWrapper
|
|
847
|
+
const responseHeaders: Record<string, string> = resWrapper
|
|
613
848
|
? { ...resWrapper.headers }
|
|
614
849
|
: {};
|
|
615
850
|
|
package/src/http/index.ts
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Type } from '../core/metadata.ts';
|
|
2
|
+
import { CalyxResponse } from './application.ts';
|
|
3
|
+
|
|
4
|
+
export enum RequestMethod {
|
|
5
|
+
GET = 0,
|
|
6
|
+
POST = 1,
|
|
7
|
+
PUT = 2,
|
|
8
|
+
DELETE = 3,
|
|
9
|
+
PATCH = 4,
|
|
10
|
+
ALL = 5,
|
|
11
|
+
OPTIONS = 6,
|
|
12
|
+
HEAD = 7,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const RequestMethodMap: Record<RequestMethod, string> = {
|
|
16
|
+
[RequestMethod.GET]: 'GET',
|
|
17
|
+
[RequestMethod.POST]: 'POST',
|
|
18
|
+
[RequestMethod.PUT]: 'PUT',
|
|
19
|
+
[RequestMethod.DELETE]: 'DELETE',
|
|
20
|
+
[RequestMethod.PATCH]: 'PATCH',
|
|
21
|
+
[RequestMethod.ALL]: 'ALL',
|
|
22
|
+
[RequestMethod.OPTIONS]: 'OPTIONS',
|
|
23
|
+
[RequestMethod.HEAD]: 'HEAD',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface RouteInfo {
|
|
27
|
+
path: string;
|
|
28
|
+
method: RequestMethod;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NestMiddleware {
|
|
32
|
+
use(req: Request, res: CalyxResponse, next: (error?: any) => void | Promise<void>): any;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MiddlewareConfigProxy {
|
|
36
|
+
exclude(...routes: (string | RouteInfo)[]): MiddlewareConfigProxy;
|
|
37
|
+
forRoutes(...routes: (string | Type<any> | RouteInfo)[]): MiddlewareConsumer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MiddlewareConsumer {
|
|
41
|
+
apply(...middleware: any[]): MiddlewareConfigProxy;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NestModule {
|
|
45
|
+
configure(consumer: MiddlewareConsumer): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MiddlewareConfiguration {
|
|
49
|
+
middleware: any[];
|
|
50
|
+
forRoutes: (string | Type<any> | RouteInfo)[];
|
|
51
|
+
exclude: (string | RouteInfo)[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class CalyxMiddlewareConsumer implements MiddlewareConsumer {
|
|
55
|
+
private configurations: MiddlewareConfiguration[] = [];
|
|
56
|
+
|
|
57
|
+
apply(...middleware: any[]): MiddlewareConfigProxy {
|
|
58
|
+
const config: MiddlewareConfiguration = {
|
|
59
|
+
middleware,
|
|
60
|
+
forRoutes: [],
|
|
61
|
+
exclude: [],
|
|
62
|
+
};
|
|
63
|
+
this.configurations.push(config);
|
|
64
|
+
|
|
65
|
+
const proxy: MiddlewareConfigProxy = {
|
|
66
|
+
exclude: (...routes: (string | RouteInfo)[]) => {
|
|
67
|
+
config.exclude.push(...routes);
|
|
68
|
+
return proxy;
|
|
69
|
+
},
|
|
70
|
+
forRoutes: (...routes: (string | Type<any> | RouteInfo)[]) => {
|
|
71
|
+
config.forRoutes.push(...routes);
|
|
72
|
+
return this;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return proxy;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getConfigurations(): MiddlewareConfiguration[] {
|
|
80
|
+
return this.configurations;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/http/router.ts
CHANGED
|
@@ -14,6 +14,8 @@ class RouterNode<T> {
|
|
|
14
14
|
export class RadixRouter<T> {
|
|
15
15
|
private root = new RouterNode<T>();
|
|
16
16
|
private staticRoutes = new Map<string, T>();
|
|
17
|
+
private handlersArray: T[] = [];
|
|
18
|
+
private compiledMatch: ((method: string, path: string) => RouteMatch<T> | null) | null = null;
|
|
17
19
|
|
|
18
20
|
insert(method: string, path: string, handler: T) {
|
|
19
21
|
const hasParams = path.includes(':') || path.includes('*');
|
|
@@ -49,61 +51,144 @@ export class RadixRouter<T> {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
node.handlers.set(method.toUpperCase(), handler);
|
|
54
|
+
this.compiledMatch = null;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
match(method: string, path: string): RouteMatch<T> | null {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (staticHandler) {
|
|
58
|
-
return { handler: staticHandler, params: {} };
|
|
58
|
+
if (this.compiledMatch) {
|
|
59
|
+
return this.compiledMatch(method, path);
|
|
59
60
|
}
|
|
61
|
+
this.compile();
|
|
62
|
+
return this.compiledMatch!(method, path);
|
|
63
|
+
}
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
65
|
+
compile() {
|
|
66
|
+
this.handlersArray = [];
|
|
67
|
+
const registerHandler = (handler: T): number => {
|
|
68
|
+
let idx = this.handlersArray.indexOf(handler);
|
|
69
|
+
if (idx === -1) {
|
|
70
|
+
idx = this.handlersArray.length;
|
|
71
|
+
this.handlersArray.push(handler);
|
|
72
|
+
}
|
|
73
|
+
return idx;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// 1. Compile static routes check
|
|
77
|
+
const staticRoutesByMethod: Record<string, Record<string, number>> = {};
|
|
78
|
+
for (const [key, handler] of this.staticRoutes.entries()) {
|
|
79
|
+
const spaceIdx = key.indexOf(' ');
|
|
80
|
+
const method = key.substring(0, spaceIdx);
|
|
81
|
+
const path = key.substring(spaceIdx + 1);
|
|
82
|
+
const handlerIdx = registerHandler(handler);
|
|
83
|
+
|
|
84
|
+
if (!staticRoutesByMethod[method]) {
|
|
85
|
+
staticRoutesByMethod[method] = {};
|
|
86
|
+
}
|
|
87
|
+
staticRoutesByMethod[method][path] = handlerIdx;
|
|
88
|
+
}
|
|
64
89
|
|
|
65
|
-
|
|
90
|
+
let staticCheckCode = '';
|
|
91
|
+
if (Object.keys(staticRoutesByMethod).length > 0) {
|
|
92
|
+
staticCheckCode = `
|
|
93
|
+
switch (method) {
|
|
94
|
+
`;
|
|
95
|
+
for (const [method, routes] of Object.entries(staticRoutesByMethod)) {
|
|
96
|
+
staticCheckCode += ` case '${method}':\n switch (path) {\n`;
|
|
97
|
+
for (const [path, handlerIdx] of Object.entries(routes)) {
|
|
98
|
+
staticCheckCode += ` case '${path}': return { handler: handlers[${handlerIdx}], params: {} };\n`;
|
|
99
|
+
}
|
|
100
|
+
staticCheckCode += ` }\n break;\n`;
|
|
101
|
+
}
|
|
102
|
+
staticCheckCode += `}\n`;
|
|
103
|
+
}
|
|
66
104
|
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
// 2. Compile dynamic routes recursively
|
|
106
|
+
const compileNode = (node: RouterNode<T>, segmentIdx: number, startVar: string, collectedParams: string[]): string => {
|
|
107
|
+
const parts: string[] = [];
|
|
108
|
+
const nextStartVar = `s${segmentIdx + 1}`;
|
|
109
|
+
const nextEndVar = `e${segmentIdx + 1}`;
|
|
110
|
+
|
|
111
|
+
parts.push(`const ${nextEndVar} = path.indexOf('/', ${startVar});`);
|
|
112
|
+
parts.push(`const ${nextStartVar} = ${nextEndVar} === -1 ? len : ${nextEndVar};`);
|
|
113
|
+
parts.push(`const len${segmentIdx} = ${nextStartVar} - (${startVar});`);
|
|
114
|
+
|
|
115
|
+
// Static children check
|
|
116
|
+
if (node.children.size > 0) {
|
|
117
|
+
for (const [segment, childNode] of node.children.entries()) {
|
|
118
|
+
parts.push(`if (len${segmentIdx} === ${segment.length} && path.startsWith('${segment}', ${startVar})) {`);
|
|
119
|
+
parts.push(compileNode(childNode, segmentIdx + 1, `${nextStartVar} + 1`, collectedParams));
|
|
120
|
+
parts.push(`}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
69
123
|
|
|
70
|
-
|
|
71
|
-
|
|
124
|
+
// Parameter child check
|
|
125
|
+
if (node.paramChild && node.paramName) {
|
|
126
|
+
parts.push(`if (len${segmentIdx} > 0) {`);
|
|
127
|
+
parts.push(` const p_${node.paramName} = path.substring(${startVar}, ${nextStartVar});`);
|
|
128
|
+
parts.push(compileNode(node.paramChild, segmentIdx + 1, `${nextStartVar} + 1`, [...collectedParams, node.paramName]));
|
|
129
|
+
parts.push(`}`);
|
|
130
|
+
}
|
|
72
131
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
132
|
+
// Wildcard child check
|
|
133
|
+
if (node.wildcardChild) {
|
|
134
|
+
parts.push(`const p_wildcard = path.substring(${startVar});`);
|
|
135
|
+
const paramsEntries = [...collectedParams.map(p => `${p}: p_${p}`), `'*': p_wildcard`].join(', ');
|
|
136
|
+
parts.push(`const params = { ${paramsEntries} };`);
|
|
137
|
+
parts.push(`switch (method) {`);
|
|
138
|
+
for (const [method, handler] of node.wildcardChild.handlers.entries()) {
|
|
139
|
+
const handlerIdx = registerHandler(handler);
|
|
140
|
+
parts.push(` case '${method}': return { handler: handlers[${handlerIdx}], params };`);
|
|
141
|
+
}
|
|
142
|
+
const fallbackWildcardHandler = node.wildcardChild.handlers.get('ALL') ?? node.wildcardChild.handlers.values().next().value;
|
|
143
|
+
if (fallbackWildcardHandler) {
|
|
144
|
+
const handlerIdx = registerHandler(fallbackWildcardHandler);
|
|
145
|
+
parts.push(` default: return { handler: handlers[${handlerIdx}], params };`);
|
|
146
|
+
}
|
|
147
|
+
parts.push(`}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Terminal handler check
|
|
151
|
+
if (node.handlers.size > 0) {
|
|
152
|
+
parts.push(`if (${nextStartVar} === len) {`);
|
|
153
|
+
const paramsEntries = collectedParams.map(p => `${p}: p_${p}`).join(', ');
|
|
154
|
+
parts.push(` const params = { ${paramsEntries} };`);
|
|
155
|
+
parts.push(` switch (method) {`);
|
|
156
|
+
for (const [method, handler] of node.handlers.entries()) {
|
|
157
|
+
const handlerIdx = registerHandler(handler);
|
|
158
|
+
parts.push(` case '${method}': return { handler: handlers[${handlerIdx}], params };`);
|
|
159
|
+
}
|
|
160
|
+
const fallbackHandler = node.handlers.get('ALL') ?? node.handlers.values().next().value;
|
|
161
|
+
if (fallbackHandler) {
|
|
162
|
+
const handlerIdx = registerHandler(fallbackHandler);
|
|
163
|
+
parts.push(` default: return { handler: handlers[${handlerIdx}], params };`);
|
|
164
|
+
}
|
|
165
|
+
parts.push(` }`);
|
|
166
|
+
parts.push(`}`);
|
|
167
|
+
}
|
|
83
168
|
|
|
84
|
-
|
|
169
|
+
return parts.join('\n');
|
|
170
|
+
};
|
|
85
171
|
|
|
86
|
-
|
|
87
|
-
const staticChild = node.children.get(segment);
|
|
88
|
-
if (staticChild) {
|
|
89
|
-
const match = this.matchSegment(staticChild, segments, index + 1, params);
|
|
90
|
-
if (match) return match;
|
|
91
|
-
}
|
|
172
|
+
const dynamicMatchCode = compileNode(this.root, 0, '1', []);
|
|
92
173
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
174
|
+
const fnBody = `
|
|
175
|
+
const len = path.length;
|
|
176
|
+
${staticCheckCode}
|
|
177
|
+
${dynamicMatchCode}
|
|
178
|
+
return null;
|
|
179
|
+
`;
|
|
100
180
|
|
|
101
|
-
// 3. Try wildcard match
|
|
102
|
-
if (node.wildcardChild) {
|
|
103
|
-
params['*'] = segments.slice(index).join('/');
|
|
104
|
-
return node.wildcardChild;
|
|
105
|
-
}
|
|
106
181
|
|
|
107
|
-
|
|
182
|
+
try {
|
|
183
|
+
this.compiledMatch = new Function('handlers', `
|
|
184
|
+
return function match(method, path) {
|
|
185
|
+
${fnBody}
|
|
186
|
+
}
|
|
187
|
+
`)(this.handlersArray);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error('Failed to compile radix router matcher:', err);
|
|
190
|
+
console.error('Generated function body was:', fnBody);
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
108
193
|
}
|
|
109
194
|
}
|
package/src/lifecycle/context.ts
CHANGED
|
@@ -2,7 +2,24 @@ import { ArgumentsHost, HttpArgumentsHost, ExecutionContext } from './interfaces
|
|
|
2
2
|
import { Type } from '../core/metadata.ts';
|
|
3
3
|
|
|
4
4
|
export class CalyxArgumentsHost implements ArgumentsHost {
|
|
5
|
-
|
|
5
|
+
protected req!: Request;
|
|
6
|
+
protected res!: any;
|
|
7
|
+
|
|
8
|
+
constructor(req?: Request, res?: any) {
|
|
9
|
+
if (req && res) {
|
|
10
|
+
this.reset(req, res);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
reset(req: Request, res: any) {
|
|
15
|
+
this.req = req;
|
|
16
|
+
this.res = res;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
clear() {
|
|
20
|
+
this.req = null as any;
|
|
21
|
+
this.res = null;
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
getArgs<T extends any[] = any[]>(): T {
|
|
8
25
|
return [this.req, this.res] as unknown as T;
|
|
@@ -22,13 +39,36 @@ export class CalyxArgumentsHost implements ArgumentsHost {
|
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
export class CalyxExecutionContext extends CalyxArgumentsHost implements ExecutionContext {
|
|
42
|
+
protected targetClass!: Type<any>;
|
|
43
|
+
protected handlerMethod!: Function;
|
|
44
|
+
|
|
25
45
|
constructor(
|
|
46
|
+
req?: Request,
|
|
47
|
+
res?: any,
|
|
48
|
+
targetClass?: Type<any>,
|
|
49
|
+
handlerMethod?: Function
|
|
50
|
+
) {
|
|
51
|
+
super(req, res);
|
|
52
|
+
if (targetClass && handlerMethod) {
|
|
53
|
+
this.resetContext(req!, res, targetClass, handlerMethod);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resetContext(
|
|
26
58
|
req: Request,
|
|
27
59
|
res: any,
|
|
28
|
-
|
|
29
|
-
|
|
60
|
+
targetClass: Type<any>,
|
|
61
|
+
handlerMethod: Function
|
|
30
62
|
) {
|
|
31
|
-
|
|
63
|
+
this.reset(req, res);
|
|
64
|
+
this.targetClass = targetClass;
|
|
65
|
+
this.handlerMethod = handlerMethod;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clearContext() {
|
|
69
|
+
this.clear();
|
|
70
|
+
this.targetClass = null as any;
|
|
71
|
+
this.handlerMethod = null as any;
|
|
32
72
|
}
|
|
33
73
|
|
|
34
74
|
getClass<T = any>(): Type<T> {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
Module,
|
|
4
|
+
Controller,
|
|
5
|
+
Get,
|
|
6
|
+
Post,
|
|
7
|
+
Injectable,
|
|
8
|
+
CalyxFactory,
|
|
9
|
+
CalyxResponse,
|
|
10
|
+
NestMiddleware,
|
|
11
|
+
MiddlewareConsumer,
|
|
12
|
+
NestModule,
|
|
13
|
+
RequestMethod,
|
|
14
|
+
Scope,
|
|
15
|
+
} from '../src/index.ts';
|
|
16
|
+
|
|
17
|
+
// 1. Functional Global Middleware
|
|
18
|
+
function globalMiddleware(req: Request, res: CalyxResponse, next: () => void) {
|
|
19
|
+
res.headers['x-global-middleware'] = 'run';
|
|
20
|
+
next();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 2. Class Middleware (Injectable)
|
|
24
|
+
@Injectable()
|
|
25
|
+
class LoggerMiddleware implements NestMiddleware {
|
|
26
|
+
use(req: Request, res: CalyxResponse, next: () => void) {
|
|
27
|
+
res.headers['x-logger-middleware'] = 'run';
|
|
28
|
+
next();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 3. Request Scoped Middleware
|
|
33
|
+
let requestScopedCount = 0;
|
|
34
|
+
|
|
35
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
36
|
+
class RequestScopedMiddleware implements NestMiddleware {
|
|
37
|
+
constructor() {
|
|
38
|
+
requestScopedCount++;
|
|
39
|
+
}
|
|
40
|
+
use(req: Request, res: CalyxResponse, next: () => void) {
|
|
41
|
+
res.headers['x-request-scoped-count'] = String(requestScopedCount);
|
|
42
|
+
next();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 4. Short-circuiting Middleware
|
|
47
|
+
@Injectable()
|
|
48
|
+
class AuthMiddleware implements NestMiddleware {
|
|
49
|
+
use(req: Request, res: CalyxResponse, next: () => void) {
|
|
50
|
+
const token = req.headers.get('x-auth-token');
|
|
51
|
+
if (token !== 'secret') {
|
|
52
|
+
res.status(401).json({ message: 'Unauthorized Middleware' });
|
|
53
|
+
return; // Do not call next()
|
|
54
|
+
}
|
|
55
|
+
next();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Controller('users')
|
|
60
|
+
class UsersController {
|
|
61
|
+
@Get()
|
|
62
|
+
findAll() {
|
|
63
|
+
return { data: 'users' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Get(':id')
|
|
67
|
+
findOne() {
|
|
68
|
+
return { data: 'user' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Post()
|
|
72
|
+
create() {
|
|
73
|
+
return { created: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Controller('admin')
|
|
78
|
+
class AdminController {
|
|
79
|
+
@Get('dashboard')
|
|
80
|
+
dashboard() {
|
|
81
|
+
return { admin: true };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Module({
|
|
86
|
+
controllers: [UsersController, AdminController],
|
|
87
|
+
providers: [LoggerMiddleware, RequestScopedMiddleware, AuthMiddleware],
|
|
88
|
+
})
|
|
89
|
+
class AppModule implements NestModule {
|
|
90
|
+
configure(consumer: MiddlewareConsumer) {
|
|
91
|
+
// Apply LoggerMiddleware to all routes under UsersController
|
|
92
|
+
consumer.apply(LoggerMiddleware).forRoutes(UsersController);
|
|
93
|
+
|
|
94
|
+
// Apply RequestScopedMiddleware to users POST route
|
|
95
|
+
consumer.apply(RequestScopedMiddleware).forRoutes({ path: 'users', method: RequestMethod.POST });
|
|
96
|
+
|
|
97
|
+
// Apply AuthMiddleware to dashboard, but exclude specific checks if needed
|
|
98
|
+
consumer.apply(AuthMiddleware)
|
|
99
|
+
.exclude('admin/dashboard/free')
|
|
100
|
+
.forRoutes('admin/*');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe('NestJS Middleware Compatibility', () => {
|
|
105
|
+
let app: any;
|
|
106
|
+
let baseUrl: string;
|
|
107
|
+
const PORT = 3855;
|
|
108
|
+
|
|
109
|
+
beforeAll(async () => {
|
|
110
|
+
app = await CalyxFactory.create(AppModule);
|
|
111
|
+
app.use(globalMiddleware);
|
|
112
|
+
await app.listen(PORT);
|
|
113
|
+
baseUrl = `http://localhost:${PORT}`;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterAll(async () => {
|
|
117
|
+
await app.close();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should run global functional middleware on any request', async () => {
|
|
121
|
+
const res = await fetch(`${baseUrl}/users`);
|
|
122
|
+
expect(res.status).toBe(200);
|
|
123
|
+
expect(res.headers.get('x-global-middleware')).toBe('run');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should apply class middleware to controller routes configured via forRoutes(Controller)', async () => {
|
|
127
|
+
const res = await fetch(`${baseUrl}/users`);
|
|
128
|
+
expect(res.headers.get('x-logger-middleware')).toBe('run');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should apply request-scoped middleware to specific route configured with RequestMethod', async () => {
|
|
132
|
+
const prevCount = requestScopedCount;
|
|
133
|
+
const res = await fetch(`${baseUrl}/users`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
body: JSON.stringify({}),
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
});
|
|
138
|
+
expect(res.status).toBe(201);
|
|
139
|
+
expect(res.headers.get('x-request-scoped-count')).toBeDefined();
|
|
140
|
+
expect(requestScopedCount).toBe(prevCount + 1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('should short-circuit pipeline and return custom response if middleware does not call next()', async () => {
|
|
144
|
+
const res = await fetch(`${baseUrl}/admin/dashboard`, {
|
|
145
|
+
headers: { 'x-auth-token': 'bad' },
|
|
146
|
+
});
|
|
147
|
+
expect(res.status).toBe(401);
|
|
148
|
+
const body = await res.json();
|
|
149
|
+
expect(body).toEqual({ message: 'Unauthorized Middleware' });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('should continue to handler if short-circuiting middleware passes', async () => {
|
|
153
|
+
const res = await fetch(`${baseUrl}/admin/dashboard`, {
|
|
154
|
+
headers: { 'x-auth-token': 'secret' },
|
|
155
|
+
});
|
|
156
|
+
expect(res.status).toBe(200);
|
|
157
|
+
const body = await res.json();
|
|
158
|
+
expect(body).toEqual({ admin: true });
|
|
159
|
+
});
|
|
160
|
+
});
|