@zdavison/matador-nest 2.0.1

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.
Files changed (74) hide show
  1. package/dist/constants.d.ts +13 -0
  2. package/dist/constants.d.ts.map +1 -0
  3. package/dist/constants.js +12 -0
  4. package/dist/decorators/index.d.ts +3 -0
  5. package/dist/decorators/index.d.ts.map +1 -0
  6. package/dist/decorators/index.js +2 -0
  7. package/dist/decorators/matador-subscriber.decorator.d.ts +20 -0
  8. package/dist/decorators/matador-subscriber.decorator.d.ts.map +1 -0
  9. package/dist/decorators/matador-subscriber.decorator.js +26 -0
  10. package/dist/decorators/on-matador-event.decorator.d.ts +24 -0
  11. package/dist/decorators/on-matador-event.decorator.d.ts.map +1 -0
  12. package/dist/decorators/on-matador-event.decorator.js +38 -0
  13. package/dist/discovery/index.d.ts +2 -0
  14. package/dist/discovery/index.d.ts.map +1 -0
  15. package/dist/discovery/index.js +1 -0
  16. package/dist/discovery/subscriber-discovery.service.d.ts +37 -0
  17. package/dist/discovery/subscriber-discovery.service.d.ts.map +1 -0
  18. package/dist/discovery/subscriber-discovery.service.js +144 -0
  19. package/dist/index.cjs +495 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.cts +7 -0
  22. package/dist/index.d.ts +7 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +10 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/module/index.d.ts +2 -0
  27. package/dist/module/index.d.ts.map +1 -0
  28. package/dist/module/index.js +1 -0
  29. package/dist/module/matador.module.d.ts +71 -0
  30. package/dist/module/matador.module.d.ts.map +1 -0
  31. package/dist/module/matador.module.js +146 -0
  32. package/dist/services/index.d.ts +2 -0
  33. package/dist/services/index.d.ts.map +1 -0
  34. package/dist/services/index.js +1 -0
  35. package/dist/services/matador.service.d.ts +105 -0
  36. package/dist/services/matador.service.d.ts.map +1 -0
  37. package/dist/services/matador.service.js +209 -0
  38. package/dist/testing/index.d.ts +2 -0
  39. package/dist/testing/index.d.ts.map +1 -0
  40. package/dist/testing/index.js +1 -0
  41. package/dist/testing/matador-testing.module.d.ts +53 -0
  42. package/dist/testing/matador-testing.module.d.ts.map +1 -0
  43. package/dist/testing/matador-testing.module.js +77 -0
  44. package/dist/testing.cjs +498 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +2 -0
  47. package/dist/testing.d.ts +2 -0
  48. package/dist/testing.d.ts.map +1 -0
  49. package/dist/testing.js +2 -0
  50. package/dist/testing.js.map +1 -0
  51. package/dist/types.d.ts +88 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +1 -0
  54. package/package.json +61 -0
  55. package/src/constants.ts +14 -0
  56. package/src/decorators/index.ts +2 -0
  57. package/src/decorators/matador-subscriber.decorator.ts +27 -0
  58. package/src/decorators/on-matador-event.decorator.ts +68 -0
  59. package/src/discovery/index.ts +1 -0
  60. package/src/discovery/subscriber-discovery.service.ts +181 -0
  61. package/src/index.ts +28 -0
  62. package/src/module/index.ts +1 -0
  63. package/src/module/matador.module.ts +162 -0
  64. package/src/services/index.ts +1 -0
  65. package/src/services/matador.service.ts +283 -0
  66. package/src/testing/index.ts +1 -0
  67. package/src/testing/matador-testing.module.ts +71 -0
  68. package/src/testing.ts +2 -0
  69. package/src/types.ts +119 -0
  70. package/test/decorators.test.ts +132 -0
  71. package/test/discovery.test.ts +278 -0
  72. package/tsconfig.json +32 -0
  73. package/tsconfig.tsbuildinfo +1 -0
  74. package/tsup.config.ts +23 -0
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import 'reflect-metadata';
3
+ import { type Envelope, MatadorEvent } from '@zdavison/matador';
4
+ import { MATADOR_EVENT_HANDLERS } from '../src/constants.js';
5
+ import { OnMatadorEvent } from '../src/decorators/on-matador-event.decorator.js';
6
+
7
+ // Test event
8
+ class TestEvent extends MatadorEvent<{ userId: string }> {
9
+ static readonly key = 'test.event';
10
+ static readonly description = 'Test event';
11
+
12
+ constructor(public readonly data: { userId: string }) {
13
+ super();
14
+ }
15
+ }
16
+
17
+ describe('@OnMatadorEvent decorator', () => {
18
+ it('stores metadata on the class', () => {
19
+ class TestService {
20
+ @OnMatadorEvent(TestEvent, {
21
+ description: 'Test handler',
22
+ })
23
+ async handleTestEvent(_envelope: Envelope<TestEvent['data']>) {
24
+ // handler
25
+ }
26
+ }
27
+
28
+ const handlers = Reflect.getMetadata(MATADOR_EVENT_HANDLERS, TestService);
29
+
30
+ expect(handlers).toBeDefined();
31
+ expect(handlers).toHaveLength(1);
32
+ expect(handlers[0].eventClass).toBe(TestEvent);
33
+ expect(handlers[0].options.description).toBe('Test handler');
34
+ expect(handlers[0].methodName).toBe('handleTestEvent');
35
+ });
36
+
37
+ it('stores multiple handlers on the same class', () => {
38
+ class AnotherEvent extends MatadorEvent<{ data: string }> {
39
+ static readonly key = 'another.event';
40
+ constructor(public readonly data: { data: string }) {
41
+ super();
42
+ }
43
+ }
44
+
45
+ class MultiHandlerService {
46
+ @OnMatadorEvent(TestEvent, {
47
+ description: 'Handler 1',
48
+ })
49
+ async handler1(_envelope: Envelope<TestEvent['data']>) {
50
+ // handler
51
+ }
52
+
53
+ @OnMatadorEvent(AnotherEvent, {
54
+ description: 'Handler 2',
55
+ })
56
+ async handler2(_envelope: Envelope<AnotherEvent['data']>) {
57
+ // handler
58
+ }
59
+ }
60
+
61
+ const handlers = Reflect.getMetadata(
62
+ MATADOR_EVENT_HANDLERS,
63
+ MultiHandlerService,
64
+ );
65
+
66
+ expect(handlers).toBeDefined();
67
+ expect(handlers).toHaveLength(2);
68
+ expect(handlers[0].eventClass).toBe(TestEvent);
69
+ expect(handlers[1].eventClass).toBe(AnotherEvent);
70
+ });
71
+
72
+ it('supports custom subscriber name', () => {
73
+ class ServiceWithCustomName {
74
+ @OnMatadorEvent(TestEvent, {
75
+ name: 'custom-name',
76
+ description: 'Custom named handler',
77
+ })
78
+ async handleEvent(_envelope: Envelope<TestEvent['data']>) {
79
+ // handler
80
+ }
81
+ }
82
+
83
+ const handlers = Reflect.getMetadata(
84
+ MATADOR_EVENT_HANDLERS,
85
+ ServiceWithCustomName,
86
+ );
87
+
88
+ expect(handlers[0].options.name).toBe('custom-name');
89
+ });
90
+
91
+ it('supports all subscriber options', () => {
92
+ class ServiceWithAllOptions {
93
+ @OnMatadorEvent(TestEvent, {
94
+ description: 'Full options handler',
95
+ idempotent: 'yes',
96
+ importance: 'must-investigate',
97
+ targetQueue: 'priority-queue',
98
+ })
99
+ async handleEvent(_envelope: Envelope<TestEvent['data']>) {
100
+ // handler
101
+ }
102
+ }
103
+
104
+ const handlers = Reflect.getMetadata(
105
+ MATADOR_EVENT_HANDLERS,
106
+ ServiceWithAllOptions,
107
+ );
108
+
109
+ expect(handlers[0].options.idempotent).toBe('yes');
110
+ expect(handlers[0].options.importance).toBe('must-investigate');
111
+ expect(handlers[0].options.targetQueue).toBe('priority-queue');
112
+ });
113
+
114
+ it('supports resumable subscribers', () => {
115
+ class ResumableService {
116
+ @OnMatadorEvent(TestEvent, {
117
+ description: 'Resumable handler',
118
+ idempotent: 'resumable',
119
+ })
120
+ async handleEvent(_envelope: Envelope<TestEvent['data']>) {
121
+ // handler
122
+ }
123
+ }
124
+
125
+ const handlers = Reflect.getMetadata(
126
+ MATADOR_EVENT_HANDLERS,
127
+ ResumableService,
128
+ );
129
+
130
+ expect(handlers[0].options.idempotent).toBe('resumable');
131
+ });
132
+ });
@@ -0,0 +1,278 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test';
2
+ import 'reflect-metadata';
3
+ import type { DiscoveryService } from '@nestjs/core';
4
+ import {
5
+ type AnySubscriber,
6
+ type Envelope,
7
+ type EventClass,
8
+ MatadorEvent,
9
+ } from '@zdavison/matador';
10
+ import { OnMatadorEvent } from '../src/decorators/on-matador-event.decorator.js';
11
+ import { SubscriberDiscoveryService } from '../src/discovery/subscriber-discovery.service.js';
12
+
13
+ // Test events
14
+ class UserCreatedEvent extends MatadorEvent<{ userId: string }> {
15
+ static readonly key = 'user.created';
16
+ static readonly description = 'User created event';
17
+
18
+ constructor(public readonly data: { userId: string }) {
19
+ super();
20
+ }
21
+ }
22
+
23
+ class OrderPlacedEvent extends MatadorEvent<{ orderId: string }> {
24
+ static readonly key = 'order.placed';
25
+ static readonly description = 'Order placed event';
26
+
27
+ constructor(public readonly data: { orderId: string }) {
28
+ super();
29
+ }
30
+ }
31
+
32
+ describe('SubscriberDiscoveryService', () => {
33
+ let discoveryService: SubscriberDiscoveryService;
34
+ let mockNestDiscovery: { getProviders: () => Array<{ instance: unknown }> };
35
+
36
+ beforeEach(() => {
37
+ mockNestDiscovery = {
38
+ getProviders: () => [],
39
+ };
40
+ });
41
+
42
+ it('discovers decorated methods and builds schema', () => {
43
+ // Create a service with decorated handlers
44
+ class TestService {
45
+ @OnMatadorEvent(UserCreatedEvent, {
46
+ description: 'Handles user creation',
47
+ })
48
+ async onUserCreated(_envelope: Envelope<UserCreatedEvent['data']>) {
49
+ // handler
50
+ }
51
+ }
52
+
53
+ const instance = new TestService();
54
+
55
+ mockNestDiscovery.getProviders = () =>
56
+ [
57
+ {
58
+ instance,
59
+ },
60
+ ] ;
61
+
62
+ discoveryService = new SubscriberDiscoveryService(
63
+ mockNestDiscovery as unknown as DiscoveryService,
64
+ );
65
+
66
+ // Trigger discovery (simulates onModuleInit)
67
+ discoveryService.onModuleInit();
68
+
69
+ const schema = discoveryService.getSchema();
70
+
71
+ expect(schema[UserCreatedEvent.key]).toBeDefined();
72
+ const [eventClass, subscribers] = schema[UserCreatedEvent.key] as [
73
+ unknown,
74
+ unknown[],
75
+ ];
76
+ expect(eventClass).toBe(UserCreatedEvent);
77
+ expect(subscribers).toHaveLength(1);
78
+ expect(subscribers[0]).toHaveProperty('name', 'TestService.onUserCreated');
79
+ expect(subscribers[0]).toHaveProperty(
80
+ 'description',
81
+ 'Handles user creation',
82
+ );
83
+ });
84
+
85
+ it('uses custom name when provided', () => {
86
+ class TestService {
87
+ @OnMatadorEvent(UserCreatedEvent, {
88
+ name: 'custom-handler-name',
89
+ description: 'Handles user creation',
90
+ })
91
+ async onUserCreated(_envelope: Envelope<UserCreatedEvent['data']>) {
92
+ // handler
93
+ }
94
+ }
95
+
96
+ const instance = new TestService();
97
+
98
+ mockNestDiscovery.getProviders = () =>
99
+ [
100
+ {
101
+ instance,
102
+ },
103
+ ] ;
104
+
105
+ discoveryService = new SubscriberDiscoveryService(
106
+ mockNestDiscovery as unknown as DiscoveryService,
107
+ );
108
+
109
+ discoveryService.onModuleInit();
110
+
111
+ const schema = discoveryService.getSchema();
112
+ const [, subscribers] = schema[UserCreatedEvent.key] as [
113
+ unknown,
114
+ unknown[],
115
+ ];
116
+ expect(subscribers[0]).toHaveProperty('name', 'custom-handler-name');
117
+ });
118
+
119
+ it('discovers multiple handlers for the same event', () => {
120
+ class ServiceA {
121
+ @OnMatadorEvent(UserCreatedEvent, {
122
+ description: 'Handler A',
123
+ })
124
+ async handle(_envelope: Envelope<UserCreatedEvent['data']>) {
125
+ // handler
126
+ }
127
+ }
128
+
129
+ class ServiceB {
130
+ @OnMatadorEvent(UserCreatedEvent, {
131
+ description: 'Handler B',
132
+ })
133
+ async handle(_envelope: Envelope<UserCreatedEvent['data']>) {
134
+ // handler
135
+ }
136
+ }
137
+
138
+ const instanceA = new ServiceA();
139
+ const instanceB = new ServiceB();
140
+
141
+ mockNestDiscovery.getProviders = () =>
142
+ [{ instance: instanceA }, { instance: instanceB }] ;
143
+
144
+ discoveryService = new SubscriberDiscoveryService(
145
+ mockNestDiscovery as unknown as DiscoveryService,
146
+ );
147
+
148
+ discoveryService.onModuleInit();
149
+
150
+ const schema = discoveryService.getSchema();
151
+ const [, subscribers] = schema[UserCreatedEvent.key] as [
152
+ unknown,
153
+ unknown[],
154
+ ];
155
+ expect(subscribers).toHaveLength(2);
156
+ });
157
+
158
+ it('discovers handlers for different events', () => {
159
+ class TestService {
160
+ @OnMatadorEvent(UserCreatedEvent, {
161
+ description: 'User handler',
162
+ })
163
+ async onUserCreated(_envelope: Envelope<UserCreatedEvent['data']>) {
164
+ // handler
165
+ }
166
+
167
+ @OnMatadorEvent(OrderPlacedEvent, {
168
+ description: 'Order handler',
169
+ })
170
+ async onOrderPlaced(_envelope: Envelope<OrderPlacedEvent['data']>) {
171
+ // handler
172
+ }
173
+ }
174
+
175
+ const instance = new TestService();
176
+
177
+ mockNestDiscovery.getProviders = () =>
178
+ [
179
+ {
180
+ instance,
181
+ },
182
+ ] ;
183
+
184
+ discoveryService = new SubscriberDiscoveryService(
185
+ mockNestDiscovery as unknown as DiscoveryService,
186
+ );
187
+
188
+ discoveryService.onModuleInit();
189
+
190
+ const schema = discoveryService.getSchema();
191
+
192
+ expect(schema[UserCreatedEvent.key]).toBeDefined();
193
+ expect(schema[OrderPlacedEvent.key]).toBeDefined();
194
+ });
195
+
196
+ it('skips providers without decorated methods', () => {
197
+ class PlainService {
198
+ someMethod() {
199
+ return 'plain';
200
+ }
201
+ }
202
+
203
+ const instance = new PlainService();
204
+
205
+ mockNestDiscovery.getProviders = () =>
206
+ [
207
+ {
208
+ instance,
209
+ },
210
+ ] ;
211
+
212
+ discoveryService = new SubscriberDiscoveryService(
213
+ mockNestDiscovery as unknown as DiscoveryService,
214
+ );
215
+
216
+ discoveryService.onModuleInit();
217
+
218
+ const schema = discoveryService.getSchema();
219
+ expect(Object.keys(schema)).toHaveLength(0);
220
+ });
221
+
222
+ it('merges additional events from options', () => {
223
+ class TestService {
224
+ @OnMatadorEvent(UserCreatedEvent, {
225
+ description: 'Discovered handler',
226
+ })
227
+ async handle(_envelope: Envelope<UserCreatedEvent['data']>) {
228
+ // handler
229
+ }
230
+ }
231
+
232
+ const instance = new TestService();
233
+
234
+ mockNestDiscovery.getProviders = () =>
235
+ [
236
+ {
237
+ instance,
238
+ },
239
+ ] ;
240
+
241
+ discoveryService = new SubscriberDiscoveryService(
242
+ mockNestDiscovery as unknown as DiscoveryService,
243
+ );
244
+
245
+ discoveryService.onModuleInit();
246
+
247
+ // Additional event with subscriber
248
+ const additionalSubscriber = {
249
+ name: 'additional-subscriber',
250
+ description: 'Additional handler',
251
+ callback: async () => {},
252
+ importance: 'should-investigate' as const,
253
+ idempotent: 'unknown' as const,
254
+ };
255
+
256
+ const additionalEvents = new Map<EventClass<unknown>, AnySubscriber[]>();
257
+ additionalEvents.set(
258
+ OrderPlacedEvent as EventClass<unknown>,
259
+ [additionalSubscriber],
260
+ );
261
+
262
+ const mergedSchema = discoveryService.getMergedSchema({
263
+ transport: {} as never,
264
+ topology: {} as never,
265
+ additionalEvents,
266
+ });
267
+
268
+ expect(mergedSchema[UserCreatedEvent.key]).toBeDefined();
269
+ expect(mergedSchema[OrderPlacedEvent.key]).toBeDefined();
270
+
271
+ const [, orderSubscribers] = mergedSchema[OrderPlacedEvent.key] as [
272
+ unknown,
273
+ unknown[],
274
+ ];
275
+ expect(orderSubscribers).toHaveLength(1);
276
+ expect(orderSubscribers[0]).toHaveProperty('name', 'additional-subscriber');
277
+ });
278
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "target": "ES2022",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "lib": ["ES2022"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "outDir": "./dist",
15
+ "rootDir": "./src",
16
+ "noEmit": false,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "noImplicitReturns": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "exactOptionalPropertyTypes": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "resolveJsonModule": true,
24
+ "isolatedModules": true,
25
+ "verbatimModuleSyntax": true,
26
+ "experimentalDecorators": true,
27
+ "emitDecoratorMetadata": true
28
+ },
29
+ "include": ["src/**/*"],
30
+ "exclude": ["node_modules", "dist"],
31
+ "references": [{ "path": "../matador" }]
32
+ }