@zdavison/matador 2.0.2 → 2.0.3

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.
@@ -31,10 +31,24 @@ export declare class TopologyBuilder {
31
31
  withNamespace(namespace: string): this;
32
32
  /**
33
33
  * Adds a queue to the topology.
34
+ * @param definition - A complete QueueDefinition object
35
+ */
36
+ addQueue(definition: QueueDefinition): this;
37
+ /**
38
+ * Adds a queue to the topology.
39
+ * @param name - Queue name
40
+ * @param options - Queue options
34
41
  */
35
42
  addQueue(name: string, options?: QueueOptions): this;
36
43
  /**
37
44
  * Alias for addQueue().
45
+ * @param definition - A complete QueueDefinition object
46
+ */
47
+ queue(definition: QueueDefinition): this;
48
+ /**
49
+ * Alias for addQueue().
50
+ * @param name - Queue name
51
+ * @param options - Queue options
38
52
  */
39
53
  queue(name: string, options?: QueueOptions): this;
40
54
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/topology/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;AAEzD;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAM,YAAW,cAAc;aASxD,MAAM,EAAE,SAAS,MAAM,EAAE;IAR3C,QAAQ,CAAC,WAAW,SAIuC;gBAGzD,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,SAAS,MAAM,EAAE;CAK5C;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B;;OAEG;IACH,MAAM,CAAC,MAAM,IAAI,eAAe;IAIhC,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,UAAU,CAGhB;IACF,OAAO,CAAC,KAAK,CAIX;IAEF;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKtC;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,IAAI;IAKxD;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,IAAI;IAIrD;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI;IAQvD;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI;IAS7C;;OAEG;IACH,YAAY,IAAI,IAAI;IAKpB;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAQzB;;OAEG;IACH,QAAQ,IAAI,SAAS,MAAM,EAAE;IAsD7B;;;OAGG;IACH,KAAK,IAAI,QAAQ;CAgBlB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/topology/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;AAWzD;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAM,YAAW,cAAc;aASxD,MAAM,EAAE,SAAS,MAAM,EAAE;IAR3C,QAAQ,CAAC,WAAW,SAIuC;gBAGzD,OAAO,EAAE,MAAM,EACC,MAAM,EAAE,SAAS,MAAM,EAAE;CAK5C;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B;;OAEG;IACH,MAAM,CAAC,MAAM,IAAI,eAAe;IAIhC,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,UAAU,CAGhB;IACF,OAAO,CAAC,KAAK,CAIX;IAEF;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKtC;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI;IAapD;;;OAGG;IACH,KAAK,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI;IACxC;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI;IAWjD;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI;IAQvD;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI;IAS7C;;OAEG;IACH,YAAY,IAAI,IAAI;IAKpB;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAQzB;;OAEG;IACH,QAAQ,IAAI,SAAS,MAAM,EAAE;IAsD7B;;;OAGG;IACH,KAAK,IAAI,QAAQ;CAgBlB"}
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Type guard to check if the argument is a QueueDefinition object.
3
+ */
4
+ function isQueueDefinition(arg) {
5
+ return typeof arg === 'object' && arg !== null && 'name' in arg;
6
+ }
1
7
  /**
2
8
  * Error thrown when topology validation fails.
3
9
  */
@@ -41,18 +47,20 @@ export class TopologyBuilder {
41
47
  this.namespace = namespace;
42
48
  return this;
43
49
  }
44
- /**
45
- * Adds a queue to the topology.
46
- */
47
- addQueue(name, options = {}) {
48
- this.queues.push({ name, ...options });
50
+ addQueue(nameOrDefinition, options = {}) {
51
+ if (isQueueDefinition(nameOrDefinition)) {
52
+ this.queues.push(nameOrDefinition);
53
+ }
54
+ else {
55
+ this.queues.push({ name: nameOrDefinition, ...options });
56
+ }
49
57
  return this;
50
58
  }
51
- /**
52
- * Alias for addQueue().
53
- */
54
- queue(name, options = {}) {
55
- return this.addQueue(name, options);
59
+ queue(nameOrDefinition, options = {}) {
60
+ if (isQueueDefinition(nameOrDefinition)) {
61
+ return this.addQueue(nameOrDefinition);
62
+ }
63
+ return this.addQueue(nameOrDefinition, options);
56
64
  }
57
65
  /**
58
66
  * Configures dead-letter queue settings.
@@ -219,4 +219,74 @@ describe('TopologyBuilder', () => {
219
219
  expect(topology.queues[0]?.exact).toBe(true);
220
220
  });
221
221
  });
222
+ describe('addQueue with QueueDefinition object', () => {
223
+ it('should accept a QueueDefinition object', () => {
224
+ const queueDef = {
225
+ name: 'events',
226
+ concurrency: 5,
227
+ consumerTimeout: 30000,
228
+ };
229
+ const topology = TopologyBuilder.create()
230
+ .withNamespace('test')
231
+ .addQueue(queueDef)
232
+ .build();
233
+ expect(topology.queues).toHaveLength(1);
234
+ expect(topology.queues[0]?.name).toBe('events');
235
+ expect(topology.queues[0]?.concurrency).toBe(5);
236
+ expect(topology.queues[0]?.consumerTimeout).toBe(30000);
237
+ });
238
+ it('should allow reusing queue definitions across builders', () => {
239
+ const sharedQueue = {
240
+ name: 'shared-queue',
241
+ concurrency: 10,
242
+ exact: true,
243
+ };
244
+ const topology1 = TopologyBuilder.create()
245
+ .withNamespace('app1')
246
+ .addQueue(sharedQueue)
247
+ .build();
248
+ const topology2 = TopologyBuilder.create()
249
+ .withNamespace('app2')
250
+ .addQueue(sharedQueue)
251
+ .build();
252
+ expect(topology1.queues[0]).toEqual(sharedQueue);
253
+ expect(topology2.queues[0]).toEqual(sharedQueue);
254
+ });
255
+ it('should work with queue() alias', () => {
256
+ const queueDef = {
257
+ name: 'analytics',
258
+ priorities: true,
259
+ };
260
+ const topology = TopologyBuilder.create()
261
+ .withNamespace('test')
262
+ .queue(queueDef)
263
+ .build();
264
+ expect(topology.queues[0]?.name).toBe('analytics');
265
+ expect(topology.queues[0]?.priorities).toBe(true);
266
+ });
267
+ it('should mix QueueDefinition objects with name+options style', () => {
268
+ const reusableQueue = {
269
+ name: 'shared',
270
+ concurrency: 3,
271
+ };
272
+ const topology = TopologyBuilder.create()
273
+ .withNamespace('test')
274
+ .addQueue(reusableQueue)
275
+ .addQueue('local', { concurrency: 1 })
276
+ .build();
277
+ expect(topology.queues).toHaveLength(2);
278
+ expect(topology.queues[0]?.name).toBe('shared');
279
+ expect(topology.queues[1]?.name).toBe('local');
280
+ });
281
+ it('should validate QueueDefinition objects the same way', () => {
282
+ const invalidQueue = {
283
+ name: '123invalid',
284
+ concurrency: 5,
285
+ };
286
+ const builder = TopologyBuilder.create()
287
+ .withNamespace('test')
288
+ .addQueue(invalidQueue);
289
+ expect(() => builder.build()).toThrow('must start with a letter');
290
+ });
291
+ });
222
292
  });
@@ -1 +1 @@
1
- {"version":3,"file":"local-transport.d.ts","sourceRoot":"","sources":["../../../src/transport/local/local-transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAiB,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACV,MAAM,iBAAiB,CAAC;AAiCzB;;;GAGG;AACH,qBAAa,cAAe,YAAW,SAAS;IAC9C,QAAQ,CAAC,IAAI,WAAW;IACxB,QAAQ,CAAC,YAAY,wBAAqB;IAE1C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsC;IAC7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2C;IACzE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAwB;IAC1D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4C;IAC1E,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,CAAC,EAAE,MAAM;IAIrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBjC,WAAW,IAAI,OAAO;IAIhB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBhD,IAAI,CACR,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAe7B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;YAkBhB,OAAO;YAgBP,oBAAoB;IA4B5B,SAAS,CACb,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,YAAY,CAAC;IAsClB,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAMhD,gBAAgB,CACpB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAQhB;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAMnC;;OAEG;IACH,YAAY,IAAI,SAAS,cAAc,EAAE;IAIzC;;OAEG;IACH,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,QAAQ,EAAE;IAMtD;;OAEG;IACH,KAAK,IAAI,IAAI;IAYb;;;OAGG;IACG,UAAU,CACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,cAAc,CAAA;KAAE,GAAG,IAAI,CAAC;IAmBlE,OAAO,CAAC,gBAAgB;CAQzB"}
1
+ {"version":3,"file":"local-transport.d.ts","sourceRoot":"","sources":["../../../src/transport/local/local-transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAiB,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACV,MAAM,iBAAiB,CAAC;AAiCzB;;;GAGG;AACH,qBAAa,cAAe,YAAW,SAAS;IAC9C,QAAQ,CAAC,IAAI,WAAW;IACxB,QAAQ,CAAC,YAAY,wBAAqB;IAE1C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsC;IAC7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2C;IACzE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAwB;IAC1D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4C;IAC1E,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,CAAC,EAAE,MAAM;IAIrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBjC,WAAW,IAAI,OAAO;IAIhB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBhD,IAAI,CACR,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAe7B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;YAkBhB,OAAO;YAgBP,oBAAoB;IA+B5B,SAAS,CACb,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,YAAY,CAAC;IAsClB,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAMhD,gBAAgB,CACpB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAQhB;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAMnC;;OAEG;IACH,YAAY,IAAI,SAAS,cAAc,EAAE;IAIzC;;OAEG;IACH,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,QAAQ,EAAE;IAMtD;;OAEG;IACH,KAAK,IAAI,IAAI;IAYb;;;OAGG;IACG,UAAU,CACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,cAAc,CAAA;KAAE,GAAG,IAAI,CAAC;IAmBlE,OAAO,CAAC,gBAAgB;CAQzB"}
@@ -21,6 +21,14 @@ export interface RabbitMQTransportConfig {
21
21
  /** Logger for transport events (defaults to console) */
22
22
  readonly logger?: Logger | undefined;
23
23
  }
24
+ /**
25
+ * Redacts credentials from an AMQP URL.
26
+ * Replaces username and password with '****' regardless of their length.
27
+ *
28
+ * @example
29
+ * redactAmqpUrl('amqp://user:pass@host:5672') // 'amqp://****:****@host:5672'
30
+ */
31
+ export declare function redactAmqpUrl(url: string): string;
24
32
  /**
25
33
  * RabbitMQ transport implementation using amqplib.
26
34
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rabbitmq-transport.d.ts","sourceRoot":"","sources":["../../../src/transport/rabbitmq/rabbitmq-transport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rabbitmq-transport.d.ts","sourceRoot":"","sources":["../../../src/transport/rabbitmq/rabbitmq-transport.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,17 @@ import { RabbitMQCodec } from '../../codec/rabbitmq-codec.js';
3
3
  import { DelayedMessagesNotSupportedError, TransportNotConnectedError, } from '../../errors/index.js';
4
4
  import { consoleLogger } from '../../hooks/index.js';
5
5
  import { ConnectionManager, } from '../connection-manager.js';
6
+ /**
7
+ * Redacts credentials from an AMQP URL.
8
+ * Replaces username and password with '****' regardless of their length.
9
+ *
10
+ * @example
11
+ * redactAmqpUrl('amqp://user:pass@host:5672') // 'amqp://****:****@host:5672'
12
+ */
13
+ export function redactAmqpUrl(url) {
14
+ const regex = /^(amqps?:\/\/)[^:]+:[^@]+@/;
15
+ return url.replace(regex, '$1****:****@');
16
+ }
6
17
  /**
7
18
  * RabbitMQ transport implementation using amqplib.
8
19
  */
@@ -260,6 +271,7 @@ export class RabbitMQTransport {
260
271
  return queueChannel;
261
272
  }
262
273
  async doConnect() {
274
+ this.logger.info(`[Matador] ⏳ Connecting to RabbitMQ at '${redactAmqpUrl(this.config.url)}'.`);
263
275
  const connection = await amqplib.connect(this.config.url);
264
276
  this.connection = connection;
265
277
  // Handle connection errors - let ConnectionManager handle reconnection
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rabbitmq-transport.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rabbitmq-transport.test.d.ts","sourceRoot":"","sources":["../../../src/transport/rabbitmq/rabbitmq-transport.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it, mock } from 'bun:test';
2
+ import { redactAmqpUrl, RabbitMQTransport } from './rabbitmq-transport.js';
3
+ describe('redactAmqpUrl', () => {
4
+ it('should redact username and password with 4 asterisks', () => {
5
+ const url = 'amqp://myuser:mypassword@localhost:5672';
6
+ const redacted = redactAmqpUrl(url);
7
+ expect(redacted).toBe('amqp://****:****@localhost:5672');
8
+ });
9
+ it('should redact credentials in amqps URLs', () => {
10
+ const url = 'amqps://admin:secret123@rabbitmq.example.com:5671';
11
+ const redacted = redactAmqpUrl(url);
12
+ expect(redacted).toBe('amqps://****:****@rabbitmq.example.com:5671');
13
+ });
14
+ it('should redact long credentials to exactly 4 asterisks', () => {
15
+ const url = 'amqp://verylongusername:verylongpassword@rabbitmq-cluster.svc.local:5672';
16
+ const redacted = redactAmqpUrl(url);
17
+ expect(redacted).toBe('amqp://****:****@rabbitmq-cluster.svc.local:5672');
18
+ });
19
+ it('should redact short credentials to exactly 4 asterisks', () => {
20
+ const url = 'amqp://a:b@host:5672';
21
+ const redacted = redactAmqpUrl(url);
22
+ expect(redacted).toBe('amqp://****:****@host:5672');
23
+ });
24
+ it('should preserve vhost in URL', () => {
25
+ const url = 'amqp://user:pass@host:5672/myvhost';
26
+ const redacted = redactAmqpUrl(url);
27
+ expect(redacted).toBe('amqp://****:****@host:5672/myvhost');
28
+ });
29
+ it('should not modify URL without credentials', () => {
30
+ const url = 'amqp://localhost:5672';
31
+ const redacted = redactAmqpUrl(url);
32
+ expect(redacted).toBe('amqp://localhost:5672');
33
+ });
34
+ it('should not modify URL with only username (no password)', () => {
35
+ const url = 'amqp://guest@localhost:5672';
36
+ const redacted = redactAmqpUrl(url);
37
+ expect(redacted).toBe('amqp://guest@localhost:5672');
38
+ });
39
+ it('should handle URL-encoded credentials', () => {
40
+ // URL-encoded @ in password: p%40ssword
41
+ const url = 'amqp://user:p%40ssword@host:5672';
42
+ const redacted = redactAmqpUrl(url);
43
+ expect(redacted).toBe('amqp://****:****@host:5672');
44
+ });
45
+ });
46
+ describe('RabbitMQTransport', () => {
47
+ describe('connection logging', () => {
48
+ it('should log redacted connection URL when connecting', async () => {
49
+ const mockLogger = {
50
+ debug: mock(() => { }),
51
+ info: mock(() => { }),
52
+ warn: mock(() => { }),
53
+ error: mock(() => { }),
54
+ };
55
+ const transport = new RabbitMQTransport({
56
+ url: 'amqp://testuser:testpass@localhost:5672',
57
+ logger: mockLogger,
58
+ connection: {
59
+ maxReconnectAttempts: 1, // Only try once to avoid long retries
60
+ initialReconnectDelay: 10,
61
+ },
62
+ });
63
+ // Attempt to connect - it will fail since there's no RabbitMQ server
64
+ // but the log should still be emitted before the connection attempt
65
+ try {
66
+ await transport.connect();
67
+ }
68
+ catch {
69
+ // Expected to fail - no RabbitMQ server running
70
+ }
71
+ // Verify the log was called with the redacted URL
72
+ expect(mockLogger.info).toHaveBeenCalledWith("[Matador] \u23F3 Connecting to RabbitMQ at 'amqp://****:****@localhost:5672'.");
73
+ });
74
+ it('should log connection URL as-is when no credentials provided', async () => {
75
+ const mockLogger = {
76
+ debug: mock(() => { }),
77
+ info: mock(() => { }),
78
+ warn: mock(() => { }),
79
+ error: mock(() => { }),
80
+ };
81
+ const transport = new RabbitMQTransport({
82
+ url: 'amqp://localhost:5672',
83
+ logger: mockLogger,
84
+ connection: {
85
+ maxReconnectAttempts: 1,
86
+ initialReconnectDelay: 10,
87
+ },
88
+ });
89
+ try {
90
+ await transport.connect();
91
+ }
92
+ catch {
93
+ // Expected to fail
94
+ }
95
+ expect(mockLogger.info).toHaveBeenCalledWith("[Matador] \u23F3 Connecting to RabbitMQ at 'amqp://localhost:5672'.");
96
+ });
97
+ });
98
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zdavison/matador",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "A batteries-included event system.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -199,12 +199,23 @@ export class Matador {
199
199
  }
200
200
 
201
201
  // Connect transport
202
+ this.hooks.logger.info(
203
+ `[Matador] ⏳ Starting backend '${this.transport.name}'...`,
204
+ );
202
205
  await this.transport.connect();
206
+ this.hooks.logger.info(
207
+ `[Matador] 🟢 Backend '${this.transport.name}' started.`,
208
+ );
203
209
 
204
210
  // Apply topology
205
211
  await this.transport.applyTopology(this.topology);
206
212
 
207
213
  // Subscribe to queues
214
+ if (this.consumeFrom.length > 0) {
215
+ this.hooks.logger.info(
216
+ `[Matador] 🟢 Worker subscribing to '${this.consumeFrom.join(',')}'.`,
217
+ );
218
+ }
208
219
  for (const queueName of this.consumeFrom) {
209
220
  const qualifiedName = getQualifiedQueueName(
210
221
  this.topology.namespace,
@@ -272,4 +272,90 @@ describe('TopologyBuilder', () => {
272
272
  expect(topology.queues[0]?.exact).toBe(true);
273
273
  });
274
274
  });
275
+
276
+ describe('addQueue with QueueDefinition object', () => {
277
+ it('should accept a QueueDefinition object', () => {
278
+ const queueDef = {
279
+ name: 'events',
280
+ concurrency: 5,
281
+ consumerTimeout: 30000,
282
+ };
283
+
284
+ const topology = TopologyBuilder.create()
285
+ .withNamespace('test')
286
+ .addQueue(queueDef)
287
+ .build();
288
+
289
+ expect(topology.queues).toHaveLength(1);
290
+ expect(topology.queues[0]?.name).toBe('events');
291
+ expect(topology.queues[0]?.concurrency).toBe(5);
292
+ expect(topology.queues[0]?.consumerTimeout).toBe(30000);
293
+ });
294
+
295
+ it('should allow reusing queue definitions across builders', () => {
296
+ const sharedQueue = {
297
+ name: 'shared-queue',
298
+ concurrency: 10,
299
+ exact: true,
300
+ };
301
+
302
+ const topology1 = TopologyBuilder.create()
303
+ .withNamespace('app1')
304
+ .addQueue(sharedQueue)
305
+ .build();
306
+
307
+ const topology2 = TopologyBuilder.create()
308
+ .withNamespace('app2')
309
+ .addQueue(sharedQueue)
310
+ .build();
311
+
312
+ expect(topology1.queues[0]).toEqual(sharedQueue);
313
+ expect(topology2.queues[0]).toEqual(sharedQueue);
314
+ });
315
+
316
+ it('should work with queue() alias', () => {
317
+ const queueDef = {
318
+ name: 'analytics',
319
+ priorities: true,
320
+ };
321
+
322
+ const topology = TopologyBuilder.create()
323
+ .withNamespace('test')
324
+ .queue(queueDef)
325
+ .build();
326
+
327
+ expect(topology.queues[0]?.name).toBe('analytics');
328
+ expect(topology.queues[0]?.priorities).toBe(true);
329
+ });
330
+
331
+ it('should mix QueueDefinition objects with name+options style', () => {
332
+ const reusableQueue = {
333
+ name: 'shared',
334
+ concurrency: 3,
335
+ };
336
+
337
+ const topology = TopologyBuilder.create()
338
+ .withNamespace('test')
339
+ .addQueue(reusableQueue)
340
+ .addQueue('local', { concurrency: 1 })
341
+ .build();
342
+
343
+ expect(topology.queues).toHaveLength(2);
344
+ expect(topology.queues[0]?.name).toBe('shared');
345
+ expect(topology.queues[1]?.name).toBe('local');
346
+ });
347
+
348
+ it('should validate QueueDefinition objects the same way', () => {
349
+ const invalidQueue = {
350
+ name: '123invalid',
351
+ concurrency: 5,
352
+ };
353
+
354
+ const builder = TopologyBuilder.create()
355
+ .withNamespace('test')
356
+ .addQueue(invalidQueue);
357
+
358
+ expect(() => builder.build()).toThrow('must start with a letter');
359
+ });
360
+ });
275
361
  });
@@ -12,6 +12,15 @@ import type {
12
12
  */
13
13
  export type QueueOptions = Omit<QueueDefinition, 'name'>;
14
14
 
15
+ /**
16
+ * Type guard to check if the argument is a QueueDefinition object.
17
+ */
18
+ function isQueueDefinition(
19
+ arg: string | QueueDefinition,
20
+ ): arg is QueueDefinition {
21
+ return typeof arg === 'object' && arg !== null && 'name' in arg;
22
+ }
23
+
15
24
  /**
16
25
  * Error thrown when topology validation fails.
17
26
  */
@@ -64,17 +73,46 @@ export class TopologyBuilder {
64
73
 
65
74
  /**
66
75
  * Adds a queue to the topology.
76
+ * @param definition - A complete QueueDefinition object
77
+ */
78
+ addQueue(definition: QueueDefinition): this;
79
+ /**
80
+ * Adds a queue to the topology.
81
+ * @param name - Queue name
82
+ * @param options - Queue options
67
83
  */
68
- addQueue(name: string, options: QueueOptions = {}): this {
69
- this.queues.push({ name, ...options });
84
+ addQueue(name: string, options?: QueueOptions): this;
85
+ addQueue(
86
+ nameOrDefinition: string | QueueDefinition,
87
+ options: QueueOptions = {},
88
+ ): this {
89
+ if (isQueueDefinition(nameOrDefinition)) {
90
+ this.queues.push(nameOrDefinition);
91
+ } else {
92
+ this.queues.push({ name: nameOrDefinition, ...options });
93
+ }
70
94
  return this;
71
95
  }
72
96
 
73
97
  /**
74
98
  * Alias for addQueue().
99
+ * @param definition - A complete QueueDefinition object
100
+ */
101
+ queue(definition: QueueDefinition): this;
102
+ /**
103
+ * Alias for addQueue().
104
+ * @param name - Queue name
105
+ * @param options - Queue options
75
106
  */
76
- queue(name: string, options: QueueOptions = {}): this {
77
- return this.addQueue(name, options);
107
+ queue(name: string, options?: QueueOptions): this;
108
+ queue(
109
+ nameOrDefinition: string | QueueDefinition,
110
+ options: QueueOptions = {},
111
+ ): this {
112
+ if (isQueueDefinition(nameOrDefinition)) {
113
+ return this.addQueue(nameOrDefinition);
114
+ }
115
+ return this.addQueue(nameOrDefinition, options);
78
116
  }
79
117
 
80
118
  /**
@@ -185,7 +185,10 @@ export class LocalTransport implements Transport {
185
185
  await sub.handler(message.envelope, receipt);
186
186
  } catch (error) {
187
187
  // Handler errors should be caught in the pipeline
188
- this.logger.error('[Matador] 🔴 Handler error in message processing', error);
188
+ this.logger.error(
189
+ '[Matador] 🔴 Handler error in message processing',
190
+ error,
191
+ );
189
192
  }
190
193
  }
191
194
  }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it, mock } from 'bun:test';
2
+ import type { Logger } from '../../hooks/index.js';
3
+ import { redactAmqpUrl, RabbitMQTransport } from './rabbitmq-transport.js';
4
+
5
+ describe('redactAmqpUrl', () => {
6
+ it('should redact username and password with 4 asterisks', () => {
7
+ const url = 'amqp://myuser:mypassword@localhost:5672';
8
+ const redacted = redactAmqpUrl(url);
9
+ expect(redacted).toBe('amqp://****:****@localhost:5672');
10
+ });
11
+
12
+ it('should redact credentials in amqps URLs', () => {
13
+ const url = 'amqps://admin:secret123@rabbitmq.example.com:5671';
14
+ const redacted = redactAmqpUrl(url);
15
+ expect(redacted).toBe('amqps://****:****@rabbitmq.example.com:5671');
16
+ });
17
+
18
+ it('should redact long credentials to exactly 4 asterisks', () => {
19
+ const url =
20
+ 'amqp://verylongusername:verylongpassword@rabbitmq-cluster.svc.local:5672';
21
+ const redacted = redactAmqpUrl(url);
22
+ expect(redacted).toBe('amqp://****:****@rabbitmq-cluster.svc.local:5672');
23
+ });
24
+
25
+ it('should redact short credentials to exactly 4 asterisks', () => {
26
+ const url = 'amqp://a:b@host:5672';
27
+ const redacted = redactAmqpUrl(url);
28
+ expect(redacted).toBe('amqp://****:****@host:5672');
29
+ });
30
+
31
+ it('should preserve vhost in URL', () => {
32
+ const url = 'amqp://user:pass@host:5672/myvhost';
33
+ const redacted = redactAmqpUrl(url);
34
+ expect(redacted).toBe('amqp://****:****@host:5672/myvhost');
35
+ });
36
+
37
+ it('should not modify URL without credentials', () => {
38
+ const url = 'amqp://localhost:5672';
39
+ const redacted = redactAmqpUrl(url);
40
+ expect(redacted).toBe('amqp://localhost:5672');
41
+ });
42
+
43
+ it('should not modify URL with only username (no password)', () => {
44
+ const url = 'amqp://guest@localhost:5672';
45
+ const redacted = redactAmqpUrl(url);
46
+ expect(redacted).toBe('amqp://guest@localhost:5672');
47
+ });
48
+
49
+ it('should handle URL-encoded credentials', () => {
50
+ // URL-encoded @ in password: p%40ssword
51
+ const url = 'amqp://user:p%40ssword@host:5672';
52
+ const redacted = redactAmqpUrl(url);
53
+ expect(redacted).toBe('amqp://****:****@host:5672');
54
+ });
55
+ });
56
+
57
+ describe('RabbitMQTransport', () => {
58
+ describe('connection logging', () => {
59
+ it('should log redacted connection URL when connecting', async () => {
60
+ const mockLogger: Logger = {
61
+ debug: mock(() => {}),
62
+ info: mock(() => {}),
63
+ warn: mock(() => {}),
64
+ error: mock(() => {}),
65
+ };
66
+
67
+ const transport = new RabbitMQTransport({
68
+ url: 'amqp://testuser:testpass@localhost:5672',
69
+ logger: mockLogger,
70
+ connection: {
71
+ maxReconnectAttempts: 1, // Only try once to avoid long retries
72
+ initialReconnectDelay: 10,
73
+ },
74
+ });
75
+
76
+ // Attempt to connect - it will fail since there's no RabbitMQ server
77
+ // but the log should still be emitted before the connection attempt
78
+ try {
79
+ await transport.connect();
80
+ } catch {
81
+ // Expected to fail - no RabbitMQ server running
82
+ }
83
+
84
+ // Verify the log was called with the redacted URL
85
+ expect(mockLogger.info).toHaveBeenCalledWith(
86
+ "[Matador] \u23F3 Connecting to RabbitMQ at 'amqp://****:****@localhost:5672'.",
87
+ );
88
+ });
89
+
90
+ it('should log connection URL as-is when no credentials provided', async () => {
91
+ const mockLogger: Logger = {
92
+ debug: mock(() => {}),
93
+ info: mock(() => {}),
94
+ warn: mock(() => {}),
95
+ error: mock(() => {}),
96
+ };
97
+
98
+ const transport = new RabbitMQTransport({
99
+ url: 'amqp://localhost:5672',
100
+ logger: mockLogger,
101
+ connection: {
102
+ maxReconnectAttempts: 1,
103
+ initialReconnectDelay: 10,
104
+ },
105
+ });
106
+
107
+ try {
108
+ await transport.connect();
109
+ } catch {
110
+ // Expected to fail
111
+ }
112
+
113
+ expect(mockLogger.info).toHaveBeenCalledWith(
114
+ "[Matador] \u23F3 Connecting to RabbitMQ at 'amqp://localhost:5672'.",
115
+ );
116
+ });
117
+ });
118
+ });