rabbitmq-sdk 0.0.1-security → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rabbitmq-sdk might be problematic. Click here for more details.

Files changed (140) hide show
  1. package/.eslintrc.js +23 -0
  2. package/.kiro/specs/sdk-rabbitmq/design.md +369 -0
  3. package/.kiro/specs/sdk-rabbitmq/requirements.md +97 -0
  4. package/.kiro/specs/sdk-rabbitmq/tasks.md +248 -0
  5. package/README.md +273 -5
  6. package/bun.lock +790 -0
  7. package/config.example.json +13 -0
  8. package/dist/components/ConfigurationManager.d.ts +35 -0
  9. package/dist/components/ConfigurationManager.d.ts.map +1 -0
  10. package/dist/components/ConfigurationManager.js +118 -0
  11. package/dist/components/ConfigurationManager.js.map +1 -0
  12. package/dist/components/ConnectionManager.d.ts +93 -0
  13. package/dist/components/ConnectionManager.d.ts.map +1 -0
  14. package/dist/components/ConnectionManager.js +349 -0
  15. package/dist/components/ConnectionManager.js.map +1 -0
  16. package/dist/components/DLQHandler.d.ts +81 -0
  17. package/dist/components/DLQHandler.d.ts.map +1 -0
  18. package/dist/components/DLQHandler.js +228 -0
  19. package/dist/components/DLQHandler.js.map +1 -0
  20. package/dist/components/Logger.d.ts +77 -0
  21. package/dist/components/Logger.d.ts.map +1 -0
  22. package/dist/components/Logger.js +193 -0
  23. package/dist/components/Logger.js.map +1 -0
  24. package/dist/components/MessagePublisher.d.ts +49 -0
  25. package/dist/components/MessagePublisher.d.ts.map +1 -0
  26. package/dist/components/MessagePublisher.js +158 -0
  27. package/dist/components/MessagePublisher.js.map +1 -0
  28. package/dist/components/MessageSubscriber.d.ts +108 -0
  29. package/dist/components/MessageSubscriber.d.ts.map +1 -0
  30. package/dist/components/MessageSubscriber.js +503 -0
  31. package/dist/components/MessageSubscriber.js.map +1 -0
  32. package/dist/components/ResourceCreator.d.ts +89 -0
  33. package/dist/components/ResourceCreator.d.ts.map +1 -0
  34. package/dist/components/ResourceCreator.js +352 -0
  35. package/dist/components/ResourceCreator.js.map +1 -0
  36. package/dist/components/SdkRabbitmq.d.ts +103 -0
  37. package/dist/components/SdkRabbitmq.d.ts.map +1 -0
  38. package/dist/components/SdkRabbitmq.js +364 -0
  39. package/dist/components/SdkRabbitmq.js.map +1 -0
  40. package/dist/components/index.d.ts +9 -0
  41. package/dist/components/index.d.ts.map +1 -0
  42. package/dist/components/index.js +20 -0
  43. package/dist/components/index.js.map +1 -0
  44. package/dist/index.d.ts +5 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +27 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/interfaces/IConfiguration.d.ts +35 -0
  49. package/dist/interfaces/IConfiguration.d.ts.map +1 -0
  50. package/dist/interfaces/IConfiguration.js +3 -0
  51. package/dist/interfaces/IConfiguration.js.map +1 -0
  52. package/dist/interfaces/IConnection.d.ts +21 -0
  53. package/dist/interfaces/IConnection.d.ts.map +1 -0
  54. package/dist/interfaces/IConnection.js +3 -0
  55. package/dist/interfaces/IConnection.js.map +1 -0
  56. package/dist/interfaces/IDLQ.d.ts +12 -0
  57. package/dist/interfaces/IDLQ.d.ts.map +1 -0
  58. package/dist/interfaces/IDLQ.js +3 -0
  59. package/dist/interfaces/IDLQ.js.map +1 -0
  60. package/dist/interfaces/IErrors.d.ts +33 -0
  61. package/dist/interfaces/IErrors.d.ts.map +1 -0
  62. package/dist/interfaces/IErrors.js +56 -0
  63. package/dist/interfaces/IErrors.js.map +1 -0
  64. package/dist/interfaces/ILogger.d.ts +14 -0
  65. package/dist/interfaces/ILogger.d.ts.map +1 -0
  66. package/dist/interfaces/ILogger.js +3 -0
  67. package/dist/interfaces/ILogger.js.map +1 -0
  68. package/dist/interfaces/IMessage.d.ts +52 -0
  69. package/dist/interfaces/IMessage.d.ts.map +1 -0
  70. package/dist/interfaces/IMessage.js +3 -0
  71. package/dist/interfaces/IMessage.js.map +1 -0
  72. package/dist/interfaces/IResource.d.ts +31 -0
  73. package/dist/interfaces/IResource.d.ts.map +1 -0
  74. package/dist/interfaces/IResource.js +3 -0
  75. package/dist/interfaces/IResource.js.map +1 -0
  76. package/dist/interfaces/ISdkRabbitmq.d.ts +17 -0
  77. package/dist/interfaces/ISdkRabbitmq.d.ts.map +1 -0
  78. package/dist/interfaces/ISdkRabbitmq.js +3 -0
  79. package/dist/interfaces/ISdkRabbitmq.js.map +1 -0
  80. package/dist/interfaces/index.d.ts +9 -0
  81. package/dist/interfaces/index.d.ts.map +1 -0
  82. package/dist/interfaces/index.js +33 -0
  83. package/dist/interfaces/index.js.map +1 -0
  84. package/dist/utils/configSchema.d.ts +8 -0
  85. package/dist/utils/configSchema.d.ts.map +1 -0
  86. package/dist/utils/configSchema.js +51 -0
  87. package/dist/utils/configSchema.js.map +1 -0
  88. package/docker-compose.yml +24 -0
  89. package/example.ts +65 -0
  90. package/examples/README-dynamic-routing.md +155 -0
  91. package/examples/bind-unbind-example.js +56 -0
  92. package/examples/test-chatbot-exchange.ts +83 -0
  93. package/examples/test-dynamic-routing-flow.js +299 -0
  94. package/examples/test-dynamic-routing-flow.ts +355 -0
  95. package/examples/test-no-disconnect.ts +0 -0
  96. package/examples/test-raw-rabbitmq.js +68 -0
  97. package/examples/test-same-channel.ts +81 -0
  98. package/examples/test-schedule-flow.ts +713 -0
  99. package/examples/test-simple-greeting.ts +66 -0
  100. package/examples/test-simple-schedule.ts +76 -0
  101. package/examples/test-wildcard.ts +364 -0
  102. package/jest.config.js +17 -0
  103. package/package.json +42 -4
  104. package/preinstall.js +1 -0
  105. package/prompts/test-dynamic-routing-flow.md +46 -0
  106. package/run.js +4 -0
  107. package/scripts/run-dynamic-routing-test.ts +31 -0
  108. package/src/.gitkeep +1 -0
  109. package/src/components/.gitkeep +1 -0
  110. package/src/components/ConfigurationManager.ts +104 -0
  111. package/src/components/ConnectionManager.ts +357 -0
  112. package/src/components/DLQHandler.ts +271 -0
  113. package/src/components/Logger.ts +224 -0
  114. package/src/components/MessagePublisher.ts +180 -0
  115. package/src/components/MessageSubscriber.ts +597 -0
  116. package/src/components/ResourceCreator.ts +411 -0
  117. package/src/components/SdkRabbitmq.ts +443 -0
  118. package/src/components/__tests__/ConfigurationManager.test.ts +357 -0
  119. package/src/components/__tests__/ConnectionManager.test.ts +387 -0
  120. package/src/components/__tests__/DLQHandler.test.ts +399 -0
  121. package/src/components/__tests__/Logger.test.ts +354 -0
  122. package/src/components/__tests__/MessagePublisher.test.ts +337 -0
  123. package/src/components/__tests__/MessageSubscriber.test.ts +542 -0
  124. package/src/components/__tests__/ResourceCreator.test.ts +465 -0
  125. package/src/components/__tests__/SdkRabbitmq.integration.test.ts +433 -0
  126. package/src/components/index.ts +8 -0
  127. package/src/index.ts +11 -0
  128. package/src/interfaces/.gitkeep +1 -0
  129. package/src/interfaces/IConfiguration.ts +38 -0
  130. package/src/interfaces/IConnection.ts +27 -0
  131. package/src/interfaces/IDLQ.ts +13 -0
  132. package/src/interfaces/IErrors.ts +53 -0
  133. package/src/interfaces/ILogger.ts +16 -0
  134. package/src/interfaces/IMessage.ts +65 -0
  135. package/src/interfaces/IResource.ts +35 -0
  136. package/src/interfaces/ISdkRabbitmq.ts +26 -0
  137. package/src/interfaces/index.ts +23 -0
  138. package/src/utils/.gitkeep +1 -0
  139. package/src/utils/configSchema.ts +58 -0
  140. package/tsconfig.json +34 -0
@@ -0,0 +1,271 @@
1
+ import { IDLQHandler } from '../interfaces/IDLQ';
2
+ import { IConfiguration } from '../interfaces/IConfiguration';
3
+ import { ConnectionManager } from './ConnectionManager';
4
+ import { ResourceCreator } from './ResourceCreator';
5
+ import { Logger } from './Logger';
6
+
7
+ /**
8
+ * DLQHandler manages Dead Letter Queue functionality
9
+ * Handles DLQ setup, configuration, and failed message routing
10
+ */
11
+ export class DLQHandler implements IDLQHandler {
12
+ private config: IConfiguration;
13
+ private connectionManager: ConnectionManager;
14
+ private resourceCreator: ResourceCreator;
15
+ private dlqSetupCache = new Set<string>();
16
+ private logger: Logger;
17
+
18
+ constructor(
19
+ config: IConfiguration,
20
+ connectionManager: ConnectionManager,
21
+ resourceCreator: ResourceCreator
22
+ ) {
23
+ this.config = config;
24
+ this.connectionManager = connectionManager;
25
+ this.resourceCreator = resourceCreator;
26
+ this.logger = Logger.createComponentLogger('DLQHandler', config.logging);
27
+ }
28
+
29
+ /**
30
+ * Check if DLQ is enabled in configuration
31
+ * @returns boolean indicating if DLQ is active
32
+ */
33
+ public isEnabled(): boolean {
34
+ return this.config.dlq.active;
35
+ }
36
+
37
+ /**
38
+ * Set up DLQ for a given original queue
39
+ * Creates DLQ exchange and queue following naming conventions
40
+ * @param originalQueue Original queue name
41
+ * @returns Promise<string> DLQ queue name
42
+ */
43
+ public async setupDLQ(originalQueue: string): Promise<string> {
44
+ if (!originalQueue) {
45
+ throw new Error('Original queue name is required for DLQ setup');
46
+ }
47
+
48
+ if (!this.isEnabled()) {
49
+ throw new Error('DLQ is not enabled in configuration');
50
+ }
51
+
52
+ // Check cache to avoid duplicate setup
53
+ if (this.dlqSetupCache.has(originalQueue)) {
54
+ return this.getDLQQueueName(originalQueue);
55
+ }
56
+
57
+ const dlqExchangeName = this.getDLQExchangeName(originalQueue);
58
+ const dlqQueueName = this.getDLQQueueName(originalQueue);
59
+
60
+ try {
61
+ // Create DLQ exchange
62
+ await this.resourceCreator.ensureExchange(dlqExchangeName, 'direct', {
63
+ durable: true,
64
+ autoDelete: false
65
+ });
66
+
67
+ // Create DLQ queue with TTL if configured
68
+ const queueOptions: any = {
69
+ durable: true,
70
+ exclusive: false,
71
+ autoDelete: false
72
+ };
73
+
74
+ // Add TTL if configured
75
+ if (this.config.dlq.ttl && this.config.dlq.ttl > 0) {
76
+ queueOptions.arguments = {
77
+ 'x-message-ttl': this.config.dlq.ttl
78
+ };
79
+ }
80
+
81
+ await this.resourceCreator.ensureQueue(dlqQueueName, queueOptions);
82
+
83
+ // Bind DLQ queue to DLQ exchange
84
+ await this.resourceCreator.bindQueue(dlqQueueName, dlqExchangeName, originalQueue);
85
+
86
+ // Cache successful setup
87
+ this.dlqSetupCache.add(originalQueue);
88
+
89
+ this.logger.logOperation('setupDLQ', `DLQ setup completed`, {
90
+ originalQueue,
91
+ dlqExchange: dlqExchangeName,
92
+ dlqQueue: dlqQueueName,
93
+ ttl: this.config.dlq.ttl
94
+ });
95
+
96
+ return dlqQueueName;
97
+ } catch (error) {
98
+ this.logger.logError(`Failed to setup DLQ`, error as Error, { originalQueue });
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Handle a failed message by routing to DLQ
105
+ * @param message Failed message object
106
+ * @param originalQueue Original queue name where message failed
107
+ */
108
+ public async handleFailedMessage(message: any, originalQueue: string): Promise<void> {
109
+ if (!originalQueue) {
110
+ throw new Error('Original queue name is required for failed message handling');
111
+ }
112
+
113
+ if (!message) {
114
+ throw new Error('Message is required for failed message handling');
115
+ }
116
+
117
+ if (!this.isEnabled()) {
118
+ this.logger.info(`DLQ is disabled. Acknowledging failed message without reprocessing`, {
119
+ originalQueue
120
+ });
121
+ return;
122
+ }
123
+
124
+ try {
125
+ // Ensure DLQ is set up for this queue
126
+ await this.setupDLQ(originalQueue);
127
+
128
+ const dlqExchangeName = this.getDLQExchangeName(originalQueue);
129
+
130
+ // Prepare message for DLQ with original metadata
131
+ const dlqMessage = this.prepareDLQMessage(message, originalQueue);
132
+
133
+ // Route message to DLQ
134
+ await this.routeMessageToDLQ(dlqExchangeName, originalQueue, dlqMessage);
135
+
136
+ this.logger.logOperation('routeToDLQ', `Failed message routed to DLQ`, {
137
+ originalQueue,
138
+ dlqExchange: dlqExchangeName
139
+ });
140
+ } catch (error) {
141
+ this.logger.logError(`Failed to handle failed message`, error as Error, { originalQueue });
142
+ throw error;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Get DLQ exchange name following naming convention
148
+ * @param originalQueue Original queue name
149
+ * @returns DLQ exchange name (original-queue.dlq)
150
+ */
151
+ private getDLQExchangeName(originalQueue: string): string {
152
+ return `${originalQueue}.dlq`;
153
+ }
154
+
155
+ /**
156
+ * Get DLQ queue name following naming convention
157
+ * @param originalQueue Original queue name
158
+ * @returns DLQ queue name (original-queue.dlq)
159
+ */
160
+ private getDLQQueueName(originalQueue: string): string {
161
+ return `${originalQueue}.dlq`;
162
+ }
163
+
164
+ /**
165
+ * Prepare message for DLQ with original metadata
166
+ * @param message Original message
167
+ * @param originalQueue Original queue name
168
+ * @returns Enhanced message with DLQ metadata
169
+ */
170
+ private prepareDLQMessage(message: any, originalQueue: string): any {
171
+ const now = new Date();
172
+
173
+ // Extract original message content and metadata
174
+ const originalContent = message.content ? message.content : message;
175
+ const originalProperties = message.properties || {};
176
+ const originalFields = message.fields || {};
177
+
178
+ return {
179
+ originalMessage: originalContent,
180
+ dlqMetadata: {
181
+ originalQueue: originalQueue,
182
+ originalExchange: originalFields.exchange || '',
183
+ originalRoutingKey: originalFields.routingKey || '',
184
+ failedAt: now.toISOString(),
185
+ retryCount: this.getRetryCount(originalProperties) + 1,
186
+ maxRetries: this.config.dlq.maxRetries || 3,
187
+ retryDelay: this.config.dlq.retryDelay || 5000
188
+ },
189
+ originalProperties: originalProperties,
190
+ originalFields: originalFields
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Route message to DLQ exchange
196
+ * @param dlqExchange DLQ exchange name
197
+ * @param routingKey Routing key (original queue name)
198
+ * @param message Message to route
199
+ */
200
+ private async routeMessageToDLQ(dlqExchange: string, routingKey: string, message: any): Promise<void> {
201
+ return this.connectionManager.executeOperation(async () => {
202
+ const connection = this.connectionManager.getConnection();
203
+ if (!connection) {
204
+ throw new Error('No active connection to RabbitMQ');
205
+ }
206
+
207
+ try {
208
+ const channel = await (connection as any).createChannel();
209
+
210
+ // Publish message to DLQ exchange
211
+ const messageBuffer = Buffer.from(JSON.stringify(message));
212
+ const publishOptions = {
213
+ persistent: true,
214
+ timestamp: Date.now(),
215
+ headers: {
216
+ 'x-dlq-routed': true,
217
+ 'x-original-queue': routingKey
218
+ }
219
+ };
220
+
221
+ const published = channel.publish(dlqExchange, routingKey, messageBuffer, publishOptions);
222
+
223
+ if (!published) {
224
+ throw new Error('Failed to publish message to DLQ - channel buffer full');
225
+ }
226
+
227
+ await channel.close();
228
+ } catch (error) {
229
+ const errorMessage = `Failed to route message to DLQ exchange '${dlqExchange}': ${error instanceof Error ? error.message : 'Unknown error'}`;
230
+ throw new Error(errorMessage);
231
+ }
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Get retry count from message properties
237
+ * @param properties Message properties
238
+ * @returns Current retry count
239
+ */
240
+ private getRetryCount(properties: any): number {
241
+ if (!properties || !properties.headers) {
242
+ return 0;
243
+ }
244
+
245
+ return properties.headers['x-retry-count'] || 0;
246
+ }
247
+
248
+ /**
249
+ * Clear DLQ setup cache (useful for testing)
250
+ */
251
+ public clearCache(): void {
252
+ this.dlqSetupCache.clear();
253
+ this.logger.info('DLQHandler cache cleared');
254
+ }
255
+
256
+ /**
257
+ * Get cache statistics for monitoring
258
+ */
259
+ public getCacheStats(): { setupCache: number } {
260
+ return {
261
+ setupCache: this.dlqSetupCache.size
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Get DLQ configuration
267
+ */
268
+ public getDLQConfig(): IConfiguration['dlq'] {
269
+ return this.config.dlq;
270
+ }
271
+ }
@@ -0,0 +1,224 @@
1
+ import { ILogger } from '../interfaces/ILogger';
2
+ import { IConfiguration } from '../interfaces/IConfiguration';
3
+
4
+ /**
5
+ * Log levels with numeric values for comparison
6
+ */
7
+ export enum LogLevel {
8
+ ERROR = 0,
9
+ WARN = 1,
10
+ INFO = 2,
11
+ DEBUG = 3
12
+ }
13
+
14
+ /**
15
+ * Log entry structure for JSON formatting
16
+ */
17
+ interface LogEntry {
18
+ timestamp: string;
19
+ level: string;
20
+ message: string;
21
+ context?: any;
22
+ component?: string;
23
+ operation?: string;
24
+ }
25
+
26
+ /**
27
+ * Logger class that provides structured logging with configurable levels and formats
28
+ */
29
+ export class Logger implements ILogger {
30
+ private static instance: Logger | null = null;
31
+ private logLevel: LogLevel;
32
+ private format: 'json' | 'text';
33
+ private component?: string;
34
+
35
+ /**
36
+ * Private constructor to enforce singleton pattern
37
+ */
38
+ private constructor(config?: IConfiguration['logging'], component?: string) {
39
+ this.logLevel = this.parseLogLevel(config?.level || 'info');
40
+ this.format = config?.format || 'text';
41
+ if (component) {
42
+ this.component = component;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Get singleton instance of Logger
48
+ */
49
+ public static getInstance(config?: IConfiguration['logging'], component?: string): Logger {
50
+ if (!Logger.instance) {
51
+ Logger.instance = new Logger(config, component);
52
+ }
53
+ return Logger.instance;
54
+ }
55
+
56
+ /**
57
+ * Create a new logger instance for a specific component
58
+ */
59
+ public static createComponentLogger(component: string, config?: IConfiguration['logging']): Logger {
60
+ return new Logger(config, component);
61
+ }
62
+
63
+ /**
64
+ * Log error message with optional context
65
+ */
66
+ public error(message: string, context?: any): void {
67
+ this.log(LogLevel.ERROR, 'ERROR', message, context);
68
+ }
69
+
70
+ /**
71
+ * Log warning message with optional context
72
+ */
73
+ public warn(message: string, context?: any): void {
74
+ this.log(LogLevel.WARN, 'WARN', message, context);
75
+ }
76
+
77
+ /**
78
+ * Log info message with optional context
79
+ */
80
+ public info(message: string, context?: any): void {
81
+ this.log(LogLevel.INFO, 'INFO', message, context);
82
+ }
83
+
84
+ /**
85
+ * Log debug message with optional context
86
+ */
87
+ public debug(message: string, context?: any): void {
88
+ this.log(LogLevel.DEBUG, 'DEBUG', message, context);
89
+ }
90
+
91
+ /**
92
+ * Log operation with context (for operation tracking)
93
+ */
94
+ public logOperation(operation: string, message: string, context?: any): void {
95
+ const operationContext = {
96
+ operation,
97
+ ...context
98
+ };
99
+ this.info(message, operationContext);
100
+ }
101
+
102
+ /**
103
+ * Log error with stack trace
104
+ */
105
+ public logError(message: string, error: Error, context?: any): void {
106
+ const errorContext = {
107
+ error: {
108
+ name: error.name,
109
+ message: error.message,
110
+ stack: error.stack
111
+ },
112
+ ...context
113
+ };
114
+ this.error(message, errorContext);
115
+ }
116
+
117
+ /**
118
+ * Log connection state changes
119
+ */
120
+ public logConnectionState(state: string, context?: any): void {
121
+ const connectionContext = {
122
+ connectionState: state,
123
+ ...context
124
+ };
125
+ this.info(`Connection state changed to: ${state}`, connectionContext);
126
+ }
127
+
128
+ /**
129
+ * Internal logging method
130
+ */
131
+ private log(level: LogLevel, levelName: string, message: string, context?: any): void {
132
+ // Check if this log level should be output
133
+ if (level > this.logLevel) {
134
+ return;
135
+ }
136
+
137
+ const timestamp = new Date().toISOString();
138
+
139
+ if (this.format === 'json') {
140
+ const logEntry: LogEntry = {
141
+ timestamp,
142
+ level: levelName,
143
+ message
144
+ };
145
+
146
+ if (context) {
147
+ try {
148
+ // Test if context can be serialized
149
+ JSON.stringify(context);
150
+ logEntry.context = context;
151
+ } catch (error) {
152
+ logEntry.context = '[Circular Reference]';
153
+ }
154
+ }
155
+
156
+ if (this.component) {
157
+ logEntry.component = this.component;
158
+ }
159
+
160
+ console.log(JSON.stringify(logEntry));
161
+ } else {
162
+ // Text format
163
+ const componentPrefix = this.component ? `[${this.component}] ` : '';
164
+ let contextSuffix = '';
165
+ if (context) {
166
+ try {
167
+ contextSuffix = ` | Context: ${JSON.stringify(context)}`;
168
+ } catch (error) {
169
+ contextSuffix = ` | Context: [Circular Reference]`;
170
+ }
171
+ }
172
+
173
+ const logMessage = `${timestamp} [${levelName}] ${componentPrefix}${message}${contextSuffix}`;
174
+
175
+ // Use appropriate console method based on level
176
+ switch (level) {
177
+ case LogLevel.ERROR:
178
+ console.error(logMessage);
179
+ break;
180
+ case LogLevel.WARN:
181
+ console.warn(logMessage);
182
+ break;
183
+ case LogLevel.DEBUG:
184
+ console.debug(logMessage);
185
+ break;
186
+ default:
187
+ console.log(logMessage);
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Parse log level string to enum value
194
+ */
195
+ private parseLogLevel(level: string): LogLevel {
196
+ switch (level.toLowerCase()) {
197
+ case 'error':
198
+ return LogLevel.ERROR;
199
+ case 'warn':
200
+ return LogLevel.WARN;
201
+ case 'info':
202
+ return LogLevel.INFO;
203
+ case 'debug':
204
+ return LogLevel.DEBUG;
205
+ default:
206
+ return LogLevel.INFO;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Update logger configuration
212
+ */
213
+ public updateConfig(config: IConfiguration['logging']): void {
214
+ this.logLevel = this.parseLogLevel(config?.level || 'info');
215
+ this.format = config?.format || 'text';
216
+ }
217
+
218
+ /**
219
+ * Reset singleton instance (for testing purposes)
220
+ */
221
+ public static resetInstance(): void {
222
+ Logger.instance = null;
223
+ }
224
+ }
@@ -0,0 +1,180 @@
1
+ // amqplib types are used via ConnectionManager
2
+ import { IMessagePublisher } from '../interfaces/IMessage';
3
+ import { PublishError } from '../interfaces/IErrors';
4
+ import { ConnectionManager } from './ConnectionManager';
5
+ import { ResourceCreator } from './ResourceCreator';
6
+ import { Logger } from './Logger';
7
+ import { IConfiguration } from '../interfaces/IConfiguration';
8
+
9
+ /**
10
+ * MessagePublisher handles message publishing to RabbitMQ exchanges
11
+ * Implements parameter validation, JSON serialization, and automatic exchange creation
12
+ */
13
+ export class MessagePublisher implements IMessagePublisher {
14
+ private connectionManager: ConnectionManager;
15
+ private resourceCreator: ResourceCreator;
16
+ private logger: Logger;
17
+
18
+ constructor(connectionManager: ConnectionManager, resourceCreator: ResourceCreator, config?: IConfiguration['logging']) {
19
+ this.connectionManager = connectionManager;
20
+ this.resourceCreator = resourceCreator;
21
+ this.logger = Logger.createComponentLogger('MessagePublisher', config);
22
+ }
23
+
24
+ /**
25
+ * Publish a message to an exchange
26
+ * @param exchange Exchange name (required)
27
+ * @param routingKey Routing key for message routing (required)
28
+ * @param payload Message payload to be JSON serialized (required)
29
+ * @returns Promise<boolean> - true for success, false for failure
30
+ */
31
+ public async publish(exchange: string, routingKey: string, payload: any): Promise<boolean> {
32
+ // Parameter validation - all parameters are required
33
+ this.validatePublishParameters(exchange, routingKey, payload);
34
+
35
+ try {
36
+ // JSON serialization for all payloads
37
+ const serializedPayload = this.serializePayload(payload);
38
+
39
+ // Automatic exchange creation before publishing
40
+ await this.ensureExchangeExists(exchange);
41
+
42
+ return await this.connectionManager.executeOperation(async () => {
43
+ const connection = this.connectionManager.getConnection();
44
+ if (!connection) {
45
+ throw new PublishError('No active connection to RabbitMQ', {
46
+ exchange,
47
+ routingKey,
48
+ payload: typeof payload
49
+ });
50
+ }
51
+
52
+ const channel = await (connection as any).createChannel();
53
+
54
+ try {
55
+ // Publish message with JSON content type
56
+ const published = channel.publish(
57
+ exchange,
58
+ routingKey,
59
+ Buffer.from(serializedPayload),
60
+ {
61
+ contentType: 'application/json',
62
+ timestamp: Date.now(),
63
+ persistent: true // Make messages persistent by default
64
+ }
65
+ );
66
+
67
+ await channel.close();
68
+
69
+ // Log successful publish
70
+ this.logger.logOperation('publish', `Message published successfully`, {
71
+ exchange,
72
+ routingKey,
73
+ payloadType: typeof payload,
74
+ payloadSize: serializedPayload.length
75
+ });
76
+
77
+ return published;
78
+ } catch (error) {
79
+ await channel.close();
80
+ throw error;
81
+ }
82
+ });
83
+ } catch (error) {
84
+ // Detailed error logging with context
85
+ this.logPublishError(error, exchange, routingKey, payload);
86
+
87
+ // Return false for failure as per requirements
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Validate publish method parameters
94
+ * @param exchange Exchange name
95
+ * @param routingKey Routing key
96
+ * @param payload Message payload
97
+ */
98
+ private validatePublishParameters(exchange: string, routingKey: string, payload: any): void {
99
+ if (!exchange || typeof exchange !== 'string') {
100
+ throw new PublishError('Exchange parameter is required and must be a non-empty string', {
101
+ exchange,
102
+ routingKey,
103
+ payload: typeof payload
104
+ });
105
+ }
106
+
107
+ if (!routingKey || typeof routingKey !== 'string') {
108
+ throw new PublishError('RoutingKey parameter is required and must be a non-empty string', {
109
+ exchange,
110
+ routingKey,
111
+ payload: typeof payload
112
+ });
113
+ }
114
+
115
+ if (payload === undefined || payload === null) {
116
+ throw new PublishError('Payload parameter is required and cannot be null or undefined', {
117
+ exchange,
118
+ routingKey,
119
+ payload
120
+ });
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Serialize payload to JSON string
126
+ * @param payload Payload to serialize
127
+ * @returns JSON string representation
128
+ */
129
+ private serializePayload(payload: any): string {
130
+ try {
131
+ return JSON.stringify(payload);
132
+ } catch (error) {
133
+ throw new PublishError('Failed to serialize payload to JSON', {
134
+ payload: typeof payload,
135
+ error: error instanceof Error ? error.message : 'Unknown serialization error'
136
+ });
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Ensure exchange exists before publishing
142
+ * @param exchange Exchange name
143
+ */
144
+ private async ensureExchangeExists(exchange: string): Promise<void> {
145
+ try {
146
+ // Use ResourceCreator to automatically create exchange if it doesn't exist
147
+ // Always use topic type by default
148
+ await this.resourceCreator.ensureExchange(exchange, 'topic');
149
+ } catch (error) {
150
+ throw new PublishError(`Failed to ensure exchange '${exchange}' exists`, {
151
+ exchange,
152
+ error: error instanceof Error ? error.message : 'Unknown error'
153
+ });
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Log publish errors with detailed context
159
+ * @param error The error that occurred
160
+ * @param exchange Exchange name
161
+ * @param routingKey Routing key
162
+ * @param payload Original payload
163
+ */
164
+ private logPublishError(error: unknown, exchange: string, routingKey: string, payload: any): void {
165
+ let payloadSize = 0;
166
+ try {
167
+ payloadSize = JSON.stringify(payload).length;
168
+ } catch {
169
+ payloadSize = -1; // Indicate serialization failed
170
+ }
171
+
172
+ this.logger.logError('Message publish failed', error as Error, {
173
+ exchange,
174
+ routingKey,
175
+ payloadType: typeof payload,
176
+ payloadSize,
177
+ operation: 'publish'
178
+ });
179
+ }
180
+ }