@martel/calyx 1.10.1 → 1.12.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,175 @@
1
+ import { Module, Injectable } from '../core/decorators.ts';
2
+ import { Type } from '../core/metadata.ts';
3
+ import { ModuleRef } from '../core/module-ref.ts';
4
+
5
+ export interface ICommand {}
6
+ export interface ICommandHandler<TCommand extends ICommand = any, TResult = any> {
7
+ execute(command: TCommand): Promise<TResult>;
8
+ }
9
+
10
+ export interface IQuery {}
11
+ export interface IQueryHandler<TQuery extends IQuery = any, TResult = any> {
12
+ execute(query: TQuery): Promise<TResult>;
13
+ }
14
+
15
+ export interface IEvent {}
16
+ export interface IEventHandler<TEvent extends IEvent = any> {
17
+ handle(event: TEvent): any;
18
+ }
19
+
20
+ import { METADATA_KEYS } from '../core/metadata.ts';
21
+
22
+ export const COMMAND_HANDLER_METADATA = 'cqrs:command_handler';
23
+ export const QUERY_HANDLER_METADATA = 'cqrs:query_handler';
24
+ export const EVENT_HANDLER_METADATA = 'cqrs:event_handler';
25
+
26
+ export const CommandHandler = (command: Type<ICommand>): ClassDecorator => {
27
+ return (target) => {
28
+ Reflect.defineMetadata(COMMAND_HANDLER_METADATA, command, target);
29
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
30
+ };
31
+ };
32
+
33
+ export const QueryHandler = (query: Type<IQuery>): ClassDecorator => {
34
+ return (target) => {
35
+ Reflect.defineMetadata(QUERY_HANDLER_METADATA, query, target);
36
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
37
+ };
38
+ };
39
+
40
+ export const EventsHandler = (...events: Type<IEvent>[]): ClassDecorator => {
41
+ return (target) => {
42
+ Reflect.defineMetadata(EVENT_HANDLER_METADATA, events, target);
43
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
44
+ };
45
+ };
46
+
47
+ @Injectable()
48
+ export class CommandBus {
49
+ private handlers = new Map<any, ICommandHandler>();
50
+
51
+ register(command: any, handler: ICommandHandler) {
52
+ this.handlers.set(command, handler);
53
+ }
54
+
55
+ async execute<T extends ICommand, R = any>(command: T): Promise<R> {
56
+ const commandClass = command.constructor;
57
+ const handler = this.handlers.get(commandClass);
58
+ if (!handler) {
59
+ throw new Error(`CommandHandler not found for command: ${commandClass.name}`);
60
+ }
61
+ return (await handler.execute(command)) as R;
62
+ }
63
+ }
64
+
65
+ @Injectable()
66
+ export class QueryBus {
67
+ private handlers = new Map<any, IQueryHandler>();
68
+
69
+ register(query: any, handler: IQueryHandler) {
70
+ this.handlers.set(query, handler);
71
+ }
72
+
73
+ async execute<T extends IQuery, R = any>(query: T): Promise<R> {
74
+ const queryClass = query.constructor;
75
+ const handler = this.handlers.get(queryClass);
76
+ if (!handler) {
77
+ throw new Error(`QueryHandler not found for query: ${queryClass.name}`);
78
+ }
79
+ return (await handler.execute(query)) as R;
80
+ }
81
+ }
82
+
83
+ @Injectable()
84
+ export class EventBus {
85
+ private handlers = new Map<any, IEventHandler[]>();
86
+
87
+ register(event: any, handler: IEventHandler) {
88
+ const existing = this.handlers.get(event) || [];
89
+ existing.push(handler);
90
+ this.handlers.set(event, existing);
91
+ }
92
+
93
+ publish<T extends IEvent>(event: T) {
94
+ const eventClass = event.constructor;
95
+ const eventHandlers = this.handlers.get(eventClass) || [];
96
+ for (const handler of eventHandlers) {
97
+ try {
98
+ handler.handle(event);
99
+ } catch (err) {
100
+ console.error(`Error in event handler:`, err);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ @Injectable()
107
+ export class EventPublisher {
108
+ constructor(private readonly eventBus: EventBus) {}
109
+
110
+ mergeClassContext<T extends Type<any>>(metatype: T): T {
111
+ const eventBus = this.eventBus;
112
+ return class extends metatype {
113
+ publish(event: IEvent) {
114
+ eventBus.publish(event);
115
+ }
116
+ publishAll(events: IEvent[]) {
117
+ for (const event of events) {
118
+ eventBus.publish(event);
119
+ }
120
+ }
121
+ };
122
+ }
123
+
124
+ mergeObjectContext<T extends Object>(object: T): T {
125
+ const eventBus = this.eventBus;
126
+ return Object.assign(object, {
127
+ publish: (event: IEvent) => eventBus.publish(event),
128
+ publishAll: (events: IEvent[]) => {
129
+ for (const event of events) {
130
+ eventBus.publish(event);
131
+ }
132
+ },
133
+ });
134
+ }
135
+ }
136
+
137
+ @Module({
138
+ providers: [CommandBus, QueryBus, EventBus, EventPublisher],
139
+ exports: [CommandBus, QueryBus, EventBus, EventPublisher],
140
+ })
141
+ export class CqrsModule {
142
+ constructor(
143
+ private readonly moduleRef: ModuleRef,
144
+ private readonly commandBus: CommandBus,
145
+ private readonly queryBus: QueryBus,
146
+ private readonly eventBus: EventBus
147
+ ) {}
148
+
149
+ onModuleInit() {
150
+ const container = (this.moduleRef as any).container;
151
+ if (!container) return;
152
+
153
+ const instances = container.getProviderAndControllerInstances();
154
+ for (const inst of instances) {
155
+ if (!inst || !inst.constructor) continue;
156
+
157
+ const command = Reflect.getMetadata(COMMAND_HANDLER_METADATA, inst.constructor);
158
+ if (command) {
159
+ this.commandBus.register(command, inst);
160
+ }
161
+
162
+ const query = Reflect.getMetadata(QUERY_HANDLER_METADATA, inst.constructor);
163
+ if (query) {
164
+ this.queryBus.register(query, inst);
165
+ }
166
+
167
+ const events = Reflect.getMetadata(EVENT_HANDLER_METADATA, inst.constructor);
168
+ if (events && Array.isArray(events)) {
169
+ for (const event of events) {
170
+ this.eventBus.register(event, inst);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
@@ -47,6 +47,22 @@ export function Parent(): ParameterDecorator {
47
47
  };
48
48
  }
49
49
 
50
+ export function Context(name?: string): ParameterDecorator {
51
+ return (target, propertyKey, parameterIndex) => {
52
+ const contextParams = Reflect.getOwnMetadata('calyx:context', target, propertyKey!) || [];
53
+ contextParams.push({ parameterIndex, name });
54
+ Reflect.defineMetadata('calyx:context', contextParams, target, propertyKey!);
55
+ };
56
+ }
57
+
58
+ export function Info(): ParameterDecorator {
59
+ return (target, propertyKey, parameterIndex) => {
60
+ const infoParams = Reflect.getOwnMetadata('calyx:info', target, propertyKey!) || [];
61
+ infoParams.push(parameterIndex);
62
+ Reflect.defineMetadata('calyx:info', infoParams, target, propertyKey!);
63
+ };
64
+ }
65
+
50
66
  export function ObjectType(): ClassDecorator {
51
67
  return (target) => {
52
68
  Reflect.defineMetadata('calyx:object_type', true, target);
@@ -1,4 +1,4 @@
1
- import { CalyxContainer } from '../core/container.ts';
1
+ import { CalyxContainer, DynamicModule } from '../core/container.ts';
2
2
  import { Module } from '../core/decorators.ts';
3
3
  import {
4
4
  GraphQLSchema,
@@ -13,17 +13,42 @@ import {
13
13
  GraphQLBoolean,
14
14
  GraphQLList,
15
15
  GraphQLNonNull,
16
+ buildSchema as buildGqlSchema,
16
17
  } from 'graphql';
17
18
 
19
+ export interface GraphQLOptions {
20
+ typeDefs?: string;
21
+ context?: (ctx: { req: any }) => any | Promise<any>;
22
+ }
23
+
18
24
  @Module({})
19
25
  export class GraphQLModule {
26
+ static forRoot(options: GraphQLOptions): DynamicModule {
27
+ return {
28
+ module: GraphQLModule,
29
+ providers: [
30
+ {
31
+ provide: 'calyx:graphql_options',
32
+ useValue: options,
33
+ },
34
+ ],
35
+ };
36
+ }
37
+
20
38
  static buildSchema(container: CalyxContainer): GraphQLSchema | null {
21
39
  const instances = container.getProviderAndControllerInstances();
22
40
  const resolverInstances = instances.filter(
23
41
  (inst) => inst && inst.constructor && Reflect.hasMetadata('calyx:resolver', inst.constructor)
24
42
  );
25
43
 
26
- if (resolverInstances.length === 0) {
44
+ let options: GraphQLOptions | undefined;
45
+ try {
46
+ options = container.getGlobalOrAnyInstance('calyx:graphql_options');
47
+ } catch {
48
+ // Options provider not bound, fallback to default code-first
49
+ }
50
+
51
+ if (resolverInstances.length === 0 && !options?.typeDefs) {
27
52
  return null;
28
53
  }
29
54
 
@@ -98,7 +123,12 @@ export class GraphQLModule {
98
123
  // Custom Scalar
99
124
  if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:scalar', typeClass)) {
100
125
  const scalarMeta = Reflect.getMetadata('calyx:scalar', typeClass);
101
- const inst = container.get(typeClass) || new typeClass();
126
+ let inst: any;
127
+ try {
128
+ inst = container.getGlobalOrAnyInstance(typeClass);
129
+ } catch {
130
+ inst = new typeClass();
131
+ }
102
132
  const gqlScalar = new GraphQLScalarType({
103
133
  name: scalarMeta.name,
104
134
  description: Reflect.getMetadata('calyx:description', typeClass),
@@ -283,6 +313,14 @@ export class GraphQLModule {
283
313
  for (const idx of parentParams) {
284
314
  params[idx] = parent;
285
315
  }
316
+ const contextParams = Reflect.getMetadata('calyx:context', resolverInstance, fieldMeta.propertyKey) || [];
317
+ for (const item of contextParams) {
318
+ params[item.parameterIndex] = item.name ? context?.[item.name] : context;
319
+ }
320
+ const infoParams = Reflect.getMetadata('calyx:info', resolverInstance, fieldMeta.propertyKey) || [];
321
+ for (const idx of infoParams) {
322
+ params[idx] = info;
323
+ }
286
324
  return resolverInstance[fieldMeta.propertyKey](...params);
287
325
  };
288
326
 
@@ -443,6 +481,14 @@ export class GraphQLModule {
443
481
  for (const idx of parentParams) {
444
482
  params[idx] = parent;
445
483
  }
484
+ const contextParams = Reflect.getMetadata('calyx:context', resolverInstance, fieldRes.propertyKey) || [];
485
+ for (const item of contextParams) {
486
+ params[item.parameterIndex] = item.name ? context?.[item.name] : context;
487
+ }
488
+ const infoParams = Reflect.getMetadata('calyx:info', resolverInstance, fieldRes.propertyKey) || [];
489
+ for (const idx of infoParams) {
490
+ params[idx] = info;
491
+ }
446
492
  return resolverInstance[fieldRes.propertyKey](...params);
447
493
  };
448
494
 
@@ -480,6 +526,60 @@ export class GraphQLModule {
480
526
  }
481
527
  }
482
528
 
529
+ // Support Schema-First Approach if typeDefs is specified
530
+ if (options?.typeDefs) {
531
+ const schema = buildGqlSchema(options.typeDefs);
532
+
533
+ // Attach code-first resolver actions onto schema-first AST definitions
534
+ const queryType = schema.getQueryType();
535
+ if (queryType) {
536
+ const fields = queryType.getFields();
537
+ for (const [fieldName, field] of Object.entries(fields)) {
538
+ if (queryFields[fieldName]) {
539
+ field.resolve = queryFields[fieldName].resolve;
540
+ }
541
+ }
542
+ }
543
+ const mutationType = schema.getMutationType();
544
+ if (mutationType) {
545
+ const fields = mutationType.getFields();
546
+ for (const [fieldName, field] of Object.entries(fields)) {
547
+ if (mutationFields[fieldName]) {
548
+ field.resolve = mutationFields[fieldName].resolve;
549
+ }
550
+ }
551
+ }
552
+ const subscriptionType = schema.getSubscriptionType();
553
+ if (subscriptionType) {
554
+ const fields = subscriptionType.getFields();
555
+ for (const [fieldName, field] of Object.entries(fields)) {
556
+ if (subscriptionFields[fieldName]) {
557
+ field.subscribe = subscriptionFields[fieldName].subscribe;
558
+ field.resolve = subscriptionFields[fieldName].resolve;
559
+ }
560
+ }
561
+ }
562
+
563
+ // Map Custom ObjectType ResolveFields from code-first typeMap
564
+ for (const [typeClass, codeFirstType] of typeMap.entries()) {
565
+ if (codeFirstType instanceof GraphQLObjectType) {
566
+ const typeName = codeFirstType.name;
567
+ const schemaFirstType = schema.getType(typeName);
568
+ if (schemaFirstType instanceof GraphQLObjectType) {
569
+ const codeFirstFields = codeFirstType.getFields();
570
+ const schemaFirstFields = schemaFirstType.getFields();
571
+ for (const [fieldName, cfField] of Object.entries(codeFirstFields)) {
572
+ if (cfField.resolve && schemaFirstFields[fieldName]) {
573
+ schemaFirstFields[fieldName].resolve = cfField.resolve;
574
+ }
575
+ }
576
+ }
577
+ }
578
+ }
579
+
580
+ return schema;
581
+ }
582
+
483
583
  if (Object.keys(queryFields).length === 0) {
484
584
  return null;
485
585
  }
@@ -10,6 +10,7 @@ import { EventEmitter } from '../event-emitter/event-emitter.ts';
10
10
  import { cors, CorsOptions } from '../security/cors.middleware.ts';
11
11
  import { helmet, HelmetOptions } from '../security/helmet.middleware.ts';
12
12
  import { CronMatcher } from '../schedule/cron.matcher.ts';
13
+ import { SchedulerRegistry } from '../schedule/scheduler-registry.ts';
13
14
  import { SerializationCompiler } from '../validation/compiler.ts';
14
15
  import { VersioningOptions, VersioningType, VersionExtractor, VERSION_METADATA_KEY } from '../versioning/versioning.ts';
15
16
  import { QueueManager, PROCESSOR_METADATA_KEY } from '../queue/queue.module.ts';
@@ -66,6 +67,41 @@ export class CalyxResponse {
66
67
  this.cookiesList.push(formatCookie(name, value, options));
67
68
  return this;
68
69
  }
70
+
71
+ header(name: string, value: string) {
72
+ return this.set(name, value);
73
+ }
74
+
75
+ type(contentType: string) {
76
+ return this.set('content-type', contentType);
77
+ }
78
+
79
+ redirect(url: string, status?: number) {
80
+ this.statusCode = status ?? 302;
81
+ this.set('location', url);
82
+ this.sent = true;
83
+ return this;
84
+ }
85
+
86
+ end() {
87
+ this.sent = true;
88
+ return this;
89
+ }
90
+
91
+ get(name: string): string | undefined {
92
+ return this.headers[name.toLowerCase()];
93
+ }
94
+
95
+ append(name: string, value: string) {
96
+ const key = name.toLowerCase();
97
+ const existing = this.headers[key];
98
+ this.headers[key] = existing ? `${existing}, ${value}` : value;
99
+ return this;
100
+ }
101
+
102
+ clearCookie(name: string, options?: any) {
103
+ return this.cookie(name, '', { ...options, expires: new Date(0) });
104
+ }
69
105
  }
70
106
 
71
107
  interface CompiledLifecycleItem {
@@ -186,7 +222,7 @@ export class CalyxApplication {
186
222
  this.isInitialized = true;
187
223
 
188
224
  // Bootstrap the dependency injection container
189
- this.container.bootstrap(this.rootModule);
225
+ await this.container.bootstrap(this.rootModule);
190
226
 
191
227
  // Resolve registered modules middlewares
192
228
  this.resolveMiddleware();
@@ -685,6 +721,10 @@ export class CalyxApplication {
685
721
  case 'query': val = config.name ? query?.[config.name] : query; break;
686
722
  case 'body': val = config.name ? body?.[config.name] : body; break;
687
723
  case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
724
+ case 'session': val = (req as any).session; break;
725
+ case 'ip': val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? ''); break;
726
+ case 'hostparam': val = config.name ? req.headers.get('host') : req.headers.get('host'); break;
727
+ case 'next': val = next; break;
688
728
  case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
689
729
  }
690
730
 
@@ -939,6 +979,18 @@ export class CalyxApplication {
939
979
  case 'headers':
940
980
  val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries());
941
981
  break;
982
+ case 'session':
983
+ val = (req as any).session;
984
+ break;
985
+ case 'ip':
986
+ val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? '');
987
+ break;
988
+ case 'hostparam':
989
+ val = config.name ? req.headers.get('host') : req.headers.get('host');
990
+ break;
991
+ case 'next':
992
+ val = () => {};
993
+ break;
942
994
  case 'custom':
943
995
  val = config.factory ? config.factory(config.name, context!) : undefined;
944
996
  break;
@@ -1231,10 +1283,25 @@ export class CalyxApplication {
1231
1283
  this.graphqlQueryCache.set(query, document);
1232
1284
  }
1233
1285
 
1286
+ let options: any;
1287
+ try {
1288
+ options = this.container.get('calyx:graphql_options');
1289
+ } catch {}
1290
+
1291
+ let contextValue: any = { req };
1292
+ if (options?.context) {
1293
+ contextValue = await options.context({ req });
1294
+ if (contextValue && typeof contextValue === 'object') {
1295
+ contextValue.req = req;
1296
+ } else {
1297
+ contextValue = { req, ...contextValue };
1298
+ }
1299
+ }
1300
+
1234
1301
  const result = await execute({
1235
1302
  schema: this.graphqlSchema,
1236
1303
  document,
1237
- contextValue: { req },
1304
+ contextValue,
1238
1305
  variableValues: variables,
1239
1306
  });
1240
1307
 
@@ -1349,11 +1416,26 @@ export class CalyxApplication {
1349
1416
  this.graphqlQueryCache.set(query, document);
1350
1417
  }
1351
1418
 
1419
+ let options: any;
1420
+ try {
1421
+ options = this.container.get('calyx:graphql_options');
1422
+ } catch {}
1423
+
1424
+ let contextValue: any = { req: ws.data?.req };
1425
+ if (options?.context) {
1426
+ contextValue = await options.context({ req: ws.data?.req });
1427
+ if (contextValue && typeof contextValue === 'object') {
1428
+ contextValue.req = ws.data?.req;
1429
+ } else {
1430
+ contextValue = { req: ws.data?.req, ...contextValue };
1431
+ }
1432
+ }
1433
+
1352
1434
  const subResult = await subscribe({
1353
1435
  schema: this.graphqlSchema,
1354
1436
  document,
1355
1437
  variableValues: variables,
1356
- contextValue: { req: ws.data?.req },
1438
+ contextValue,
1357
1439
  });
1358
1440
 
1359
1441
  if (subResult && Symbol.asyncIterator in subResult) {
@@ -1524,11 +1606,18 @@ export class CalyxApplication {
1524
1606
  return;
1525
1607
  }
1526
1608
 
1609
+ let registry: SchedulerRegistry | null = null;
1610
+ try {
1611
+ registry = this.container.getGlobalOrAnyInstance(SchedulerRegistry);
1612
+ } catch {
1613
+ // ignore
1614
+ }
1615
+
1527
1616
  const instances = this.container.getProviderAndControllerInstances();
1528
1617
  for (const instance of instances) {
1529
1618
  if (!instance || !instance.constructor) continue;
1530
1619
 
1531
- const crons: { expression: string; propertyKey: string | symbol }[] =
1620
+ const crons: { expression: string; propertyKey: string | symbol; name?: string }[] =
1532
1621
  Reflect.getMetadata('calyx:cron', instance.constructor) || [];
1533
1622
  for (const cron of crons) {
1534
1623
  const parts = cron.expression.split(' ');
@@ -1554,9 +1643,16 @@ export class CalyxApplication {
1554
1643
  const intervalMs = isSecondLevel ? 1000 : 20000;
1555
1644
  const timer = setInterval(tick, intervalMs);
1556
1645
  this.cleanupListeners.push(() => clearInterval(timer));
1646
+
1647
+ if (registry && cron.name) {
1648
+ registry.addCronJob(cron.name, {
1649
+ start: () => {},
1650
+ stop: () => clearInterval(timer),
1651
+ });
1652
+ }
1557
1653
  }
1558
1654
 
1559
- const intervals: { ms: number; propertyKey: string | symbol }[] =
1655
+ const intervals: { ms: number; propertyKey: string | symbol; name?: string }[] =
1560
1656
  Reflect.getMetadata('calyx:interval', instance.constructor) || [];
1561
1657
  for (const interval of intervals) {
1562
1658
  const timer = setInterval(() => {
@@ -1567,9 +1663,13 @@ export class CalyxApplication {
1567
1663
  }
1568
1664
  }, interval.ms);
1569
1665
  this.cleanupListeners.push(() => clearInterval(timer));
1666
+
1667
+ if (registry && interval.name) {
1668
+ registry.addInterval(interval.name, timer);
1669
+ }
1570
1670
  }
1571
1671
 
1572
- const timeouts: { ms: number; propertyKey: string | symbol }[] =
1672
+ const timeouts: { ms: number; propertyKey: string | symbol; name?: string }[] =
1573
1673
  Reflect.getMetadata('calyx:timeout', instance.constructor) || [];
1574
1674
  for (const timeout of timeouts) {
1575
1675
  const timer = setTimeout(() => {
@@ -1580,6 +1680,10 @@ export class CalyxApplication {
1580
1680
  }
1581
1681
  }, timeout.ms);
1582
1682
  this.cleanupListeners.push(() => clearTimeout(timer));
1683
+
1684
+ if (registry && timeout.name) {
1685
+ registry.addTimeout(timeout.name, timer);
1686
+ }
1583
1687
  }
1584
1688
  }
1585
1689
  }
@@ -1605,14 +1709,24 @@ export class CalyxApplication {
1605
1709
  for (const sub of subMessages) {
1606
1710
  const paramMapping: any[] = [];
1607
1711
 
1712
+ const classPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, instance.constructor) || [];
1713
+ const methodPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, instance.constructor.prototype, sub.propertyKey) || [];
1714
+ const methodPipesCompiled = this.compileLifecycleItems(this.rootModule, [...this.globalPipes, ...classPipes, ...methodPipes]);
1715
+
1608
1716
  for (const bp of bodyParams) {
1609
1717
  if (bp.propertyKey === sub.propertyKey) {
1610
- paramMapping[bp.parameterIndex] = 'body';
1718
+ paramMapping[bp.parameterIndex] = {
1719
+ type: 'body',
1720
+ name: bp.name,
1721
+ pipes: [...methodPipesCompiled, ...this.compileLifecycleItems(this.rootModule, bp.pipes || [])],
1722
+ };
1611
1723
  }
1612
1724
  }
1613
1725
  for (const sp of socketParams) {
1614
1726
  if (sp.propertyKey === sub.propertyKey) {
1615
- paramMapping[sp.parameterIndex] = 'socket';
1727
+ paramMapping[sp.parameterIndex] = {
1728
+ type: 'socket',
1729
+ };
1616
1730
  }
1617
1731
  }
1618
1732
 
@@ -1624,11 +1738,16 @@ export class CalyxApplication {
1624
1738
  const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor.prototype, sub.propertyKey) || [];
1625
1739
  const interceptors = this.compileLifecycleItems(this.rootModule, [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors]);
1626
1740
 
1741
+ const classFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, instance.constructor) || [];
1742
+ const methodFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, instance.constructor.prototype, sub.propertyKey) || [];
1743
+ const filters = this.compileLifecycleItems(this.rootModule, [...this.globalFilters, ...classFilters, ...methodFilters]);
1744
+
1627
1745
  handlers.set(sub.event, {
1628
1746
  propertyKey: sub.propertyKey,
1629
1747
  paramMapping,
1630
1748
  guards,
1631
1749
  interceptors,
1750
+ filters,
1632
1751
  gatewayClass: instance.constructor,
1633
1752
  });
1634
1753
  }
@@ -1708,16 +1827,6 @@ export class CalyxApplication {
1708
1827
 
1709
1828
  const handlerInfo = gateway.handlers.get(event);
1710
1829
  if (handlerInfo) {
1711
- const args: any[] = [];
1712
- for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
1713
- const type = handlerInfo.paramMapping[i];
1714
- if (type === 'body') {
1715
- args[i] = data;
1716
- } else if (type === 'socket') {
1717
- args[i] = ws;
1718
- }
1719
- }
1720
-
1721
1830
  const context = this.contextPool.acquire();
1722
1831
  context.resetContextWs(ws, data, handlerInfo.gatewayClass, gateway.instance[handlerInfo.propertyKey]);
1723
1832
 
@@ -1729,6 +1838,26 @@ export class CalyxApplication {
1729
1838
  }
1730
1839
  }
1731
1840
 
1841
+ const args: any[] = [];
1842
+ for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
1843
+ const mapping = handlerInfo.paramMapping[i];
1844
+ if (!mapping) continue;
1845
+ if (mapping.type === 'body') {
1846
+ let val = mapping.name ? data?.[mapping.name] : data;
1847
+ for (const pipe of mapping.pipes || []) {
1848
+ const transformed = pipe.instance.transform(val, {
1849
+ type: 'body',
1850
+ metatype: undefined,
1851
+ data: mapping.name,
1852
+ });
1853
+ val = transformed instanceof Promise ? await transformed : transformed;
1854
+ }
1855
+ args[i] = val;
1856
+ } else if (mapping.type === 'socket') {
1857
+ args[i] = ws;
1858
+ }
1859
+ }
1860
+
1732
1861
  const nextCall = {
1733
1862
  handle: async () => {
1734
1863
  return gateway.instance[handlerInfo.propertyKey](...args);
@@ -1752,7 +1881,19 @@ export class CalyxApplication {
1752
1881
  ws.send(JSON.stringify(result));
1753
1882
  }
1754
1883
  } catch (err: any) {
1755
- // ignore or log
1884
+ if (handlerInfo.filters && handlerInfo.filters.length > 0) {
1885
+ for (const filter of handlerInfo.filters) {
1886
+ const catchException = Reflect.getMetadata('calyx:catch', filter.token) || [];
1887
+ if (catchException.length === 0 || catchException.some((exc: any) => err instanceof exc)) {
1888
+ const filterResult = filter.instance.catch(err, context);
1889
+ if (filterResult !== undefined) {
1890
+ ws.send(JSON.stringify(filterResult));
1891
+ return;
1892
+ }
1893
+ }
1894
+ }
1895
+ }
1896
+ ws.send(JSON.stringify({ event: 'error', data: err.message || 'Internal error' }));
1756
1897
  } finally {
1757
1898
  context.clearContext();
1758
1899
  this.contextPool.release(context);
@@ -82,6 +82,10 @@ export const Headers = (first?: any, ...pipes: any[]) => {
82
82
  return createHttpParamDecorator('headers', name, parsedPipes);
83
83
  };
84
84
 
85
+ export const Ip = () => createHttpParamDecorator('ip');
86
+ export const HostParam = (name?: string) => createHttpParamDecorator('hostparam', name);
87
+ export const Next = () => createHttpParamDecorator('next');
88
+
85
89
  export function HttpCode(code: number): MethodDecorator {
86
90
  return (target, propertyKey) => {
87
91
  Reflect.defineMetadata(METADATA_KEYS.HTTP_CODE, code, target, propertyKey);
package/src/index.ts CHANGED
@@ -24,4 +24,6 @@ export * from './http-client/index.ts';
24
24
  export * from './session/index.ts';
25
25
  export * from './mvc/index.ts';
26
26
  export * from './sse/index.ts';
27
+ export * from './terminus/terminus.ts';
28
+ export * from './cqrs/cqrs.ts';
27
29