core-services-sdk 1.3.6 → 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 (39) 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/generators.js +8 -0
  8. package/src/ids/index.js +2 -0
  9. package/src/ids/prefixes.js +3 -0
  10. package/src/index.js +7 -21
  11. package/src/mailer/transport.factory.js +19 -6
  12. package/src/mongodb/initialize-mongodb.js +9 -7
  13. package/src/rabbit-mq/index.js +1 -186
  14. package/src/rabbit-mq/rabbit-mq.js +189 -0
  15. package/src/templates/index.js +1 -0
  16. package/tests/fastify/error-handler.unit.test.js +39 -0
  17. package/tests/{with-error-handling.test.js → fastify/error-handlers/with-error-handling.test.js} +4 -3
  18. package/tests/http/HttpError.unit.test.js +112 -0
  19. package/tests/http/http-method.unit.test.js +29 -0
  20. package/tests/http/http.unit.test.js +167 -0
  21. package/tests/http/responseType.unit.test.js +45 -0
  22. package/tests/ids/generators.unit.test.js +5 -0
  23. package/tests/ids/prefixes.unit.test.js +1 -0
  24. package/tests/mailer/mailer.integration.test.js +95 -0
  25. package/tests/{mailer.unit.test.js → mailer/mailer.unit.test.js} +7 -11
  26. package/tests/mailer/transport.factory.unit.test.js +204 -0
  27. package/tests/mongodb/connect.unit.test.js +60 -0
  28. package/tests/mongodb/initialize-mongodb.unit.test.js +98 -0
  29. package/tests/mongodb/validate-mongo-uri.unit.test.js +52 -0
  30. package/tests/{rabbit-mq.test.js → rabbit-mq/rabbit-mq.test.js} +3 -2
  31. package/tests/{template-loader.integration.test.js → templates/template-loader.integration.test.js} +1 -1
  32. package/tests/{template-loader.unit.test.js → templates/template-loader.unit.test.js} +1 -1
  33. package/vitest.config.js +3 -0
  34. package/index.js +0 -3
  35. package/tests/HttpError.test.js +0 -80
  36. package/tests/core-util.js +0 -24
  37. package/tests/mailer.integration.test.js +0 -46
  38. package/tests/mongodb.test.js +0 -70
  39. package/tests/transport.factory.unit.test.js +0 -128
@@ -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'
@@ -0,0 +1,189 @@
1
+ // @ts-nocheck
2
+ import * as amqp from 'amqplib'
3
+ import { v4 as uuidv4 } from 'uuid'
4
+
5
+ /**
6
+ * @typedef {Object} Log
7
+ * @property {(msg: string) => void} info
8
+ * @property {(msg: string, ...args: any[]) => void} error
9
+ */
10
+
11
+ /**
12
+ * Connects to RabbitMQ server.
13
+ * @param {{ host: string }} options
14
+ * @returns {Promise<amqp.Connection>}
15
+ */
16
+ export const connectQueueService = async ({ host }) => {
17
+ try {
18
+ return await amqp.connect(host)
19
+ } catch (error) {
20
+ console.error('Failed to connect to RabbitMQ:', error)
21
+ throw error
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Creates a channel from RabbitMQ connection.
27
+ * @param {{ host: string }} options
28
+ * @returns {Promise<amqp.Channel>}
29
+ */
30
+ export const createChannel = async ({ host }) => {
31
+ try {
32
+ const connection = /** @type {amqp.Connection} */ (
33
+ await connectQueueService({ host })
34
+ )
35
+ return await connection.createChannel()
36
+ } catch (error) {
37
+ console.error('Failed to create channel:', error)
38
+ throw error
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Parses a RabbitMQ message.
44
+ * @param {amqp.ConsumeMessage} msgInfo
45
+ * @returns {{ msgId: string, data: any }}
46
+ */
47
+ const parseMessage = (msgInfo) => {
48
+ return JSON.parse(msgInfo.content.toString())
49
+ }
50
+
51
+ /**
52
+ * Subscribes to a queue to receive messages.
53
+ *
54
+ * @param {Object} options
55
+ * @param {import('amqplib').Channel} options.channel - RabbitMQ channel
56
+ * @param {string} options.queue - Queue name to subscribe to
57
+ * @param {(data: any) => Promise<void>} options.onReceive - Async handler for incoming message
58
+ * @param {Log} options.log - Logging utility
59
+ * @param {boolean} [options.nackOnError=false] - Whether to nack the message on error (default: false)
60
+ * @param {number} [options.prefetch=1] - Max unacked messages per consumer (default: 1)
61
+ *
62
+ * @returns {Promise<void>}
63
+ */
64
+ export const subscribeToQueue = async ({
65
+ log,
66
+ queue,
67
+ channel,
68
+ prefetch = 1,
69
+ onReceive,
70
+ nackOnError = false,
71
+ }) => {
72
+ try {
73
+ await channel.assertQueue(queue, { durable: true })
74
+
75
+ !!prefetch && (await channel.prefetch(prefetch))
76
+
77
+ channel.consume(queue, async (msgInfo) => {
78
+ if (!msgInfo) return
79
+
80
+ try {
81
+ const { msgId, data } = parseMessage(msgInfo)
82
+ log.info(`Handling message from '${queue}' msgId: ${msgId}`)
83
+ await onReceive(data)
84
+ channel.ack(msgInfo)
85
+ } catch (error) {
86
+ const { msgId } = parseMessage(msgInfo)
87
+ log.error(`Error handling message: ${msgId} on queue '${queue}'`)
88
+ log.error(error)
89
+ nackOnError ? channel.nack(msgInfo) : channel.ack(msgInfo)
90
+ }
91
+ })
92
+ } catch (error) {
93
+ console.error('Failed to subscribe to queue:', error)
94
+ throw error
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Initializes RabbitMQ integration with publish and subscribe support.
100
+ *
101
+ * @param {Object} options
102
+ * @param {string} options.host - RabbitMQ connection URI (e.g., 'amqp://user:pass@localhost:5672')
103
+ * @param {Log} options.log - Logging utility with `info()` and `error()` methods
104
+ *
105
+ * @returns {Promise<{
106
+ * publish: (queue: string, data: any) => Promise<boolean>,
107
+ * subscribe: (options: {
108
+ * queue: string,
109
+ * onReceive: (data: any) => Promise<void>,
110
+ * nackOnError?: boolean
111
+ * }) => Promise<void>,
112
+ * channel: amqp.Channel
113
+ * }>}
114
+ *
115
+ * @example
116
+ * const rabbit = await initializeQueue({ host, log });
117
+ * await rabbit.publish('jobs', { task: 'sendEmail' });
118
+ * await rabbit.subscribe({
119
+ * queue: 'jobs',
120
+ * onReceive: async (data) => { console.log(data); },
121
+ * });
122
+ */
123
+ export const initializeQueue = async ({ host, log }) => {
124
+ const channel = await createChannel({ host })
125
+
126
+ /**
127
+ * Publishes a message to a queue.
128
+ * @param {string} queue
129
+ * @param {any} data
130
+ * @returns {Promise<boolean>}
131
+ */
132
+ const publish = async (queue, data) => {
133
+ const msgId = uuidv4()
134
+ try {
135
+ await channel.assertQueue(queue, { durable: true })
136
+ log.info(`Publishing to '${queue}' msgId: ${msgId}`)
137
+ return channel.sendToQueue(
138
+ queue,
139
+ Buffer.from(JSON.stringify({ msgId, data })),
140
+ )
141
+ } catch (error) {
142
+ log.error(`Error publishing to '${queue}' msgId: ${msgId}`)
143
+ log.error(error)
144
+ throw error
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Subscribes to a queue.
150
+ * @param {{
151
+ * queue: string,
152
+ * onReceive: (data: any) => Promise<void>,
153
+ * nackOnError?: boolean
154
+ * }} options
155
+ * @returns {Promise<void>}
156
+ */
157
+ const subscribe = async ({ queue, onReceive, nackOnError = false }) => {
158
+ return subscribeToQueue({ channel, queue, onReceive, log, nackOnError })
159
+ }
160
+
161
+ return {
162
+ channel,
163
+ publish,
164
+ subscribe,
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Builds RabbitMQ URI from environment variables.
170
+ * @param {{
171
+ * RABBIT_HOST: string,
172
+ * RABBIT_PORT: string | number,
173
+ * RABBIT_USERNAME: string,
174
+ * RABBIT_PASSWORD: string,
175
+ * RABBIT_PROTOCOL?: string
176
+ * }} env
177
+ * @returns {string}
178
+ */
179
+ export const rabbitUriFromEnv = (env) => {
180
+ const {
181
+ RABBIT_HOST,
182
+ RABBIT_PORT,
183
+ RABBIT_USERNAME,
184
+ RABBIT_PASSWORD,
185
+ RABBIT_PROTOCOL = 'amqp',
186
+ } = env
187
+
188
+ return `${RABBIT_PROTOCOL}://${RABBIT_USERNAME}:${RABBIT_PASSWORD}@${RABBIT_HOST}:${RABBIT_PORT}`
189
+ }
@@ -0,0 +1 @@
1
+ export * from './template-loader.js'
@@ -0,0 +1,39 @@
1
+ import httpStatus from 'http-status'
2
+ import { describe, it, expect } from 'vitest'
3
+
4
+ import { GENERAL_ERROR } from '../../src/fastify/error-codes.js'
5
+
6
+ describe('GENERAL_ERROR', () => {
7
+ it('should have correct status code', () => {
8
+ expect(GENERAL_ERROR.httpStatusCode).toBe(httpStatus.INTERNAL_SERVER_ERROR)
9
+ })
10
+
11
+ it('should have correct status text', () => {
12
+ expect(GENERAL_ERROR.httpStatusText).toBe(
13
+ httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
14
+ )
15
+ })
16
+
17
+ it('should have correct code format', () => {
18
+ const expectedCode = `GENERAL.${
19
+ httpStatus[httpStatus.INTERNAL_SERVER_ERROR]
20
+ }`
21
+ expect(GENERAL_ERROR.code).toBe(expectedCode)
22
+ })
23
+
24
+ it('should have all required properties and types', () => {
25
+ expect(typeof GENERAL_ERROR.httpStatusCode).toBe('number')
26
+ expect(typeof GENERAL_ERROR.httpStatusText).toBe('string')
27
+ expect(typeof GENERAL_ERROR.code).toBe('string')
28
+ })
29
+
30
+ it('should match full expected structure', () => {
31
+ const expected = {
32
+ httpStatusCode: 500,
33
+ httpStatusText: 'Internal Server Error',
34
+ code: 'GENERAL.Internal Server Error',
35
+ }
36
+
37
+ expect(GENERAL_ERROR).toEqual(expected)
38
+ })
39
+ })
@@ -1,13 +1,14 @@
1
+ // @ts-nocheck
1
2
  import httpStatus from 'http-status'
2
3
  import { describe, it, expect, vi, beforeEach } from 'vitest'
3
4
 
4
- import { HttpError } from '../src/http/HttpError.js'
5
- import { GENERAL_ERROR } from '../src/fastify/error-codes.js'
5
+ import { HttpError } from '../../../src/http/HttpError.js'
6
+ import { GENERAL_ERROR } from '../../../src/fastify/error-codes.js'
6
7
  import {
7
8
  replyOnErrorOnly,
8
9
  withErrorHandling,
9
10
  withErrorHandlingReply,
10
- } from '../src/fastify/error-handlers/with-error-handling.js'
11
+ } from '../../../src/fastify/error-handlers/with-error-handling.js'
11
12
 
12
13
  describe('withErrorHandling', () => {
13
14
  const log = { error: vi.fn() }
@@ -0,0 +1,112 @@
1
+ import httpStatus from 'http-status'
2
+ import { describe, it, expect } from 'vitest'
3
+
4
+ import { HttpError } from '../../src/http/HttpError.js'
5
+
6
+ describe('HttpError', () => {
7
+ it('should create an instance with custom message, code, and extendInfo', () => {
8
+ const error = new HttpError({
9
+ code: 'INVALID_INPUT',
10
+ message: 'Invalid input provided',
11
+ httpStatusCode: 400,
12
+ extendInfo: { field: 'email', reason: 'missing' },
13
+ })
14
+
15
+ expect(error).toBeInstanceOf(HttpError)
16
+ expect(error.message).toBe('Invalid input provided')
17
+ expect(error.code).toBe('INVALID_INPUT')
18
+ expect(error.httpStatusCode).toBe(400)
19
+ expect(error.httpStatusText).toBe(httpStatus[400])
20
+ expect(error.extendInfo).toEqual({ field: 'email', reason: 'missing' })
21
+ })
22
+
23
+ it('should fallback to default message from status if message is missing', () => {
24
+ const error = new HttpError({
25
+ code: 'BAD_REQUEST',
26
+ httpStatusCode: 400,
27
+ })
28
+
29
+ expect(error.message).toBe(httpStatus[400])
30
+ expect(error.httpStatusText).toBe(httpStatus[400])
31
+ expect(error.extendInfo).toBeUndefined()
32
+ })
33
+
34
+ it('should fallback to code as message if message and status code missing', () => {
35
+ const error = new HttpError({ code: 'ERROR_CODE_ONLY' })
36
+
37
+ expect(error.message).toBe('ERROR_CODE_ONLY')
38
+ expect(error.httpStatusCode).toBeUndefined()
39
+ expect(error.extendInfo).toBeUndefined()
40
+ })
41
+
42
+ it('should fallback to "Unknown error" if no data provided', () => {
43
+ const error = new HttpError()
44
+
45
+ expect(error.message).toBe('Unknown error')
46
+ expect(error.extendInfo).toBeUndefined()
47
+ })
48
+
49
+ it('should return correct JSON representation without extendInfo', () => {
50
+ const error = new HttpError({
51
+ code: 'NOT_FOUND',
52
+ message: 'Resource not found',
53
+ httpStatusCode: 404,
54
+ })
55
+
56
+ expect(error.toJSON()).toEqual({
57
+ code: 'NOT_FOUND',
58
+ message: 'Resource not found',
59
+ httpStatusCode: 404,
60
+ httpStatusText: httpStatus[404],
61
+ })
62
+ })
63
+
64
+ it('should return correct JSON representation with extendInfo', () => {
65
+ const error = new HttpError({
66
+ code: 'NOT_FOUND',
67
+ message: 'Resource not found',
68
+ httpStatusCode: 404,
69
+ extendInfo: { resource: 'user', id: 123 },
70
+ })
71
+
72
+ expect(error.toJSON()).toEqual({
73
+ code: 'NOT_FOUND',
74
+ message: 'Resource not found',
75
+ httpStatusCode: 404,
76
+ httpStatusText: httpStatus[404],
77
+ extendInfo: { resource: 'user', id: 123 },
78
+ })
79
+ })
80
+
81
+ it('should detect instance using isHttpError', () => {
82
+ const error = new HttpError({
83
+ code: 'TEST',
84
+ httpStatusCode: 500,
85
+ })
86
+
87
+ expect(HttpError.isHttpError(error)).toBe(true)
88
+ expect(HttpError.isHttpError(new Error('x'))).toBe(false)
89
+ })
90
+
91
+ it('FromError should return same instance if already HttpError', () => {
92
+ const original = new HttpError({
93
+ code: 'ALREADY_HTTP',
94
+ httpStatusCode: 401,
95
+ })
96
+ const result = HttpError.FromError(original)
97
+
98
+ expect(result).toBe(original)
99
+ })
100
+
101
+ it('FromError should wrap generic Error', () => {
102
+ const err = new Error('Boom!')
103
+ const httpError = HttpError.FromError(err)
104
+
105
+ expect(httpError).toBeInstanceOf(HttpError)
106
+ expect(httpError.message).toBe('Boom!')
107
+ expect(httpError.code).toBe('UNHANDLED_ERROR')
108
+ expect(httpError.httpStatusCode).toBe(500)
109
+ expect(httpError.httpStatusText).toBe(httpStatus[500])
110
+ expect(httpError.extendInfo).toBeUndefined()
111
+ })
112
+ })
@@ -0,0 +1,29 @@
1
+ // @ts-nocheck
2
+ import { describe, it, expect } from 'vitest'
3
+
4
+ import { HTTP_METHODS } from '../../src/http/http-method.js'
5
+
6
+ describe('HTTP_METHODS constant', () => {
7
+ it('should include all standard HTTP methods', () => {
8
+ expect(HTTP_METHODS).toEqual({
9
+ GET: 'GET',
10
+ POST: 'POST',
11
+ PUT: 'PUT',
12
+ PATCH: 'PATCH',
13
+ DELETE: 'DELETE',
14
+ })
15
+ })
16
+
17
+ it('should be frozen (immutable)', () => {
18
+ expect(Object.isFrozen(HTTP_METHODS)).toBe(true)
19
+
20
+ // Attempt to mutate and verify it fails
21
+ try {
22
+ HTTP_METHODS.NEW = 'NEW'
23
+ } catch (err) {
24
+ // Ignore in strict mode
25
+ }
26
+
27
+ expect(HTTP_METHODS).not.toHaveProperty('NEW')
28
+ })
29
+ })