@amqp-contract/contract 0.1.4 → 0.2.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.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # @amqp-contract/contract
2
2
 
3
- Contract builder for amqp-contract - Define type-safe AMQP messaging contracts.
3
+ **Contract builder for amqp-contract - Define type-safe AMQP messaging contracts.**
4
+
5
+ [![CI](https://github.com/btravers/amqp-contract/actions/workflows/ci.yml/badge.svg)](https://github.com/btravers/amqp-contract/actions/workflows/ci.yml)
6
+ [![npm version](https://img.shields.io/npm/v/@amqp-contract/contract.svg?logo=npm)](https://www.npmjs.com/package/@amqp-contract/contract)
7
+ [![npm downloads](https://img.shields.io/npm/dm/@amqp-contract/contract.svg)](https://www.npmjs.com/package/@amqp-contract/contract)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?logo=typescript)](https://www.typescriptlang.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
10
 
5
11
  📖 **[Full documentation →](https://btravers.github.io/amqp-contract/api/contract)**
6
12
 
@@ -13,52 +19,61 @@ pnpm add @amqp-contract/contract
13
19
  ## Usage
14
20
 
15
21
  ```typescript
16
- import { defineContract, defineExchange, defineQueue, defineBinding, defineExchangeBinding, definePublisher, defineConsumer } from '@amqp-contract/contract';
22
+ import { defineContract, defineExchange, defineQueue, defineQueueBinding, defineExchangeBinding, definePublisher, defineConsumer, defineMessage } from '@amqp-contract/contract';
17
23
  import { z } from 'zod';
18
24
 
19
- // Define your contract
25
+ // Define exchanges and queues first so they can be referenced
26
+ const ordersExchange = defineExchange('orders', 'topic', { durable: true });
27
+ const analyticsExchange = defineExchange('analytics', 'topic', { durable: true });
28
+ const orderProcessingQueue = defineQueue('order-processing', { durable: true });
29
+ const analyticsProcessingQueue = defineQueue('analytics-processing', { durable: true });
30
+
31
+ // Define message schemas with metadata
32
+ const orderMessage = defineMessage(
33
+ z.object({
34
+ orderId: z.string(),
35
+ amount: z.number(),
36
+ }),
37
+ {
38
+ summary: 'Order created event',
39
+ description: 'Emitted when a new order is created',
40
+ }
41
+ );
42
+
43
+ // Define your contract using object references
20
44
  const contract = defineContract({
21
45
  exchanges: {
22
- orders: defineExchange('orders', 'topic', { durable: true }),
23
- analytics: defineExchange('analytics', 'topic', { durable: true }),
46
+ orders: ordersExchange,
47
+ analytics: analyticsExchange,
24
48
  },
25
49
  queues: {
26
- orderProcessing: defineQueue('order-processing', { durable: true }),
27
- analyticsProcessing: defineQueue('analytics-processing', { durable: true }),
50
+ orderProcessing: orderProcessingQueue,
51
+ analyticsProcessing: analyticsProcessingQueue,
28
52
  },
29
53
  bindings: {
30
54
  // Queue-to-exchange binding
31
- orderBinding: defineBinding('order-processing', 'orders', {
55
+ orderBinding: defineQueueBinding(orderProcessingQueue, ordersExchange, {
32
56
  routingKey: 'order.created',
33
57
  }),
34
58
  // Exchange-to-exchange binding
35
- analyticsBinding: defineExchangeBinding('analytics', 'orders', {
59
+ analyticsBinding: defineExchangeBinding(analyticsExchange, ordersExchange, {
36
60
  routingKey: 'order.#',
37
61
  }),
38
62
  // Queue receives from analytics exchange
39
- analyticsQueueBinding: defineBinding('analytics-processing', 'analytics', {
63
+ analyticsQueueBinding: defineQueueBinding(analyticsProcessingQueue, analyticsExchange, {
40
64
  routingKey: 'order.#',
41
65
  }),
42
66
  },
43
67
  publishers: {
44
- orderCreated: definePublisher('orders', z.object({
45
- orderId: z.string(),
46
- amount: z.number(),
47
- }), {
68
+ orderCreated: definePublisher(ordersExchange, orderMessage, {
48
69
  routingKey: 'order.created',
49
70
  }),
50
71
  },
51
72
  consumers: {
52
- processOrder: defineConsumer('order-processing', z.object({
53
- orderId: z.string(),
54
- amount: z.number(),
55
- }), {
73
+ processOrder: defineConsumer(orderProcessingQueue, orderMessage, {
56
74
  prefetch: 10,
57
75
  }),
58
- processAnalytics: defineConsumer('analytics-processing', z.object({
59
- orderId: z.string(),
60
- amount: z.number(),
61
- })),
76
+ processAnalytics: defineConsumer(analyticsProcessingQueue, orderMessage),
62
77
  },
63
78
  });
64
79
  ```
@@ -67,32 +82,77 @@ const contract = defineContract({
67
82
 
68
83
  ### `defineExchange(name, type, options?)`
69
84
 
70
- Define an AMQP exchange.
85
+ Define an AMQP exchange. Returns an exchange definition object that can be referenced by bindings and publishers.
86
+
87
+ **Types:** `'fanout'`, `'direct'`, or `'topic'`
71
88
 
72
89
  ### `defineQueue(name, options?)`
73
90
 
74
- Define an AMQP queue.
91
+ Define an AMQP queue. Returns a queue definition object that can be referenced by bindings and consumers.
92
+
93
+ ### `defineMessage(payloadSchema, options?)`
94
+
95
+ Define a message definition with a payload schema and optional metadata (headers, summary, description).
96
+ This is useful for documentation generation and type inference.
97
+
98
+ ### `defineQueueBinding(queue, exchange, options?)`
75
99
 
76
- ### `defineBinding(queue, exchange, options?)`
100
+ Define a binding between a queue and an exchange. Pass the queue and exchange objects (not strings).
77
101
 
78
- Define a binding between a queue and an exchange.
102
+ **For fanout exchanges:** Routing key is optional (fanout ignores routing keys).
103
+ **For direct/topic exchanges:** Routing key is required in options.
79
104
 
80
105
  ### `defineExchangeBinding(destination, source, options?)`
81
106
 
82
107
  Define a binding between two exchanges (source → destination). Messages published to the source exchange will be routed to the destination exchange based on the routing key pattern.
83
108
 
84
- ### `definePublisher(exchange, messageSchema, options?)`
109
+ Pass the exchange objects (not strings).
85
110
 
86
- Define a message publisher with validation schema.
111
+ ### `definePublisher(exchange, message, options?)`
87
112
 
88
- ### `defineConsumer(queue, messageSchema, options?)`
113
+ Define a message publisher with validation schema. Pass the exchange object (not a string).
89
114
 
90
- Define a message consumer with validation schema.
115
+ **For fanout exchanges:** Routing key is optional (fanout ignores routing keys).
116
+ **For direct/topic exchanges:** Routing key is required in options.
117
+
118
+ ### `defineConsumer(queue, message, options?)`
119
+
120
+ Define a message consumer with validation schema. Pass the queue object (not a string).
91
121
 
92
122
  ### `defineContract(definition)`
93
123
 
94
124
  Create a complete AMQP contract with exchanges, queues, bindings, publishers, and consumers.
95
125
 
126
+ ## Key Concepts
127
+
128
+ ### Composition Pattern
129
+
130
+ The contract API uses a composition pattern where you:
131
+
132
+ 1. Define exchanges and queues first as variables
133
+ 2. Reference these objects in bindings, publishers, and consumers
134
+ 3. Compose everything together in `defineContract`
135
+
136
+ This provides:
137
+
138
+ - **Better type safety**: TypeScript can validate exchange/queue types
139
+ - **Better refactoring**: Rename an exchange in one place
140
+ - **DRY principle**: Define once, reference many times
141
+
142
+ ### Exchange Types & Routing Keys
143
+
144
+ The API enforces routing key requirements based on exchange type:
145
+
146
+ - **Fanout exchanges**: Don't use routing keys (all messages go to all bound queues)
147
+ - **Direct exchanges**: Require explicit routing keys for exact matching
148
+ - **Topic exchanges**: Require routing key patterns (e.g., `order.*`, `order.#`)
149
+
150
+ TypeScript enforces these rules at compile time through discriminated unions.
151
+
152
+ ## Documentation
153
+
154
+ 📖 **[Read the full documentation →](https://btravers.github.io/amqp-contract)**
155
+
96
156
  ## License
97
157
 
98
158
  MIT
package/dist/index.cjs CHANGED
@@ -1,68 +1,212 @@
1
1
 
2
2
  //#region src/builder.ts
3
3
  /**
4
- * Define a message schema with metadata
4
+ * Define an AMQP exchange.
5
+ *
6
+ * An exchange receives messages from publishers and routes them to queues based on the exchange type
7
+ * and routing rules. This is the implementation function - use the type-specific overloads for better
8
+ * type safety.
9
+ *
10
+ * @param name - The name of the exchange
11
+ * @param type - The type of exchange: "fanout", "direct", or "topic"
12
+ * @param options - Optional exchange configuration
13
+ * @returns An exchange definition
14
+ * @internal
5
15
  */
6
- function defineMessage(name, schema) {
16
+ function defineExchange(name, type, options) {
7
17
  return {
8
18
  name,
9
- schema,
10
- "~standard": schema["~standard"]
19
+ type,
20
+ ...options
11
21
  };
12
22
  }
13
23
  /**
14
- * Define an AMQP exchange
24
+ * Define an AMQP queue.
25
+ *
26
+ * A queue stores messages until they are consumed by workers. Queues can be bound to exchanges
27
+ * to receive messages based on routing rules.
28
+ *
29
+ * @param name - The name of the queue
30
+ * @param options - Optional queue configuration
31
+ * @param options.durable - If true, the queue survives broker restarts (default: false)
32
+ * @param options.exclusive - If true, the queue can only be used by the declaring connection (default: false)
33
+ * @param options.autoDelete - If true, the queue is deleted when the last consumer unsubscribes (default: false)
34
+ * @param options.arguments - Additional AMQP arguments (e.g., x-message-ttl, x-dead-letter-exchange)
35
+ * @returns A queue definition
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const orderProcessingQueue = defineQueue('order-processing', {
40
+ * durable: true,
41
+ * arguments: {
42
+ * 'x-message-ttl': 86400000, // 24 hours
43
+ * 'x-dead-letter-exchange': 'orders-dlx'
44
+ * }
45
+ * });
46
+ * ```
15
47
  */
16
- function defineExchange(name, type, options) {
48
+ function defineQueue(name, options) {
17
49
  return {
18
50
  name,
19
- type,
20
51
  ...options
21
52
  };
22
53
  }
23
54
  /**
24
- * Define an AMQP queue
55
+ * Define a message definition with payload and optional headers/metadata.
56
+ *
57
+ * A message definition specifies the schema for message payloads and headers using
58
+ * Standard Schema v1 compatible libraries (Zod, Valibot, ArkType, etc.).
59
+ * The schemas are used for automatic validation when publishing or consuming messages.
60
+ *
61
+ * @param payload - The payload schema (must be Standard Schema v1 compatible)
62
+ * @param options - Optional message metadata
63
+ * @param options.headers - Optional header schema for message headers
64
+ * @param options.summary - Brief description for documentation (used in AsyncAPI generation)
65
+ * @param options.description - Detailed description for documentation (used in AsyncAPI generation)
66
+ * @returns A message definition with inferred types
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * import { z } from 'zod';
71
+ *
72
+ * const orderMessage = defineMessage(
73
+ * z.object({
74
+ * orderId: z.string().uuid(),
75
+ * customerId: z.string().uuid(),
76
+ * amount: z.number().positive(),
77
+ * items: z.array(z.object({
78
+ * productId: z.string(),
79
+ * quantity: z.number().int().positive(),
80
+ * })),
81
+ * }),
82
+ * {
83
+ * summary: 'Order created event',
84
+ * description: 'Emitted when a new order is created in the system'
85
+ * }
86
+ * );
87
+ * ```
25
88
  */
26
- function defineQueue(name, options) {
89
+ function defineMessage(payload, options) {
27
90
  return {
28
- name,
91
+ payload,
29
92
  ...options
30
93
  };
31
94
  }
32
95
  /**
33
- * Define a binding between queue and exchange
96
+ * Define a binding between a queue and an exchange.
97
+ *
98
+ * This is the implementation function - use the type-specific overloads for better type safety.
99
+ *
100
+ * @param queue - The queue definition to bind
101
+ * @param exchange - The exchange definition
102
+ * @param options - Optional binding configuration
103
+ * @returns A queue binding definition
104
+ * @internal
34
105
  */
35
- function defineBinding(queue, exchange, options) {
106
+ function defineQueueBinding(queue, exchange, options) {
107
+ if (exchange.type === "fanout") return {
108
+ type: "queue",
109
+ queue,
110
+ exchange,
111
+ ...options?.arguments && { arguments: options.arguments }
112
+ };
36
113
  return {
37
114
  type: "queue",
38
115
  queue,
39
116
  exchange,
40
- ...options
117
+ routingKey: options?.routingKey,
118
+ ...options?.arguments && { arguments: options.arguments }
41
119
  };
42
120
  }
43
121
  /**
44
- * Define a binding between exchange and exchange (source -> destination)
122
+ * Define a binding between two exchanges (exchange-to-exchange routing).
123
+ *
124
+ * This is the implementation function - use the type-specific overloads for better type safety.
125
+ *
126
+ * @param destination - The destination exchange definition
127
+ * @param source - The source exchange definition
128
+ * @param options - Optional binding configuration
129
+ * @returns An exchange binding definition
130
+ * @internal
45
131
  */
46
132
  function defineExchangeBinding(destination, source, options) {
133
+ if (source.type === "fanout") return {
134
+ type: "exchange",
135
+ source,
136
+ destination,
137
+ ...options?.arguments && { arguments: options.arguments }
138
+ };
47
139
  return {
48
140
  type: "exchange",
49
141
  source,
50
142
  destination,
51
- ...options
143
+ routingKey: options?.routingKey ?? "",
144
+ ...options?.arguments && { arguments: options.arguments }
52
145
  };
53
146
  }
54
147
  /**
55
- * Define a message publisher
148
+ * Define a message publisher.
149
+ *
150
+ * This is the implementation function - use the type-specific overloads for better type safety.
151
+ *
152
+ * @param exchange - The exchange definition
153
+ * @param message - The message definition
154
+ * @param options - Optional publisher configuration
155
+ * @returns A publisher definition
156
+ * @internal
56
157
  */
57
158
  function definePublisher(exchange, message, options) {
159
+ if (exchange.type === "fanout") return {
160
+ exchange,
161
+ message
162
+ };
58
163
  return {
59
164
  exchange,
60
165
  message,
61
- ...options
166
+ routingKey: options?.routingKey ?? ""
62
167
  };
63
168
  }
64
169
  /**
65
- * Define a message consumer
170
+ * Define a message consumer.
171
+ *
172
+ * A consumer receives and processes messages from a queue. The message schema is validated
173
+ * automatically when messages are consumed, ensuring type safety for your handlers.
174
+ *
175
+ * Consumers are associated with a specific queue and message type. When you create a worker
176
+ * with this consumer, it will process messages from the queue according to the schema.
177
+ *
178
+ * @param queue - The queue definition to consume from
179
+ * @param message - The message definition with payload schema
180
+ * @param options - Optional consumer configuration
181
+ * @returns A consumer definition with inferred message types
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * import { z } from 'zod';
186
+ *
187
+ * const orderQueue = defineQueue('order-processing', { durable: true });
188
+ * const orderMessage = defineMessage(
189
+ * z.object({
190
+ * orderId: z.string().uuid(),
191
+ * customerId: z.string().uuid(),
192
+ * amount: z.number().positive(),
193
+ * })
194
+ * );
195
+ *
196
+ * const processOrderConsumer = defineConsumer(orderQueue, orderMessage);
197
+ *
198
+ * // Later, when creating a worker, you'll provide a handler for this consumer:
199
+ * // const worker = await TypedAmqpWorker.create({
200
+ * // contract,
201
+ * // handlers: {
202
+ * // processOrder: async (message) => {
203
+ * // // message is automatically typed based on the schema
204
+ * // console.log(message.orderId); // string
205
+ * // }
206
+ * // },
207
+ * // connection
208
+ * // });
209
+ * ```
66
210
  */
67
211
  function defineConsumer(queue, message, options) {
68
212
  return {
@@ -72,18 +216,84 @@ function defineConsumer(queue, message, options) {
72
216
  };
73
217
  }
74
218
  /**
75
- * Define an AMQP contract
219
+ * Define an AMQP contract.
220
+ *
221
+ * A contract is the central definition of your AMQP messaging topology. It brings together
222
+ * all exchanges, queues, bindings, publishers, and consumers in a single, type-safe definition.
223
+ *
224
+ * The contract is used by both clients (for publishing) and workers (for consuming) to ensure
225
+ * type safety throughout your messaging infrastructure. TypeScript will infer all message types
226
+ * and publisher/consumer names from the contract.
227
+ *
228
+ * @param definition - The contract definition containing all AMQP resources
229
+ * @param definition.exchanges - Named exchange definitions
230
+ * @param definition.queues - Named queue definitions
231
+ * @param definition.bindings - Named binding definitions (queue-to-exchange or exchange-to-exchange)
232
+ * @param definition.publishers - Named publisher definitions for sending messages
233
+ * @param definition.consumers - Named consumer definitions for receiving messages
234
+ * @returns The same contract definition with full type inference
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * import {
239
+ * defineContract,
240
+ * defineExchange,
241
+ * defineQueue,
242
+ * defineQueueBinding,
243
+ * definePublisher,
244
+ * defineConsumer,
245
+ * defineMessage,
246
+ * } from '@amqp-contract/contract';
247
+ * import { z } from 'zod';
248
+ *
249
+ * // Define resources
250
+ * const ordersExchange = defineExchange('orders', 'topic', { durable: true });
251
+ * const orderQueue = defineQueue('order-processing', { durable: true });
252
+ * const orderMessage = defineMessage(
253
+ * z.object({
254
+ * orderId: z.string(),
255
+ * amount: z.number(),
256
+ * })
257
+ * );
258
+ *
259
+ * // Compose contract
260
+ * export const contract = defineContract({
261
+ * exchanges: {
262
+ * orders: ordersExchange,
263
+ * },
264
+ * queues: {
265
+ * orderProcessing: orderQueue,
266
+ * },
267
+ * bindings: {
268
+ * orderBinding: defineQueueBinding(orderQueue, ordersExchange, {
269
+ * routingKey: 'order.created',
270
+ * }),
271
+ * },
272
+ * publishers: {
273
+ * orderCreated: definePublisher(ordersExchange, orderMessage, {
274
+ * routingKey: 'order.created',
275
+ * }),
276
+ * },
277
+ * consumers: {
278
+ * processOrder: defineConsumer(orderQueue, orderMessage),
279
+ * },
280
+ * });
281
+ *
282
+ * // TypeScript now knows:
283
+ * // - client.publish('orderCreated', { orderId: string, amount: number })
284
+ * // - handler: async (message: { orderId: string, amount: number }) => void
285
+ * ```
76
286
  */
77
287
  function defineContract(definition) {
78
288
  return definition;
79
289
  }
80
290
 
81
291
  //#endregion
82
- exports.defineBinding = defineBinding;
83
292
  exports.defineConsumer = defineConsumer;
84
293
  exports.defineContract = defineContract;
85
294
  exports.defineExchange = defineExchange;
86
295
  exports.defineExchangeBinding = defineExchangeBinding;
87
296
  exports.defineMessage = defineMessage;
88
297
  exports.definePublisher = definePublisher;
89
- exports.defineQueue = defineQueue;
298
+ exports.defineQueue = defineQueue;
299
+ exports.defineQueueBinding = defineQueueBinding;