core-services-sdk 1.3.7 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +3 -2
  2. package/src/fastify/error-codes.js +11 -0
  3. package/src/http/HttpError.js +84 -10
  4. package/src/http/http.js +41 -31
  5. package/src/http/index.js +4 -0
  6. package/src/http/responseType.js +10 -0
  7. package/src/ids/index.js +2 -0
  8. package/src/index.js +7 -21
  9. package/src/mailer/transport.factory.js +19 -6
  10. package/src/mongodb/initialize-mongodb.js +9 -7
  11. package/src/rabbit-mq/index.js +1 -186
  12. package/src/rabbit-mq/rabbit-mq.js +189 -0
  13. package/src/templates/index.js +1 -0
  14. package/tests/fastify/error-handler.unit.test.js +39 -0
  15. package/tests/{with-error-handling.test.js → fastify/error-handlers/with-error-handling.test.js} +4 -3
  16. package/tests/http/HttpError.unit.test.js +112 -0
  17. package/tests/http/http-method.unit.test.js +29 -0
  18. package/tests/http/http.unit.test.js +167 -0
  19. package/tests/http/responseType.unit.test.js +45 -0
  20. package/tests/ids/prefixes.unit.test.js +1 -0
  21. package/tests/mailer/mailer.integration.test.js +95 -0
  22. package/tests/{mailer.unit.test.js → mailer/mailer.unit.test.js} +7 -11
  23. package/tests/mailer/transport.factory.unit.test.js +204 -0
  24. package/tests/mongodb/connect.unit.test.js +60 -0
  25. package/tests/mongodb/initialize-mongodb.unit.test.js +98 -0
  26. package/tests/mongodb/validate-mongo-uri.unit.test.js +52 -0
  27. package/tests/{rabbit-mq.test.js → rabbit-mq/rabbit-mq.test.js} +3 -2
  28. package/tests/{template-loader.integration.test.js → templates/template-loader.integration.test.js} +1 -1
  29. package/tests/{template-loader.unit.test.js → templates/template-loader.unit.test.js} +1 -1
  30. package/vitest.config.js +3 -0
  31. package/index.js +0 -3
  32. package/tests/HttpError.test.js +0 -80
  33. package/tests/core-util.js +0 -24
  34. package/tests/mailer.integration.test.js +0 -46
  35. package/tests/mongodb.test.js +0 -70
  36. package/tests/transport.factory.unit.test.js +0 -128
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -19,8 +19,9 @@
19
19
  "homepage": "https://github.com/haim-rubin/core-services-sdk#readme",
20
20
  "description": "",
21
21
  "dependencies": {
22
+ "@aws-sdk/client-ses": "^3.862.0",
23
+ "@aws-sdk/credential-provider-node": "^3.862.0",
22
24
  "amqplib": "^0.10.8",
23
- "aws-sdk": "^2.1692.0",
24
25
  "crypto": "^1.0.1",
25
26
  "dot": "^1.1.3",
26
27
  "fastify": "^5.4.0",
@@ -1,5 +1,16 @@
1
1
  import httpStatus from 'http-status'
2
2
 
3
+ /**
4
+ * A reusable generic error object representing a server-side failure (HTTP 500).
5
+ * Useful as a fallback error descriptor for unhandled or unexpected failures.
6
+ *
7
+ * @typedef {Object} GeneralError
8
+ * @property {number} httpStatusCode - The HTTP status code (500).
9
+ * @property {string} httpStatusText - The human-readable status text ("Internal Server Error").
10
+ * @property {string} code - An application-specific error code in the format "GENERAL.<StatusText>".
11
+ */
12
+
13
+ /** @type {GeneralError} */
3
14
  export const GENERAL_ERROR = {
4
15
  httpStatusCode: httpStatus.INTERNAL_SERVER_ERROR,
5
16
  httpStatusText: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
@@ -1,24 +1,46 @@
1
1
  import httpStatus from 'http-status'
2
2
 
3
+ /**
4
+ * Represents a custom HTTP error with optional status code, status text, error code, and extra metadata.
5
+ * Useful for consistent error handling across services.
6
+ */
3
7
  export class HttpError extends Error {
4
- /** @type {string|undefined} */
8
+ /**
9
+ * @type {string | number | undefined}
10
+ * A short application-specific error code (e.g., "INVALID_INPUT" or a numeric code).
11
+ */
5
12
  code
6
13
 
7
- /** @type {number|undefined} */
14
+ /**
15
+ * @type {number | undefined}
16
+ * HTTP status code associated with the error (e.g., 400, 500).
17
+ */
8
18
  httpStatusCode
9
19
 
10
- /** @type {string|undefined} */
20
+ /**
21
+ * @type {string | undefined}
22
+ * Human-readable HTTP status text (e.g., "Bad Request").
23
+ */
11
24
  httpStatusText
12
25
 
13
26
  /**
14
- * @param {object} [error]
15
- * @param {string} [error.code]
16
- * @param {string} [error.message]
17
- * @param {number} [error.httpStatusCode]
18
- * @param {string} [error.httpStatusText]
27
+ * @type {object | undefined}
28
+ * Optional metadata for debugging/logging (e.g., request ID, user ID, retryAfter).
29
+ */
30
+ extendInfo
31
+
32
+ /**
33
+ * Creates an instance of HttpError.
34
+ *
35
+ * @param {Object} [error] - Optional error object.
36
+ * @param {string | number} [error.code] - Application-specific error code.
37
+ * @param {string} [error.message] - Custom error message.
38
+ * @param {number} [error.httpStatusCode] - HTTP status code (e.g., 404, 500).
39
+ * @param {string} [error.httpStatusText] - Optional human-readable HTTP status text.
40
+ * @param {object} [error.extendInfo] - Optional extended metadata for diagnostics.
19
41
  */
20
42
  constructor(error = {}) {
21
- const { code, message, httpStatusCode, httpStatusText } = error
43
+ const { code, message, httpStatusCode, httpStatusText, extendInfo } = error
22
44
 
23
45
  super(
24
46
  message ||
@@ -31,16 +53,29 @@ export class HttpError extends Error {
31
53
  this.httpStatusCode = httpStatusCode
32
54
  this.httpStatusText =
33
55
  httpStatusText || (httpStatusCode && httpStatus[httpStatusCode])
56
+ this.extendInfo = extendInfo
34
57
 
35
58
  if (typeof Error.captureStackTrace === 'function') {
36
59
  Error.captureStackTrace(this, this.constructor)
37
60
  }
38
61
  }
39
62
 
63
+ /**
64
+ * Checks if a given object is an instance of `HttpError`.
65
+ *
66
+ * @param {*} instance - The object to check.
67
+ * @returns {boolean} True if `instance` is an instance of `HttpError`.
68
+ */
40
69
  static isInstanceOf(instance) {
41
70
  return instance instanceof HttpError
42
71
  }
43
72
 
73
+ /**
74
+ * Custom implementation for `instanceof` checks.
75
+ *
76
+ * @param {*} instance - The object to check.
77
+ * @returns {boolean} True if it has HttpError-like structure.
78
+ */
44
79
  static [Symbol.hasInstance](instance) {
45
80
  return (
46
81
  instance &&
@@ -51,18 +86,57 @@ export class HttpError extends Error {
51
86
  )
52
87
  }
53
88
 
89
+ /**
90
+ * Converts the error to a plain object (useful for logging or sending as JSON).
91
+ *
92
+ * @returns {{
93
+ * code: string | number | undefined,
94
+ * message: string,
95
+ * httpStatusCode: number | undefined,
96
+ * httpStatusText: string | undefined,
97
+ * extendInfo?: object
98
+ * }}
99
+ */
54
100
  toJSON() {
55
101
  return {
56
102
  code: this.code,
57
103
  message: this.message,
58
104
  httpStatusCode: this.httpStatusCode,
59
105
  httpStatusText: this.httpStatusText,
106
+ ...(this.extendInfo ? { extendInfo: this.extendInfo } : {}),
60
107
  }
61
108
  }
62
109
 
110
+ /**
111
+ * Checks if the error is an instance of `HttpError` or has similar shape.
112
+ *
113
+ * @param {object} error
114
+ * @returns {error is HttpError}
115
+ */
116
+ static isHttpError(error) {
117
+ return (
118
+ error instanceof HttpError ||
119
+ (error &&
120
+ typeof error === 'object' &&
121
+ 'httpStatusCode' in error &&
122
+ 'httpStatusText' in error &&
123
+ 'toJSON' in error)
124
+ )
125
+ }
126
+
127
+ /**
128
+ * Creates an HttpError from a generic Error instance or returns it if already an HttpError.
129
+ *
130
+ * @param {Error | HttpError} error
131
+ * @returns {HttpError}
132
+ */
63
133
  static FromError(error) {
134
+ if (HttpError.isHttpError(error)) {
135
+ return error
136
+ }
137
+
64
138
  const httpError = new HttpError({
65
- code: error.code || 'UNHANDLED_ERROR',
139
+ code: 'UNHANDLED_ERROR',
66
140
  message: error.message || 'An unexpected error occurred',
67
141
  httpStatusCode: httpStatus.INTERNAL_SERVER_ERROR,
68
142
  httpStatusText: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
package/src/http/http.js CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * A lightweight HTTP client wrapper around `node-fetch` supporting JSON, XML, and plain text responses.
3
+ * Provides simplified helper methods for GET, POST, PUT, PATCH, and DELETE with automatic error handling.
4
+ *
5
+ * @module http
6
+ */
7
+
1
8
  import fetch from 'node-fetch'
2
9
  import httpStatus from 'http-status'
3
10
  import { parseStringPromise } from 'xml2js'
@@ -13,40 +20,36 @@ const JSON_HEADER = {
13
20
  /**
14
21
  * Checks if the HTTP status is considered successful (2xx).
15
22
  *
16
- * @param {Response} res - The fetch response object.
17
- * @returns {boolean} `true` if status code is between 200-299.
23
+ * @param {import('node-fetch').Response} response
24
+ * @returns {boolean}
18
25
  */
19
26
  const isOkStatus = ({ status }) =>
20
27
  status >= httpStatus.OK && status < httpStatus.MULTIPLE_CHOICES
21
28
 
22
29
  /**
23
- * Verifies response status and throws a structured HttpError if not OK.
24
- *
25
- * @param {Response} res - The fetch response object.
26
- * @throws {HttpError} When the response status is not OK.
27
- * @returns {Response} The response if status is OK.
30
+ * @param {import('node-fetch').Response} response
31
+ * @returns {Promise<import('node-fetch').Response>}
28
32
  */
29
- const checkStatus = async (res) => {
30
- if (!isOkStatus(res)) {
31
- const text = await res.text()
33
+ const checkStatus = async (response) => {
34
+ if (!isOkStatus(response)) {
35
+ const text = await response.text()
32
36
  const info = tryConvertJsonResponse(text)
33
- const { status, statusText } = res
37
+ const { status, statusText } = response
34
38
 
35
39
  throw new HttpError({
36
40
  code: status,
37
41
  httpStatusCode: status,
38
42
  httpStatusText: statusText,
39
- details: info,
40
43
  })
41
44
  }
42
- return res
45
+ return response
43
46
  }
44
47
 
45
48
  /**
46
49
  * Reads the raw text from a fetch response.
47
50
  *
48
- * @param {Response} response - The fetch response.
49
- * @returns {Promise<string>} The text body.
51
+ * @param {import('node-fetch').Response} response
52
+ * @returns {Promise<string>} The plain text body.
50
53
  */
51
54
  const getTextResponse = async (response) => {
52
55
  return await response.text()
@@ -71,8 +74,8 @@ const tryConvertJsonResponse = (responseText) => {
71
74
  /**
72
75
  * Attempts to extract a JSON object from a fetch response.
73
76
  *
74
- * @param {Response} response - The fetch response.
75
- * @returns {Promise<Object|string>} Parsed object or raw string on failure.
77
+ * @param {import('node-fetch').Response} response
78
+ * @returns {Promise<Object|string>} Parsed JSON or raw string if parsing fails.
76
79
  */
77
80
  const tryGetJsonResponse = async (response) => {
78
81
  let jsonText
@@ -88,8 +91,8 @@ const tryGetJsonResponse = async (response) => {
88
91
  /**
89
92
  * Attempts to extract an XML object from a fetch response.
90
93
  *
91
- * @param {Response} response - The fetch response.
92
- * @returns {Promise<Object|string>} Parsed XML object or raw string.
94
+ * @param {import('node-fetch').Response} response
95
+ * @returns {Promise<Object|string>} Parsed XML object or raw string if parsing fails.
93
96
  */
94
97
  const tryGetXmlResponse = async (response) => {
95
98
  let xmlText
@@ -105,8 +108,8 @@ const tryGetXmlResponse = async (response) => {
105
108
  /**
106
109
  * Extracts and parses the fetch response body based on expected type.
107
110
  *
108
- * @param {Response} response - The fetch response.
109
- * @param {string} responseType - The expected response type ('json', 'xml', 'text').
111
+ * @param {import('node-fetch').Response} response
112
+ * @param {string} responseType - Expected type: 'json', 'xml', or 'text'.
110
113
  * @returns {Promise<any>} The parsed response payload.
111
114
  */
112
115
  const getResponsePayload = async (response, responseType) => {
@@ -125,10 +128,10 @@ const getResponsePayload = async (response, responseType) => {
125
128
  * Sends an HTTP GET request.
126
129
  *
127
130
  * @param {Object} params
128
- * @param {string} params.url - The target URL.
129
- * @param {Object} [params.headers] - Optional custom headers.
130
- * @param {string} [params.credentials='include'] - Credential mode.
131
- * @param {string} [params.expectedType='json'] - Response type.
131
+ * @param {string} params.url - Target URL.
132
+ * @param {Object} [params.headers] - Optional request headers.
133
+ * @param {string} [params.credentials='include'] - Credential policy.
134
+ * @param {string} [params.expectedType='json'] - Expected response format.
132
135
  * @returns {Promise<any>} Parsed response data.
133
136
  */
134
137
  export const get = async ({
@@ -137,11 +140,13 @@ export const get = async ({
137
140
  credentials = 'include',
138
141
  expectedType = ResponseType.json,
139
142
  }) => {
143
+ /** @type {import('node-fetch').Response} */
140
144
  const response = await fetch(url, {
141
145
  method: HTTP_METHODS.GET,
142
146
  headers: { ...JSON_HEADER, ...headers },
143
147
  ...(credentials ? { credentials } : {}),
144
148
  })
149
+
145
150
  await checkStatus(response)
146
151
  return await getResponsePayload(response, expectedType)
147
152
  }
@@ -150,11 +155,11 @@ export const get = async ({
150
155
  * Sends an HTTP POST request.
151
156
  *
152
157
  * @param {Object} params
153
- * @param {string} params.url - The target URL.
154
- * @param {Object} params.body - Body data to send.
155
- * @param {Object} [params.headers] - Optional custom headers.
156
- * @param {string} [params.credentials='include'] - Credential mode.
157
- * @param {string} [params.expectedType='json'] - Response type.
158
+ * @param {string} params.url - Target URL.
159
+ * @param {Object} params.body - Request body (will be JSON.stringify-ed).
160
+ * @param {Object} [params.headers] - Optional request headers.
161
+ * @param {string} [params.credentials='include'] - Credential policy.
162
+ * @param {string} [params.expectedType='json'] - Expected response format.
158
163
  * @returns {Promise<any>} Parsed response data.
159
164
  */
160
165
  export const post = async ({
@@ -177,7 +182,12 @@ export const post = async ({
177
182
  /**
178
183
  * Sends an HTTP PUT request.
179
184
  *
180
- * @param {Object} params - Same as `post`.
185
+ * @param {Object} params
186
+ * @param {string} params.url
187
+ * @param {Object} params.body
188
+ * @param {Object} [params.headers]
189
+ * @param {string} [params.credentials='include']
190
+ * @param {string} [params.expectedType='json']
181
191
  * @returns {Promise<any>} Parsed response data.
182
192
  */
183
193
  export const put = async ({
@@ -0,0 +1,4 @@
1
+ export * from './http.js'
2
+ export * from './HttpError.js'
3
+ export * from './http-method.js'
4
+ export * from './responseType.js'
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Enum representing supported response types for HTTP client parsing.
3
+ *
4
+ * @readonly
5
+ * @enum {string}
6
+ * @property {string} xml - XML response (parsed using xml2js).
7
+ * @property {string} json - JSON response (parsed via JSON.parse).
8
+ * @property {string} text - Plain text response.
9
+ * @property {string} file - Binary file or blob (not automatically parsed).
10
+ */
1
11
  export const ResponseType = Object.freeze({
2
12
  xml: 'xml',
3
13
  json: 'json',
@@ -0,0 +1,2 @@
1
+ export * from './prefixes.js'
2
+ export * from './generators.js'
package/src/index.js CHANGED
@@ -1,24 +1,10 @@
1
- export * from './ids/prefixes.js'
1
+ export * from './core/index.js'
2
+ export * from './crypto/index.js'
2
3
  export * from './fastify/index.js'
4
+ export * from './http/index.js'
5
+ export * from './ids/index.js'
3
6
  export * from './mongodb/index.js'
4
- export * from './crypto/crypto.js'
5
- export * from './ids/generators.js'
7
+ export * from './logger/index.js'
8
+ export * from './mailer/index.js'
6
9
  export * from './rabbit-mq/index.js'
7
- export * from './core/regex-utils.js'
8
- export * from './logger/get-logger.js'
9
- export * as http from './http/http.js'
10
- export * from './http/responseType.js'
11
- export * from './crypto/encryption.js'
12
- export * from './core/otp-generators.js'
13
- export * from './core/sanitize-objects.js'
14
- export * from './core/normalize-to-array.js'
15
- export { initMailer } from './mailer/index.js'
16
- export * from './core/combine-unique-arrays.js'
17
- export { HttpError } from './http/HttpError.js'
18
- export { Mailer } from './mailer/mailer.service.js'
19
- export { TransportFactory } from './mailer/transport.factory.js'
20
- export {
21
- isItFile,
22
- loadTemplates,
23
- getTemplateContent,
24
- } from './templates/template-loader.js'
10
+ export * from './templates/index.js'
@@ -1,6 +1,7 @@
1
- import aws from 'aws-sdk'
2
1
  import nodemailer from 'nodemailer'
3
2
  import sgTransport from 'nodemailer-sendgrid-transport'
3
+ import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'
4
+ import { defaultProvider } from '@aws-sdk/credential-provider-node'
4
5
 
5
6
  /**
6
7
  * Factory for creating Nodemailer transporters based on configuration.
@@ -54,13 +55,25 @@ export class TransportFactory {
54
55
  }),
55
56
  )
56
57
 
57
- case 'ses':
58
- const ses = new aws.SES({
59
- accessKeyId: config.accessKeyId,
60
- secretAccessKey: config.secretAccessKey,
58
+ case 'ses': {
59
+ const sesClient = new SESClient({
61
60
  region: config.region,
61
+ credentials:
62
+ config.accessKeyId && config.secretAccessKey
63
+ ? {
64
+ accessKeyId: config.accessKeyId,
65
+ secretAccessKey: config.secretAccessKey,
66
+ }
67
+ : defaultProvider(),
62
68
  })
63
- return nodemailer.createTransport({ SES: { ses, aws } })
69
+
70
+ return nodemailer.createTransport({
71
+ SES: {
72
+ ses: sesClient,
73
+ aws: { SendRawEmailCommand },
74
+ },
75
+ })
76
+ }
64
77
 
65
78
  default:
66
79
  throw new Error(`Unsupported transport type: ${config.type}`)
@@ -2,15 +2,18 @@
2
2
  import { mongoConnect } from './connect.js'
3
3
 
4
4
  /**
5
- * Initializes MongoDB collections and provides a transaction wrapper and read-only client accessor.
5
+ * Initializes MongoDB collections and provides:
6
+ * - Named collection references
7
+ * - A transaction wrapper (`withTransaction`)
8
+ * - A read-only MongoDB client accessor
6
9
  *
7
10
  * @param {Object} options
8
11
  * @param {{ uri: string, options: { dbName: string } }} options.config - MongoDB connection config
9
- * @param {Record<string, string>} options.collectionNames - Map of collection keys to MongoDB collection names
12
+ * @param {Record<string, string>} options.collectionNames - Map of keys to actual MongoDB collection names
10
13
  *
11
14
  * @returns {Promise<
12
15
  * Record<string, import('mongodb').Collection> & {
13
- * withTransaction: (action: ({ session: import('mongodb').ClientSession }) => Promise<void>) => Promise<void>,
16
+ * withTransaction: <T>(action: ({ session: import('mongodb').ClientSession }) => Promise<T>) => Promise<T>,
14
17
  * readonly client: import('mongodb').MongoClient
15
18
  * }
16
19
  * >}
@@ -32,7 +35,7 @@ import { mongoConnect } from './connect.js'
32
35
  * await logs.insertOne({ event: 'UserCreated', user: 'Alice' }, { session });
33
36
  * });
34
37
  *
35
- * await client.close(); // Close connection manually
38
+ * await client.close(); // Close connection manually when done
36
39
  */
37
40
  export const initializeMongoDb = async ({ config, collectionNames = {} }) => {
38
41
  const client = await mongoConnect(config)
@@ -48,17 +51,16 @@ export const initializeMongoDb = async ({ config, collectionNames = {} }) => {
48
51
 
49
52
  const withTransaction = async (action) => {
50
53
  const session = client.startSession()
51
- let actionResponse
52
54
  try {
53
55
  session.startTransaction()
54
- actionResponse = await action({ session })
56
+ const actionResponse = await action({ session })
55
57
  await session.commitTransaction()
58
+ return actionResponse
56
59
  } catch (error) {
57
60
  await session.abortTransaction()
58
61
  throw error
59
62
  } finally {
60
63
  await session.endSession()
61
- return actionResponse
62
64
  }
63
65
  }
64
66
 
@@ -1,186 +1 @@
1
- import amqp from 'amqplib'
2
- import { v4 as uuidv4 } from 'uuid'
3
-
4
- /**
5
- * @typedef {Object} Log
6
- * @property {(msg: string) => void} info
7
- * @property {(msg: string, ...args: any[]) => void} error
8
- */
9
-
10
- /**
11
- * Connects to RabbitMQ server.
12
- * @param {{ host: string }} options
13
- * @returns {Promise<import('amqplib').Connection>}
14
- */
15
- export const connectQueueService = async ({ host }) => {
16
- try {
17
- return await amqp.connect(host)
18
- } catch (error) {
19
- console.error('Failed to connect to RabbitMQ:', error)
20
- throw error
21
- }
22
- }
23
-
24
- /**
25
- * Creates a channel from RabbitMQ connection.
26
- * @param {{ host: string }} options
27
- * @returns {Promise<amqp.Channel>}
28
- */
29
- export const createChannel = async ({ host }) => {
30
- try {
31
- const connection = await connectQueueService({ host })
32
- return await connection.createChannel()
33
- } catch (error) {
34
- console.error('Failed to create channel:', error)
35
- throw error
36
- }
37
- }
38
-
39
- /**
40
- * Parses a RabbitMQ message.
41
- * @param {amqp.ConsumeMessage} msgInfo
42
- * @returns {{ msgId: string, data: any }}
43
- */
44
- const parseMessage = (msgInfo) => {
45
- return JSON.parse(msgInfo.content.toString())
46
- }
47
-
48
- /**
49
- * Subscribes to a queue to receive messages.
50
- *
51
- * @param {Object} options
52
- * @param {import('amqplib').Channel} options.channel - RabbitMQ channel
53
- * @param {string} options.queue - Queue name to subscribe to
54
- * @param {(data: any) => Promise<void>} options.onReceive - Async handler for incoming message
55
- * @param {Log} options.log - Logging utility
56
- * @param {boolean} [options.nackOnError=false] - Whether to nack the message on error (default: false)
57
- * @param {number} [options.prefetch=1] - Max unacked messages per consumer (default: 1)
58
- *
59
- * @returns {Promise<void>}
60
- */
61
- export const subscribeToQueue = async ({
62
- log,
63
- queue,
64
- channel,
65
- prefetch = 1,
66
- onReceive,
67
- nackOnError = false,
68
- }) => {
69
- try {
70
- await channel.assertQueue(queue, { durable: true })
71
-
72
- !!prefetch && (await channel.prefetch(prefetch))
73
-
74
- channel.consume(queue, async (msgInfo) => {
75
- if (!msgInfo) return
76
-
77
- try {
78
- const { msgId, data } = parseMessage(msgInfo)
79
- log.info(`Handling message from '${queue}' msgId: ${msgId}`)
80
- await onReceive(data)
81
- channel.ack(msgInfo)
82
- } catch (error) {
83
- const { msgId } = parseMessage(msgInfo)
84
- log.error(`Error handling message: ${msgId} on queue '${queue}'`)
85
- log.error(error)
86
- nackOnError ? channel.nack(msgInfo) : channel.ack(msgInfo)
87
- }
88
- })
89
- } catch (error) {
90
- console.error('Failed to subscribe to queue:', error)
91
- throw error
92
- }
93
- }
94
-
95
- /**
96
- * Initializes RabbitMQ integration with publish and subscribe support.
97
- *
98
- * @param {Object} options
99
- * @param {string} options.host - RabbitMQ connection URI (e.g., 'amqp://user:pass@localhost:5672')
100
- * @param {Log} options.log - Logging utility with `info()` and `error()` methods
101
- *
102
- * @returns {Promise<{
103
- * publish: (queue: string, data: any) => Promise<boolean>,
104
- * subscribe: (options: {
105
- * queue: string,
106
- * onReceive: (data: any) => Promise<void>,
107
- * nackOnError?: boolean
108
- * }) => Promise<void>,
109
- * channel: amqp.Channel
110
- * }>}
111
- *
112
- * @example
113
- * const rabbit = await initializeQueue({ host, log });
114
- * await rabbit.publish('jobs', { task: 'sendEmail' });
115
- * await rabbit.subscribe({
116
- * queue: 'jobs',
117
- * onReceive: async (data) => { console.log(data); },
118
- * });
119
- */
120
- export const initializeQueue = async ({ host, log }) => {
121
- const channel = await createChannel({ host })
122
-
123
- /**
124
- * Publishes a message to a queue.
125
- * @param {string} queue
126
- * @param {any} data
127
- * @returns {Promise<boolean>}
128
- */
129
- const publish = async (queue, data) => {
130
- const msgId = uuidv4()
131
- try {
132
- await channel.assertQueue(queue, { durable: true })
133
- log.info(`Publishing to '${queue}' msgId: ${msgId}`)
134
- return channel.sendToQueue(
135
- queue,
136
- Buffer.from(JSON.stringify({ msgId, data })),
137
- )
138
- } catch (error) {
139
- log.error(`Error publishing to '${queue}' msgId: ${msgId}`)
140
- log.error(error)
141
- throw error
142
- }
143
- }
144
-
145
- /**
146
- * Subscribes to a queue.
147
- * @param {{
148
- * queue: string,
149
- * onReceive: (data: any) => Promise<void>,
150
- * nackOnError?: boolean
151
- * }} options
152
- * @returns {Promise<void>}
153
- */
154
- const subscribe = async ({ queue, onReceive, nackOnError = false }) => {
155
- return subscribeToQueue({ channel, queue, onReceive, log, nackOnError })
156
- }
157
-
158
- return {
159
- channel,
160
- publish,
161
- subscribe,
162
- }
163
- }
164
-
165
- /**
166
- * Builds RabbitMQ URI from environment variables.
167
- * @param {{
168
- * RABBIT_HOST: string,
169
- * RABBIT_PORT: string | number,
170
- * RABBIT_USERNAME: string,
171
- * RABBIT_PASSWORD: string,
172
- * RABBIT_PROTOCOL?: string
173
- * }} env
174
- * @returns {string}
175
- */
176
- export const rabbitUriFromEnv = (env) => {
177
- const {
178
- RABBIT_HOST,
179
- RABBIT_PORT,
180
- RABBIT_USERNAME,
181
- RABBIT_PASSWORD,
182
- RABBIT_PROTOCOL = 'amqp',
183
- } = env
184
-
185
- return `${RABBIT_PROTOCOL}://${RABBIT_USERNAME}:${RABBIT_PASSWORD}@${RABBIT_HOST}:${RABBIT_PORT}`
186
- }
1
+ export * from './rabbit-mq.js'