@martel/calyx 0.1.0 → 1.0.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.
@@ -1,12 +1,12 @@
1
- import { calyxContainer } from '../core/container.ts';
2
- import { METADATA_KEYS } from '../core/metadata.ts';
1
+ import { CalyxContainer } from '../core/container.ts';
2
+ import { METADATA_KEYS, Scope, REQUEST } from '../core/metadata.ts';
3
3
  import { RadixRouter } from './router.ts';
4
4
  import { ParameterConfig } from './decorators.ts';
5
5
  import { HttpException, NotFoundException } from './exceptions.ts';
6
6
  import { ArgumentsHost, ExecutionContext } from '../lifecycle/interfaces.ts';
7
- import { calyxArgumentsHost, calyxExecutionContext } from '../lifecycle/context.ts';
7
+ import { CalyxArgumentsHost, CalyxExecutionContext } from '../lifecycle/context.ts';
8
8
 
9
- export class calyxResponse {
9
+ export class CalyxResponse {
10
10
  statusCode = 200;
11
11
  headers = new Headers();
12
12
  body: any = null;
@@ -36,21 +36,39 @@ export class calyxResponse {
36
36
  }
37
37
  }
38
38
 
39
+ interface CompiledLifecycleItem {
40
+ isReqScoped: boolean;
41
+ token: any;
42
+ instance: any;
43
+ }
44
+
45
+ interface CompiledParameterConfig extends ParameterConfig {
46
+ pipesList: CompiledLifecycleItem[];
47
+ paramType: any;
48
+ }
49
+
39
50
  interface HandlerConfig {
51
+ controllerClass: any;
52
+ moduleClass: any;
40
53
  instance: any;
41
54
  methodName: string;
42
- paramsConfig: ParameterConfig[];
55
+ paramsConfig: CompiledParameterConfig[];
43
56
  httpCode?: number;
44
57
  headers: { name: string; value: string }[];
45
58
  redirect?: { url: string; statusCode?: number };
46
59
  hasBodyParam: boolean;
47
60
  hasQueryParam: boolean;
48
61
  hasResParam: boolean;
62
+ isPassthrough: boolean;
63
+ isRequestScoped: boolean;
49
64
  hasLifecycleMetadata: boolean;
65
+ guardsList: CompiledLifecycleItem[];
66
+ interceptorsList: CompiledLifecycleItem[];
67
+ filtersList: CompiledLifecycleItem[];
50
68
  }
51
69
 
52
- export class calyxApplication {
53
- private container = new calyxContainer();
70
+ export class CalyxApplication {
71
+ private container = new CalyxContainer();
54
72
  private router = new RadixRouter<HandlerConfig>();
55
73
  private server: any;
56
74
 
@@ -83,6 +101,12 @@ export class calyxApplication {
83
101
 
84
102
  // Build the routing table from registered controllers
85
103
  this.buildRoutes();
104
+
105
+ // Call OnModuleInit hooks
106
+ await this.runOnModuleInit();
107
+
108
+ // Call OnApplicationBootstrap hooks
109
+ await this.runOnApplicationBootstrap();
86
110
  }
87
111
 
88
112
  private buildRoutes() {
@@ -90,10 +114,11 @@ export class calyxApplication {
90
114
  for (const [moduleClass, record] of modules.entries()) {
91
115
  for (const controllerClass of record.controllers) {
92
116
  const prefix = Reflect.getMetadata(METADATA_KEYS.CONTROLLER, controllerClass) ?? '';
93
- const instance = record.instances.get(controllerClass);
94
- if (!instance) continue;
95
-
96
117
  const methods = this.getMethods(controllerClass.prototype);
118
+ const controllerScope = this.container.getControllerScope(controllerClass);
119
+ const isRequestScoped = controllerScope === Scope.REQUEST;
120
+ const instance = isRequestScoped ? null : record.instances.get(controllerClass);
121
+
97
122
  for (const method of methods) {
98
123
  const routeMeta = Reflect.getMetadata(METADATA_KEYS.HTTP_METHOD, controllerClass.prototype, method);
99
124
  if (!routeMeta) continue;
@@ -114,29 +139,59 @@ export class calyxApplication {
114
139
  const hasBodyParam = paramsConfig.some((p) => p.type === 'body');
115
140
  const hasQueryParam = paramsConfig.some((p) => p.type === 'query');
116
141
  const hasResParam = paramsConfig.some((p) => p.type === 'res');
142
+ const resParamConfig = paramsConfig.find((p) => p.type === 'res');
143
+ const isPassthrough = resParamConfig?.name === 'passthrough';
144
+
145
+ // Compile lifecycle lists
146
+ const classGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, controllerClass) || [];
147
+ const methodGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, controllerClass.prototype, method) || [];
148
+ const guardsList = this.compileLifecycleItems(moduleClass, [...this.globalGuards, ...classGuards, ...methodGuards]);
149
+
150
+ const classInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass) || [];
151
+ const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, controllerClass.prototype, method) || [];
152
+ const interceptorsList = this.compileLifecycleItems(moduleClass, [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors]);
153
+
154
+ const classFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, controllerClass) || [];
155
+ const methodFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, controllerClass.prototype, method) || [];
156
+ const filtersList = this.compileLifecycleItems(moduleClass, [...this.globalFilters, ...classFilters, ...methodFilters]);
157
+
158
+ const paramTypes = Reflect.getMetadata('design:paramtypes', controllerClass.prototype, method) || [];
159
+ const compiledParamsConfig: CompiledParameterConfig[] = paramsConfig.map((p) => {
160
+ const classPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, controllerClass) || [];
161
+ const methodPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, controllerClass.prototype, method) || [];
162
+ const paramPipes = p.pipes || [];
163
+ const allPipes = [...this.globalPipes, ...classPipes, ...methodPipes, ...paramPipes];
164
+ return {
165
+ ...p,
166
+ pipesList: this.compileLifecycleItems(moduleClass, allPipes),
167
+ paramType: paramTypes[p.index],
168
+ };
169
+ });
117
170
 
118
171
  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);
172
+ guardsList.length > 0 ||
173
+ interceptorsList.length > 0 ||
174
+ filtersList.length > 0 ||
175
+ compiledParamsConfig.some((p) => p.pipesList.length > 0);
128
176
 
129
177
  this.router.insert(routeMeta.method, fullPath, {
178
+ controllerClass,
179
+ moduleClass,
130
180
  instance,
131
181
  methodName: method,
132
- paramsConfig,
182
+ paramsConfig: compiledParamsConfig,
133
183
  httpCode,
134
184
  headers,
135
185
  redirect,
136
186
  hasBodyParam,
137
187
  hasQueryParam,
138
188
  hasResParam,
189
+ isPassthrough,
190
+ isRequestScoped,
139
191
  hasLifecycleMetadata,
192
+ guardsList,
193
+ interceptorsList,
194
+ filtersList,
140
195
  });
141
196
  }
142
197
  }
@@ -188,21 +243,30 @@ export class calyxApplication {
188
243
 
189
244
  const { handler, params } = matched;
190
245
 
246
+ let instance = handler.instance;
247
+ let requestContext: Map<any, any> | undefined = undefined;
248
+
249
+ if (handler.isRequestScoped) {
250
+ requestContext = new Map<any, any>();
251
+ requestContext.set(REQUEST, req);
252
+ instance = this.container.resolveControllerInRequestContext(handler.moduleClass, handler.controllerClass, requestContext);
253
+ }
254
+
191
255
  // Check if we need to run lifecycle hooks
192
256
  const hasLifecycle = handler.hasLifecycleMetadata || this.hasGlobalLifecycle();
193
257
  if (hasLifecycle) {
194
258
  if (handler.hasBodyParam && req.body && req.method !== 'GET' && req.method !== 'HEAD') {
195
- return this.handleRequestPipelineAsync(req, handler, params, search);
259
+ return this.handleRequestPipelineAsync(req, handler, params, search, instance, requestContext);
196
260
  }
197
- return this.handleRequestPipeline(req, handler, params, search, null);
261
+ return this.handleRequestPipeline(req, handler, params, search, null, instance, requestContext);
198
262
  }
199
263
 
200
264
  // Check if we need to parse body (which makes it async)
201
265
  if (handler.hasBodyParam && req.body && req.method !== 'GET' && req.method !== 'HEAD') {
202
- return this.handleRequestAsync(req, handler, params, search);
266
+ return this.handleRequestAsync(req, handler, params, search, instance, requestContext);
203
267
  }
204
268
 
205
- return this.executeHandlerSync(req, handler, params, search, null);
269
+ return this.executeHandlerSync(req, handler, params, search, null, instance, requestContext);
206
270
  } catch (err: any) {
207
271
  return this.handleError(err);
208
272
  }
@@ -212,7 +276,9 @@ export class calyxApplication {
212
276
  req: Request,
213
277
  handler: HandlerConfig,
214
278
  params: Record<string, string>,
215
- search: string
279
+ search: string,
280
+ instance: any,
281
+ requestContext?: Map<any, any>
216
282
  ): Promise<Response> {
217
283
  try {
218
284
  let body: any = null;
@@ -238,11 +304,11 @@ export class calyxApplication {
238
304
  }
239
305
  }
240
306
 
241
- return this.handleRequestPipeline(req, handler, params, search, body);
307
+ return this.handleRequestPipeline(req, handler, params, search, body, instance, requestContext);
242
308
  } 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);
309
+ const resWrapper = new CalyxResponse();
310
+ const host = new CalyxArgumentsHost(req, resWrapper);
311
+ return this.handleLifecycleError(err, host, handler.controllerClass, handler.methodName, requestContext);
246
312
  }
247
313
  }
248
314
 
@@ -251,34 +317,32 @@ export class calyxApplication {
251
317
  handler: HandlerConfig,
252
318
  params: Record<string, string>,
253
319
  search: string,
254
- body: any
320
+ body: any,
321
+ instance: any,
322
+ requestContext?: Map<any, any>
255
323
  ): 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]);
324
+ const { methodName, paramsConfig, httpCode, headers, redirect, hasQueryParam } = handler;
325
+ const controllerClass = handler.controllerClass;
326
+ const moduleClass = handler.moduleClass;
327
+ const resWrapper = new CalyxResponse();
328
+ const host = new CalyxArgumentsHost(req, resWrapper);
329
+ const context = new CalyxExecutionContext(req, resWrapper, controllerClass, instance[methodName]);
262
330
 
263
331
  try {
264
332
  // 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) {
333
+ for (const guard of handler.guardsList) {
334
+ const guardInstance = guard.isReqScoped
335
+ ? this.container.resolveTokenInModuleContext(moduleClass, guard.token, requestContext)
336
+ : guard.instance;
337
+ const canActivate = guardInstance.canActivate(context);
338
+ if (canActivate instanceof Promise) {
339
+ const resolved = await canActivate;
340
+ if (!resolved) throw new HttpException('Forbidden resource', 403);
341
+ } else if (!canActivate) {
273
342
  throw new HttpException('Forbidden resource', 403);
274
343
  }
275
344
  }
276
345
 
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
346
  let query: any = null;
283
347
  if (hasQueryParam && search) {
284
348
  query = Object.fromEntries(new URLSearchParams(search).entries());
@@ -286,83 +350,83 @@ export class calyxApplication {
286
350
  query = {};
287
351
  }
288
352
 
289
- const paramTypes = Reflect.getMetadata('design:paramtypes', controllerClass.prototype, methodName) || [];
290
-
291
353
  // Execute pipes for each argument
292
354
  const args: any[] = [];
293
355
  for (const config of paramsConfig) {
294
356
  let val: any;
295
357
  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;
358
+ case 'req': val = req; break;
359
+ case 'res': val = resWrapper; break;
360
+ case 'param': val = config.name ? params[config.name] : params; break;
361
+ case 'query': val = config.name ? query?.[config.name] : query; break;
362
+ case 'body': val = config.name ? body?.[config.name] : body; break;
363
+ case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
364
+ case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
314
365
  }
315
366
 
316
367
  // 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, {
368
+ for (const pipe of config.pipesList) {
369
+ const pipeInstance = pipe.isReqScoped
370
+ ? this.container.resolveTokenInModuleContext(moduleClass, pipe.token, requestContext)
371
+ : pipe.instance;
372
+ const transformed = pipeInstance.transform(val, {
325
373
  type: config.type,
326
- metatype: paramTypes[config.index],
374
+ metatype: config.paramType,
327
375
  data: config.name,
328
376
  });
377
+ if (transformed instanceof Promise) {
378
+ val = await transformed;
379
+ } else {
380
+ val = transformed;
381
+ }
329
382
  }
330
383
 
331
384
  args[config.index] = val;
332
385
  }
333
386
 
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() });
387
+ let result: any;
388
+ if (handler.interceptorsList.length > 0) {
389
+ let interceptorIndex = 0;
390
+ const executeHandler = async (): Promise<any> => {
391
+ if (interceptorIndex < handler.interceptorsList.length) {
392
+ const interceptor = handler.interceptorsList[interceptorIndex++];
393
+ const interceptorInstance = interceptor.isReqScoped
394
+ ? this.container.resolveTokenInModuleContext(moduleClass, interceptor.token, requestContext)
395
+ : interceptor.instance;
396
+ return interceptorInstance.intercept(context, { handle: () => executeHandler() });
397
+ }
398
+ return instance[methodName](...args);
399
+ };
400
+ result = await executeHandler();
401
+ } else {
402
+ result = instance[methodName](...args);
403
+ if (result instanceof Promise) {
404
+ result = await result;
340
405
  }
341
- return instance[methodName](...args);
342
- };
406
+ }
343
407
 
344
- const result = await executeHandler();
408
+ if (this.isObservable(result)) {
409
+ result = await this.resolveObservable(result);
410
+ }
345
411
 
346
- return this.processResult(req, result, resWrapper, httpCode, headers, redirect);
412
+ return this.processResult(req, result, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam);
347
413
  } catch (err: any) {
348
- return this.handleLifecycleError(err, host, controllerClass, methodName);
414
+ return this.handleLifecycleError(err, host, handler, requestContext);
349
415
  }
350
416
  }
351
417
 
352
418
  private async handleLifecycleError(
353
419
  err: any,
354
420
  host: ArgumentsHost,
355
- controllerClass: any,
356
- methodName: string
421
+ handler: HandlerConfig,
422
+ requestContext?: Map<any, any>
357
423
  ): 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);
424
+ const moduleClass = handler.moduleClass;
363
425
 
364
- for (const filter of filters) {
365
- const filterInstance = this.resolveLifecycleInstance(moduleClass, filter);
426
+ for (const filter of handler.filtersList) {
427
+ const filterInstance = filter.isReqScoped
428
+ ? this.container.resolveTokenInModuleContext(moduleClass, filter.token, requestContext)
429
+ : filter.instance;
366
430
  const catchExceptions = Reflect.getMetadata(METADATA_KEYS.CATCH, filterInstance.constructor) || [];
367
431
 
368
432
  const catchesException =
@@ -370,8 +434,11 @@ export class calyxApplication {
370
434
  catchExceptions.some((excClass: any) => err instanceof excClass);
371
435
 
372
436
  if (catchesException) {
373
- await filterInstance.catch(err, host);
374
- const resWrapper = host.switchToHttp().getResponse<calyxResponse>();
437
+ const catchResult = filterInstance.catch(err, host);
438
+ if (catchResult instanceof Promise) {
439
+ await catchResult;
440
+ }
441
+ const resWrapper = host.switchToHttp().getResponse<CalyxResponse>();
375
442
  if (resWrapper.sent) {
376
443
  return new Response(resWrapper.body, {
377
444
  status: resWrapper.statusCode,
@@ -384,32 +451,33 @@ export class calyxApplication {
384
451
  return this.handleError(err);
385
452
  }
386
453
 
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();
454
+ private compileLifecycleItems(moduleClass: any, items: any[]): CompiledLifecycleItem[] {
455
+ return items.map((item) => {
456
+ if (typeof item === 'function') {
457
+ const scope = this.container.getProviderScope(item);
458
+ if (scope === Scope.REQUEST) {
459
+ return { isReqScoped: true, token: item, instance: null };
460
+ } else {
461
+ let instance: any;
462
+ try {
463
+ instance = this.container.resolveTokenInModuleContext(moduleClass, item);
464
+ } catch {
465
+ instance = new item();
466
+ }
467
+ return { isReqScoped: false, token: item, instance };
468
+ }
403
469
  }
404
- }
405
- return item;
470
+ return { isReqScoped: false, token: item.constructor, instance: item };
471
+ });
406
472
  }
407
473
 
408
474
  private async handleRequestAsync(
409
475
  req: Request,
410
476
  handler: HandlerConfig,
411
477
  params: Record<string, string>,
412
- search: string
478
+ search: string,
479
+ instance: any,
480
+ requestContext?: Map<any, any>
413
481
  ): Promise<Response> {
414
482
  try {
415
483
  let body: any = null;
@@ -435,7 +503,7 @@ export class calyxApplication {
435
503
  }
436
504
  }
437
505
 
438
- return this.executeHandlerSync(req, handler, params, search, body);
506
+ return this.executeHandlerSync(req, handler, params, search, body, instance, requestContext);
439
507
  } catch (err: any) {
440
508
  return this.handleError(err);
441
509
  }
@@ -446,9 +514,11 @@ export class calyxApplication {
446
514
  handler: HandlerConfig,
447
515
  params: Record<string, string>,
448
516
  search: string,
449
- body: any
517
+ body: any,
518
+ instance: any,
519
+ requestContext?: Map<any, any>
450
520
  ): Response | Promise<Response> {
451
- const { instance, methodName, paramsConfig, httpCode, headers, redirect, hasQueryParam, hasResParam } = handler;
521
+ const { methodName, paramsConfig, httpCode, headers, redirect, hasQueryParam, hasResParam } = handler;
452
522
 
453
523
  // Parse query on-demand
454
524
  let query: any = null;
@@ -458,9 +528,12 @@ export class calyxApplication {
458
528
  query = {};
459
529
  }
460
530
 
461
- const resWrapper = hasResParam ? new calyxResponse() : null;
531
+ const resWrapper = hasResParam ? new CalyxResponse() : null;
462
532
  const args: any[] = [];
463
533
 
534
+ const hasCustomParam = paramsConfig.some((p) => p.type === 'custom');
535
+ const context = hasCustomParam ? new CalyxExecutionContext(req, resWrapper, instance.constructor, instance[methodName]) : null;
536
+
464
537
  for (const config of paramsConfig) {
465
538
  let val: any;
466
539
  switch (config.type) {
@@ -482,47 +555,63 @@ export class calyxApplication {
482
555
  case 'headers':
483
556
  val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries());
484
557
  break;
558
+ case 'custom':
559
+ val = config.factory ? config.factory(config.name, context!) : undefined;
560
+ break;
485
561
  }
486
562
  args[config.index] = val;
487
563
  }
488
564
 
489
- const result = instance[methodName](...args);
565
+ let result = instance[methodName](...args);
566
+
567
+ if (this.isObservable(result)) {
568
+ result = this.resolveObservable(result);
569
+ }
490
570
 
491
571
  if (result instanceof Promise) {
492
- return result.then((resolvedResult) => this.processResult(req, resolvedResult, resWrapper, httpCode, headers, redirect));
572
+ return result.then((resolvedResult) => this.processResult(req, resolvedResult, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam));
493
573
  }
494
574
 
495
- return this.processResult(req, result, resWrapper, httpCode, headers, redirect);
575
+ return this.processResult(req, result, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam);
496
576
  }
497
577
 
498
578
  private processResult(
499
579
  req: Request,
500
580
  result: any,
501
- resWrapper: calyxResponse | null,
581
+ resWrapper: CalyxResponse | null,
502
582
  httpCode: number | undefined,
503
583
  headers: { name: string; value: string }[],
504
- redirect: { url: string; statusCode?: number } | undefined
584
+ redirect: { url: string; statusCode?: number } | undefined,
585
+ isPassthrough = false,
586
+ hasResParam = false
505
587
  ): Response {
506
588
  if (redirect) {
507
589
  return Response.redirect(redirect.url, redirect.statusCode || 302);
508
590
  }
509
591
 
510
- if (resWrapper?.sent) {
511
- return new Response(resWrapper.body, {
512
- status: resWrapper.statusCode,
513
- headers: resWrapper.headers,
514
- });
592
+ if (resWrapper) {
593
+ if (resWrapper.sent && !isPassthrough) {
594
+ return new Response(resWrapper.body, {
595
+ status: resWrapper.statusCode,
596
+ headers: resWrapper.headers,
597
+ });
598
+ }
599
+ if (hasResParam && !isPassthrough) {
600
+ return new Response(resWrapper.body, {
601
+ status: resWrapper.statusCode,
602
+ headers: resWrapper.headers,
603
+ });
604
+ }
515
605
  }
516
606
 
517
- const status = httpCode ?? (req.method === 'POST' ? 201 : 200);
607
+ const status = resWrapper && isPassthrough
608
+ ? resWrapper.statusCode
609
+ : (httpCode ?? (req.method === 'POST' ? 201 : 200));
518
610
 
519
611
  // 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
- }
612
+ let responseHeaders = resWrapper && isPassthrough ? resWrapper.headers : new Headers();
613
+ for (const header of headers) {
614
+ responseHeaders.set(header.name, header.value);
526
615
  }
527
616
 
528
617
  if (result === undefined || result === null) {
@@ -534,17 +623,26 @@ export class calyxApplication {
534
623
  }
535
624
 
536
625
  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 });
626
+ responseHeaders.set('content-type', 'application/json');
627
+ return new Response(JSON.stringify(result), { status, headers: responseHeaders });
542
628
  }
543
629
 
544
- if (responseHeaders) {
545
- return new Response(String(result), { status, headers: responseHeaders });
546
- }
547
- return new Response(String(result), { status });
630
+ return new Response(String(result), { status, headers: responseHeaders });
631
+ }
632
+
633
+ private isObservable(obj: any): boolean {
634
+ return obj && typeof obj === 'object' && typeof obj.subscribe === 'function';
635
+ }
636
+
637
+ private resolveObservable(obs: any): Promise<any> {
638
+ return new Promise((resolve, reject) => {
639
+ let lastVal: any = undefined;
640
+ obs.subscribe({
641
+ next: (val: any) => { lastVal = val; },
642
+ error: (err: any) => reject(err),
643
+ complete: () => resolve(lastVal),
644
+ });
645
+ });
548
646
  }
549
647
 
550
648
  private handleError(err: any): Response {
@@ -580,9 +678,77 @@ export class calyxApplication {
580
678
  return this.server;
581
679
  }
582
680
 
583
- async close() {
681
+ private cleanupListeners: (() => void)[] = [];
682
+
683
+ enableShutdownHooks(signals: string[] = ['SIGTERM', 'SIGINT']) {
684
+ const handler = async (signal: string) => {
685
+ await this.close(signal);
686
+ process.exit(0);
687
+ };
688
+
689
+ for (const signal of signals) {
690
+ const listener = () => handler(signal);
691
+ process.on(signal as any, listener);
692
+ this.cleanupListeners.push(() => {
693
+ process.off(signal as any, listener);
694
+ });
695
+ }
696
+ }
697
+
698
+ async close(signal?: string) {
699
+ // Run shutdown hooks
700
+ await this.runShutdownHooks(signal);
701
+
702
+ for (const cleanup of this.cleanupListeners) {
703
+ cleanup();
704
+ }
705
+ this.cleanupListeners = [];
706
+
584
707
  if (this.server) {
585
708
  this.server.stop();
586
709
  }
587
710
  }
711
+
712
+ private async runOnModuleInit() {
713
+ const instances = this.container.getProviderAndControllerInstances();
714
+ for (const instance of instances) {
715
+ if (instance && typeof instance.onModuleInit === 'function') {
716
+ await instance.onModuleInit();
717
+ }
718
+ }
719
+ }
720
+
721
+ private async runOnApplicationBootstrap() {
722
+ const instances = this.container.getProviderAndControllerInstances();
723
+ for (const instance of instances) {
724
+ if (instance && typeof instance.onApplicationBootstrap === 'function') {
725
+ await instance.onApplicationBootstrap();
726
+ }
727
+ }
728
+ }
729
+
730
+ private async runShutdownHooks(signal?: string) {
731
+ const instances = this.container.getProviderAndControllerInstances();
732
+
733
+ // 1. Run OnModuleDestroy
734
+ for (const instance of instances) {
735
+ if (instance && typeof instance.onModuleDestroy === 'function') {
736
+ await instance.onModuleDestroy();
737
+ }
738
+ }
739
+
740
+ // 2. Run BeforeApplicationShutdown
741
+ for (const instance of instances) {
742
+ if (instance && typeof instance.beforeApplicationShutdown === 'function') {
743
+ await instance.beforeApplicationShutdown(signal);
744
+ }
745
+ }
746
+
747
+ // 3. Run OnApplicationShutdown
748
+ for (const instance of instances) {
749
+ if (instance && typeof instance.onApplicationShutdown === 'function') {
750
+ await instance.onApplicationShutdown(signal);
751
+ }
752
+ }
753
+ }
588
754
  }