@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.
- package/dist/core/matador.d.ts.map +1 -1
- package/dist/core/matador.js +5 -0
- package/dist/index.cjs +36 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/topology/builder.d.ts +14 -0
- package/dist/topology/builder.d.ts.map +1 -1
- package/dist/topology/builder.js +18 -10
- package/dist/topology/builder.test.js +70 -0
- package/dist/transport/local/local-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +8 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -1
- package/dist/transport/rabbitmq/rabbitmq-transport.js +12 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts +2 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.test.d.ts.map +1 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.test.js +98 -0
- package/package.json +1 -1
- package/src/core/matador.ts +11 -0
- package/src/topology/builder.test.ts +86 -0
- package/src/topology/builder.ts +42 -4
- package/src/transport/local/local-transport.ts +4 -1
- package/src/transport/rabbitmq/rabbitmq-transport.test.ts +118 -0
- package/src/transport/rabbitmq/rabbitmq-transport.ts +15 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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;
|
|
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"}
|
package/dist/topology/builder.js
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return this.addQueue(
|
|
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;
|
|
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":"AAOA,OAAO,EAAE,KAAK,MAAM,EAAiB,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,EAAmB,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACV,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,8BAA8B;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB,uCAAuC;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,GAAG,SAAS,CAAC;IAEnE,uDAAuD;IACvD,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAE5C,wDAAwD;IACxD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE9C,8EAA8E;IAC9E,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAErD,wDAAwD;IACxD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAmBD;;GAEG;AACH,qBAAa,iBAAkB,YAAW,SAAS;IACjD,QAAQ,CAAC,IAAI,cAAc;IAE3B,OAAO,CAAC,aAAa,CAQnB;IAEF,IAAI,YAAY,IAAI,qBAAqB,CAExC;IAED,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAoB;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmC;IACjE,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuB;IAE7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAIrB;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,wBAAwB,CAAS;gBAE7B,MAAM,EAAE,uBAAuB;IAiBrC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC,WAAW,IAAI,OAAO;IAIhB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA0ChD,IAAI,CACR,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IA0DvB,SAAS,CACb,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,YAAY,CAAC;IAwFlB,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAahD,gBAAgB,CACpB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC;IA6ChB;;;;;OAKG;YACW,uBAAuB;YAqCvB,SAAS;
|
|
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 @@
|
|
|
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
package/src/core/matador.ts
CHANGED
|
@@ -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
|
});
|
package/src/topology/builder.ts
CHANGED
|
@@ -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
|
|
69
|
-
|
|
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
|
|
77
|
-
|
|
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(
|
|
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
|
+
});
|