@martel/calyx 0.1.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.
@@ -0,0 +1,588 @@
1
+ import { calyxContainer } from '../core/container.ts';
2
+ import { METADATA_KEYS } from '../core/metadata.ts';
3
+ import { RadixRouter } from './router.ts';
4
+ import { ParameterConfig } from './decorators.ts';
5
+ import { HttpException, NotFoundException } from './exceptions.ts';
6
+ import { ArgumentsHost, ExecutionContext } from '../lifecycle/interfaces.ts';
7
+ import { calyxArgumentsHost, calyxExecutionContext } from '../lifecycle/context.ts';
8
+
9
+ export class calyxResponse {
10
+ statusCode = 200;
11
+ headers = new Headers();
12
+ body: any = null;
13
+ sent = false;
14
+
15
+ status(code: number) {
16
+ this.statusCode = code;
17
+ return this;
18
+ }
19
+
20
+ send(body: any) {
21
+ this.body = body;
22
+ this.sent = true;
23
+ return this;
24
+ }
25
+
26
+ json(body: any) {
27
+ this.headers.set('content-type', 'application/json');
28
+ this.body = JSON.stringify(body);
29
+ this.sent = true;
30
+ return this;
31
+ }
32
+
33
+ set(name: string, value: string) {
34
+ this.headers.set(name, value);
35
+ return this;
36
+ }
37
+ }
38
+
39
+ interface HandlerConfig {
40
+ instance: any;
41
+ methodName: string;
42
+ paramsConfig: ParameterConfig[];
43
+ httpCode?: number;
44
+ headers: { name: string; value: string }[];
45
+ redirect?: { url: string; statusCode?: number };
46
+ hasBodyParam: boolean;
47
+ hasQueryParam: boolean;
48
+ hasResParam: boolean;
49
+ hasLifecycleMetadata: boolean;
50
+ }
51
+
52
+ export class calyxApplication {
53
+ private container = new calyxContainer();
54
+ private router = new RadixRouter<HandlerConfig>();
55
+ private server: any;
56
+
57
+ private globalGuards: any[] = [];
58
+ private globalInterceptors: any[] = [];
59
+ private globalPipes: any[] = [];
60
+ private globalFilters: any[] = [];
61
+
62
+ useGlobalGuards(...guards: any[]) {
63
+ this.globalGuards.push(...guards);
64
+ }
65
+
66
+ useGlobalInterceptors(...interceptors: any[]) {
67
+ this.globalInterceptors.push(...interceptors);
68
+ }
69
+
70
+ useGlobalPipes(...pipes: any[]) {
71
+ this.globalPipes.push(...pipes);
72
+ }
73
+
74
+ useGlobalFilters(...filters: any[]) {
75
+ this.globalFilters.push(...filters);
76
+ }
77
+
78
+ constructor(private rootModule: any) {}
79
+
80
+ async init() {
81
+ // Bootstrap the dependency injection container
82
+ this.container.bootstrap(this.rootModule);
83
+
84
+ // Build the routing table from registered controllers
85
+ this.buildRoutes();
86
+ }
87
+
88
+ private buildRoutes() {
89
+ const modules = this.container.getModules();
90
+ for (const [moduleClass, record] of modules.entries()) {
91
+ for (const controllerClass of record.controllers) {
92
+ const prefix = Reflect.getMetadata(METADATA_KEYS.CONTROLLER, controllerClass) ?? '';
93
+ const instance = record.instances.get(controllerClass);
94
+ if (!instance) continue;
95
+
96
+ const methods = this.getMethods(controllerClass.prototype);
97
+ for (const method of methods) {
98
+ const routeMeta = Reflect.getMetadata(METADATA_KEYS.HTTP_METHOD, controllerClass.prototype, method);
99
+ if (!routeMeta) continue;
100
+
101
+ // Normalize prefix and path
102
+ const normalizedPrefix = prefix.replace(/^\/|\/$/g, '');
103
+ const normalizedPath = routeMeta.path.replace(/^\/|\/$/g, '');
104
+ const fullPath = '/' + [normalizedPrefix, normalizedPath].filter(Boolean).join('/');
105
+
106
+ const paramsConfig: ParameterConfig[] = Reflect.getMetadata(METADATA_KEYS.HTTP_PARAMS, controllerClass.prototype, method) || [];
107
+ // Sort parameters by index ascending so they map correctly to function arguments
108
+ paramsConfig.sort((a, b) => a.index - b.index);
109
+
110
+ const httpCode = Reflect.getMetadata(METADATA_KEYS.HTTP_CODE, controllerClass.prototype, method);
111
+ const headers = Reflect.getMetadata(METADATA_KEYS.HTTP_HEADERS, controllerClass.prototype, method) || [];
112
+ const redirect = Reflect.getMetadata(METADATA_KEYS.REDIRECT, controllerClass.prototype, method);
113
+
114
+ const hasBodyParam = paramsConfig.some((p) => p.type === 'body');
115
+ const hasQueryParam = paramsConfig.some((p) => p.type === 'query');
116
+ const hasResParam = paramsConfig.some((p) => p.type === 'res');
117
+
118
+ const hasLifecycleMetadata =
119
+ Reflect.hasMetadata(METADATA_KEYS.GUARDS, controllerClass) ||
120
+ Reflect.hasMetadata(METADATA_KEYS.GUARDS, controllerClass.prototype, method) ||
121
+ Reflect.hasMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass) ||
122
+ Reflect.hasMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass.prototype, method) ||
123
+ Reflect.hasMetadata(METADATA_KEYS.PIPES, controllerClass) ||
124
+ Reflect.hasMetadata(METADATA_KEYS.PIPES, controllerClass.prototype, method) ||
125
+ Reflect.hasMetadata(METADATA_KEYS.FILTERS, controllerClass) ||
126
+ Reflect.hasMetadata(METADATA_KEYS.FILTERS, controllerClass.prototype, method) ||
127
+ paramsConfig.some((p) => p.pipes && p.pipes.length > 0);
128
+
129
+ this.router.insert(routeMeta.method, fullPath, {
130
+ instance,
131
+ methodName: method,
132
+ paramsConfig,
133
+ httpCode,
134
+ headers,
135
+ redirect,
136
+ hasBodyParam,
137
+ hasQueryParam,
138
+ hasResParam,
139
+ hasLifecycleMetadata,
140
+ });
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ private getMethods(obj: any): string[] {
147
+ const properties = new Set<string>();
148
+ let currentObj = obj;
149
+ while (currentObj && currentObj !== Object.prototype) {
150
+ Object.getOwnPropertyNames(currentObj).forEach((item) => properties.add(item));
151
+ currentObj = Object.getPrototypeOf(currentObj);
152
+ }
153
+ return Array.from(properties).filter((item) => typeof obj[item] === 'function' && item !== 'constructor');
154
+ }
155
+
156
+ private hasGlobalLifecycle(): boolean {
157
+ return (
158
+ this.globalGuards.length > 0 ||
159
+ this.globalInterceptors.length > 0 ||
160
+ this.globalPipes.length > 0 ||
161
+ this.globalFilters.length > 0
162
+ );
163
+ }
164
+
165
+ private handleRequest(req: Request): Response | Promise<Response> {
166
+ try {
167
+ const urlStr = req.url;
168
+ // Fast path parsing
169
+ let pathname = '/';
170
+ let search = '';
171
+ const protoIdx = urlStr.indexOf('://');
172
+ if (protoIdx !== -1) {
173
+ const slashIdx = urlStr.indexOf('/', protoIdx + 3);
174
+ if (slashIdx !== -1) {
175
+ pathname = urlStr.substring(slashIdx);
176
+ }
177
+ }
178
+ const queryIdx = pathname.indexOf('?');
179
+ if (queryIdx !== -1) {
180
+ search = pathname.substring(queryIdx);
181
+ pathname = pathname.substring(0, queryIdx);
182
+ }
183
+
184
+ const matched = this.router.match(req.method, pathname);
185
+ if (!matched) {
186
+ throw new NotFoundException(`Cannot ${req.method} ${pathname}`);
187
+ }
188
+
189
+ const { handler, params } = matched;
190
+
191
+ // Check if we need to run lifecycle hooks
192
+ const hasLifecycle = handler.hasLifecycleMetadata || this.hasGlobalLifecycle();
193
+ if (hasLifecycle) {
194
+ if (handler.hasBodyParam && req.body && req.method !== 'GET' && req.method !== 'HEAD') {
195
+ return this.handleRequestPipelineAsync(req, handler, params, search);
196
+ }
197
+ return this.handleRequestPipeline(req, handler, params, search, null);
198
+ }
199
+
200
+ // Check if we need to parse body (which makes it async)
201
+ if (handler.hasBodyParam && req.body && req.method !== 'GET' && req.method !== 'HEAD') {
202
+ return this.handleRequestAsync(req, handler, params, search);
203
+ }
204
+
205
+ return this.executeHandlerSync(req, handler, params, search, null);
206
+ } catch (err: any) {
207
+ return this.handleError(err);
208
+ }
209
+ }
210
+
211
+ private async handleRequestPipelineAsync(
212
+ req: Request,
213
+ handler: HandlerConfig,
214
+ params: Record<string, string>,
215
+ search: string
216
+ ): Promise<Response> {
217
+ try {
218
+ let body: any = null;
219
+ const contentType = req.headers.get('content-type') || '';
220
+ if (contentType.includes('application/json')) {
221
+ try {
222
+ body = await req.json();
223
+ } catch {
224
+ body = {};
225
+ }
226
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
227
+ try {
228
+ const text = await req.text();
229
+ body = Object.fromEntries(new URLSearchParams(text).entries());
230
+ } catch {
231
+ body = {};
232
+ }
233
+ } else {
234
+ try {
235
+ body = await req.text();
236
+ } catch {
237
+ body = null;
238
+ }
239
+ }
240
+
241
+ return this.handleRequestPipeline(req, handler, params, search, body);
242
+ } catch (err: any) {
243
+ const resWrapper = new calyxResponse();
244
+ const host = new calyxArgumentsHost(req, resWrapper);
245
+ return this.handleLifecycleError(err, host, handler.instance.constructor, handler.methodName);
246
+ }
247
+ }
248
+
249
+ private async handleRequestPipeline(
250
+ req: Request,
251
+ handler: HandlerConfig,
252
+ params: Record<string, string>,
253
+ search: string,
254
+ body: any
255
+ ): Promise<Response> {
256
+ const { instance, methodName, paramsConfig, httpCode, headers, redirect, hasQueryParam, hasResParam } = handler;
257
+ const controllerClass = instance.constructor;
258
+ const moduleClass = this.getControllerModule(controllerClass);
259
+ const resWrapper = new calyxResponse();
260
+ const host = new calyxArgumentsHost(req, resWrapper);
261
+ const context = new calyxExecutionContext(req, resWrapper, controllerClass, instance[methodName]);
262
+
263
+ try {
264
+ // 1. Run Guards
265
+ const classGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, controllerClass) || [];
266
+ const methodGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, controllerClass.prototype, methodName) || [];
267
+ const guards = [...this.globalGuards, ...classGuards, ...methodGuards];
268
+
269
+ for (const guard of guards) {
270
+ const guardInstance = this.resolveLifecycleInstance(moduleClass, guard);
271
+ const canActivate = await guardInstance.canActivate(context);
272
+ if (!canActivate) {
273
+ throw new HttpException('Forbidden resource', 403);
274
+ }
275
+ }
276
+
277
+ // 2. Run Interceptors & Pipes & Handler
278
+ const classInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass) || [];
279
+ const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass.prototype, methodName) || [];
280
+ const interceptors = [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors];
281
+
282
+ let query: any = null;
283
+ if (hasQueryParam && search) {
284
+ query = Object.fromEntries(new URLSearchParams(search).entries());
285
+ } else if (hasQueryParam) {
286
+ query = {};
287
+ }
288
+
289
+ const paramTypes = Reflect.getMetadata('design:paramtypes', controllerClass.prototype, methodName) || [];
290
+
291
+ // Execute pipes for each argument
292
+ const args: any[] = [];
293
+ for (const config of paramsConfig) {
294
+ let val: any;
295
+ switch (config.type) {
296
+ case 'req':
297
+ val = req;
298
+ break;
299
+ case 'res':
300
+ val = resWrapper;
301
+ break;
302
+ case 'param':
303
+ val = config.name ? params[config.name] : params;
304
+ break;
305
+ case 'query':
306
+ val = config.name ? query?.[config.name] : query;
307
+ break;
308
+ case 'body':
309
+ val = config.name ? body?.[config.name] : body;
310
+ break;
311
+ case 'headers':
312
+ val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries());
313
+ break;
314
+ }
315
+
316
+ // Apply pipes
317
+ const classPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, controllerClass) || [];
318
+ const methodPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, controllerClass.prototype, methodName) || [];
319
+ const paramPipes = config.pipes || [];
320
+ const pipes = [...this.globalPipes, ...classPipes, ...methodPipes, ...paramPipes];
321
+
322
+ for (const pipe of pipes) {
323
+ const pipeInstance = this.resolveLifecycleInstance(moduleClass, pipe);
324
+ val = await pipeInstance.transform(val, {
325
+ type: config.type,
326
+ metatype: paramTypes[config.index],
327
+ data: config.name,
328
+ });
329
+ }
330
+
331
+ args[config.index] = val;
332
+ }
333
+
334
+ let interceptorIndex = 0;
335
+ const executeHandler = async (): Promise<any> => {
336
+ if (interceptorIndex < interceptors.length) {
337
+ const interceptor = interceptors[interceptorIndex++];
338
+ const interceptorInstance = this.resolveLifecycleInstance(moduleClass, interceptor);
339
+ return interceptorInstance.intercept(context, { handle: () => executeHandler() });
340
+ }
341
+ return instance[methodName](...args);
342
+ };
343
+
344
+ const result = await executeHandler();
345
+
346
+ return this.processResult(req, result, resWrapper, httpCode, headers, redirect);
347
+ } catch (err: any) {
348
+ return this.handleLifecycleError(err, host, controllerClass, methodName);
349
+ }
350
+ }
351
+
352
+ private async handleLifecycleError(
353
+ err: any,
354
+ host: ArgumentsHost,
355
+ controllerClass: any,
356
+ methodName: string
357
+ ): Promise<Response> {
358
+ const classFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, controllerClass) || [];
359
+ const methodFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, controllerClass.prototype, methodName) || [];
360
+ const filters = [...this.globalFilters, ...classFilters, ...methodFilters];
361
+
362
+ const moduleClass = this.getControllerModule(controllerClass);
363
+
364
+ for (const filter of filters) {
365
+ const filterInstance = this.resolveLifecycleInstance(moduleClass, filter);
366
+ const catchExceptions = Reflect.getMetadata(METADATA_KEYS.CATCH, filterInstance.constructor) || [];
367
+
368
+ const catchesException =
369
+ catchExceptions.length === 0 ||
370
+ catchExceptions.some((excClass: any) => err instanceof excClass);
371
+
372
+ if (catchesException) {
373
+ await filterInstance.catch(err, host);
374
+ const resWrapper = host.switchToHttp().getResponse<calyxResponse>();
375
+ if (resWrapper.sent) {
376
+ return new Response(resWrapper.body, {
377
+ status: resWrapper.statusCode,
378
+ headers: resWrapper.headers,
379
+ });
380
+ }
381
+ }
382
+ }
383
+
384
+ return this.handleError(err);
385
+ }
386
+
387
+ private getControllerModule(controllerClass: any): any {
388
+ const modules = this.container.getModules();
389
+ for (const [moduleClass, record] of modules.entries()) {
390
+ if (record.controllers.has(controllerClass)) {
391
+ return moduleClass;
392
+ }
393
+ }
394
+ return null;
395
+ }
396
+
397
+ private resolveLifecycleInstance(moduleClass: any, item: any): any {
398
+ if (typeof item === 'function') {
399
+ try {
400
+ return this.container.resolveTokenInModuleContext(moduleClass, item);
401
+ } catch {
402
+ return new item();
403
+ }
404
+ }
405
+ return item;
406
+ }
407
+
408
+ private async handleRequestAsync(
409
+ req: Request,
410
+ handler: HandlerConfig,
411
+ params: Record<string, string>,
412
+ search: string
413
+ ): Promise<Response> {
414
+ try {
415
+ let body: any = null;
416
+ const contentType = req.headers.get('content-type') || '';
417
+ if (contentType.includes('application/json')) {
418
+ try {
419
+ body = await req.json();
420
+ } catch {
421
+ body = {};
422
+ }
423
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
424
+ try {
425
+ const text = await req.text();
426
+ body = Object.fromEntries(new URLSearchParams(text).entries());
427
+ } catch {
428
+ body = {};
429
+ }
430
+ } else {
431
+ try {
432
+ body = await req.text();
433
+ } catch {
434
+ body = null;
435
+ }
436
+ }
437
+
438
+ return this.executeHandlerSync(req, handler, params, search, body);
439
+ } catch (err: any) {
440
+ return this.handleError(err);
441
+ }
442
+ }
443
+
444
+ private executeHandlerSync(
445
+ req: Request,
446
+ handler: HandlerConfig,
447
+ params: Record<string, string>,
448
+ search: string,
449
+ body: any
450
+ ): Response | Promise<Response> {
451
+ const { instance, methodName, paramsConfig, httpCode, headers, redirect, hasQueryParam, hasResParam } = handler;
452
+
453
+ // Parse query on-demand
454
+ let query: any = null;
455
+ if (hasQueryParam && search) {
456
+ query = Object.fromEntries(new URLSearchParams(search).entries());
457
+ } else if (hasQueryParam) {
458
+ query = {};
459
+ }
460
+
461
+ const resWrapper = hasResParam ? new calyxResponse() : null;
462
+ const args: any[] = [];
463
+
464
+ for (const config of paramsConfig) {
465
+ let val: any;
466
+ switch (config.type) {
467
+ case 'req':
468
+ val = req;
469
+ break;
470
+ case 'res':
471
+ val = resWrapper;
472
+ break;
473
+ case 'param':
474
+ val = config.name ? params[config.name] : params;
475
+ break;
476
+ case 'query':
477
+ val = config.name ? query?.[config.name] : query;
478
+ break;
479
+ case 'body':
480
+ val = config.name ? body?.[config.name] : body;
481
+ break;
482
+ case 'headers':
483
+ val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries());
484
+ break;
485
+ }
486
+ args[config.index] = val;
487
+ }
488
+
489
+ const result = instance[methodName](...args);
490
+
491
+ if (result instanceof Promise) {
492
+ return result.then((resolvedResult) => this.processResult(req, resolvedResult, resWrapper, httpCode, headers, redirect));
493
+ }
494
+
495
+ return this.processResult(req, result, resWrapper, httpCode, headers, redirect);
496
+ }
497
+
498
+ private processResult(
499
+ req: Request,
500
+ result: any,
501
+ resWrapper: calyxResponse | null,
502
+ httpCode: number | undefined,
503
+ headers: { name: string; value: string }[],
504
+ redirect: { url: string; statusCode?: number } | undefined
505
+ ): Response {
506
+ if (redirect) {
507
+ return Response.redirect(redirect.url, redirect.statusCode || 302);
508
+ }
509
+
510
+ if (resWrapper?.sent) {
511
+ return new Response(resWrapper.body, {
512
+ status: resWrapper.statusCode,
513
+ headers: resWrapper.headers,
514
+ });
515
+ }
516
+
517
+ const status = httpCode ?? (req.method === 'POST' ? 201 : 200);
518
+
519
+ // Build headers only if we have custom headers or content-type requirements
520
+ let responseHeaders: Headers | undefined = undefined;
521
+ if (headers.length > 0) {
522
+ responseHeaders = new Headers();
523
+ for (const header of headers) {
524
+ responseHeaders.set(header.name, header.value);
525
+ }
526
+ }
527
+
528
+ if (result === undefined || result === null) {
529
+ return new Response(null, { status, headers: responseHeaders });
530
+ }
531
+
532
+ if (result instanceof Response) {
533
+ return result;
534
+ }
535
+
536
+ if (typeof result === 'object') {
537
+ if (responseHeaders) {
538
+ responseHeaders.set('content-type', 'application/json');
539
+ return new Response(JSON.stringify(result), { status, headers: responseHeaders });
540
+ }
541
+ return Response.json(result, { status });
542
+ }
543
+
544
+ if (responseHeaders) {
545
+ return new Response(String(result), { status, headers: responseHeaders });
546
+ }
547
+ return new Response(String(result), { status });
548
+ }
549
+
550
+ private handleError(err: any): Response {
551
+ if (err instanceof HttpException) {
552
+ const response = err.getResponse();
553
+ const status = err.getStatus();
554
+ const body = typeof response === 'object' ? JSON.stringify(response) : JSON.stringify({ statusCode: status, message: response });
555
+ return new Response(body, {
556
+ status,
557
+ headers: { 'content-type': 'application/json' },
558
+ });
559
+ }
560
+
561
+ console.error(err);
562
+ return new Response(
563
+ JSON.stringify({
564
+ statusCode: 500,
565
+ message: 'Internal server error',
566
+ }),
567
+ {
568
+ status: 500,
569
+ headers: { 'content-type': 'application/json' },
570
+ }
571
+ );
572
+ }
573
+
574
+ async listen(port: number): Promise<any> {
575
+ await this.init();
576
+ this.server = Bun.serve({
577
+ port,
578
+ fetch: (req) => this.handleRequest(req),
579
+ });
580
+ return this.server;
581
+ }
582
+
583
+ async close() {
584
+ if (this.server) {
585
+ this.server.stop();
586
+ }
587
+ }
588
+ }
@@ -0,0 +1,103 @@
1
+ import 'reflect-metadata';
2
+ import { METADATA_KEYS } from '../core/metadata.ts';
3
+
4
+ export function Controller(prefix = ''): ClassDecorator {
5
+ return (target) => {
6
+ Reflect.defineMetadata(METADATA_KEYS.CONTROLLER, prefix, target);
7
+ };
8
+ }
9
+
10
+ function createRouteDecorator(method: string) {
11
+ return (path = ''): MethodDecorator => {
12
+ return (target, propertyKey) => {
13
+ Reflect.defineMetadata(METADATA_KEYS.HTTP_METHOD, { method, path }, target, propertyKey);
14
+ };
15
+ };
16
+ }
17
+
18
+ export const Get = createRouteDecorator('GET');
19
+ export const Post = createRouteDecorator('POST');
20
+ export const Put = createRouteDecorator('PUT');
21
+ export const Delete = createRouteDecorator('DELETE');
22
+ export const Patch = createRouteDecorator('PATCH');
23
+ export const Options = createRouteDecorator('OPTIONS');
24
+ export const Head = createRouteDecorator('HEAD');
25
+ export const All = createRouteDecorator('ALL');
26
+
27
+ export interface ParameterConfig {
28
+ index: number;
29
+ type: 'req' | 'res' | 'param' | 'query' | 'body' | 'headers';
30
+ name?: string;
31
+ pipes?: any[];
32
+ }
33
+
34
+ function createParamDecorator(type: ParameterConfig['type'], name?: string, pipes: any[] = []): ParameterDecorator {
35
+ return (target, propertyKey, parameterIndex) => {
36
+ if (!propertyKey) return;
37
+ const existingParams: ParameterConfig[] =
38
+ Reflect.getOwnMetadata(METADATA_KEYS.HTTP_PARAMS, target, propertyKey) || [];
39
+ existingParams.push({ index: parameterIndex, type, name, pipes });
40
+ Reflect.defineMetadata(METADATA_KEYS.HTTP_PARAMS, existingParams, target, propertyKey);
41
+ };
42
+ }
43
+
44
+ function parseParamArgs(first?: any, ...rest: any[]) {
45
+ let name: string | undefined = undefined;
46
+ let pipes: any[] = [];
47
+
48
+ if (typeof first === 'string') {
49
+ name = first;
50
+ pipes = rest;
51
+ } else if (first !== undefined) {
52
+ pipes = [first, ...rest];
53
+ }
54
+
55
+ return { name, pipes };
56
+ }
57
+
58
+ export const Req = () => createParamDecorator('req');
59
+ export const Request = Req;
60
+
61
+ export const Res = () => createParamDecorator('res');
62
+ export const Response = Res;
63
+
64
+ export const Param = (first?: any, ...pipes: any[]) => {
65
+ const { name, pipes: parsedPipes } = parseParamArgs(first, ...pipes);
66
+ return createParamDecorator('param', name, parsedPipes);
67
+ };
68
+
69
+ export const Query = (first?: any, ...pipes: any[]) => {
70
+ const { name, pipes: parsedPipes } = parseParamArgs(first, ...pipes);
71
+ return createParamDecorator('query', name, parsedPipes);
72
+ };
73
+
74
+ export const Body = (first?: any, ...pipes: any[]) => {
75
+ const { name, pipes: parsedPipes } = parseParamArgs(first, ...pipes);
76
+ return createParamDecorator('body', name, parsedPipes);
77
+ };
78
+
79
+ export const Headers = (first?: any, ...pipes: any[]) => {
80
+ const { name, pipes: parsedPipes } = parseParamArgs(first, ...pipes);
81
+ return createParamDecorator('headers', name, parsedPipes);
82
+ };
83
+
84
+ export function HttpCode(code: number): MethodDecorator {
85
+ return (target, propertyKey) => {
86
+ Reflect.defineMetadata(METADATA_KEYS.HTTP_CODE, code, target, propertyKey);
87
+ };
88
+ }
89
+
90
+ export function Header(name: string, value: string): MethodDecorator {
91
+ return (target, propertyKey) => {
92
+ const existingHeaders: { name: string; value: string }[] =
93
+ Reflect.getOwnMetadata(METADATA_KEYS.HTTP_HEADERS, target, propertyKey) || [];
94
+ existingHeaders.push({ name, value });
95
+ Reflect.defineMetadata(METADATA_KEYS.HTTP_HEADERS, existingHeaders, target, propertyKey);
96
+ };
97
+ }
98
+
99
+ export function Redirect(url: string, statusCode = 302): MethodDecorator {
100
+ return (target, propertyKey) => {
101
+ Reflect.defineMetadata(METADATA_KEYS.REDIRECT, { url, statusCode }, target, propertyKey);
102
+ };
103
+ }