core-services-sdk 1.0.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.
@@ -0,0 +1 @@
1
+ *.env
@@ -0,0 +1,15 @@
1
+ {
2
+ "arrowParens": "always",
3
+ "bracketSpacing": true,
4
+ "endOfLine": "lf",
5
+ "requirePragma": false,
6
+ "insertPragma": false,
7
+ "jsxBracketSameLine": false,
8
+ "jsxSingleQuote": true,
9
+ "printWidth": 80,
10
+ "quoteProps": "consistent",
11
+ "semi": false,
12
+ "singleQuote": true,
13
+ "tabWidth": 2,
14
+ "trailingComma": "all"
15
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+
6
+ "version": "0.2.0",
7
+ "configurations": [
8
+ {
9
+ "type": "node",
10
+ "request": "launch",
11
+ "name": "Debug Vitest",
12
+ "autoAttachChildProcesses": true,
13
+ "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
14
+ "args": ["run"],
15
+ "smartStep": true,
16
+ "skipFiles": ["<node_internals>/**"],
17
+ "console": "integratedTerminal"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "explorer.sortOrder": "filesFirst",
3
+ "explorer.openEditors.visible": 0,
4
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
5
+ "editor.formatOnSave": true,
6
+ "[javascript]": {
7
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
8
+ },
9
+ "launch": {
10
+ "configurations": [],
11
+ "compounds": []
12
+ },
13
+ "javascript.preferences.importModuleSpecifierEnding": "js",
14
+ "js/ts.implicitProjectConfig.checkJs": true,
15
+ "javascript.validate.enable": false
16
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "core-services-sdk",
3
+ "version": "1.0.0",
4
+ "main": "src/index.js",
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "vitest run --coverage"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/haim-rubin/core-services-sdk.git"
12
+ },
13
+ "keywords": [],
14
+ "author": "",
15
+ "license": "ISC",
16
+ "bugs": {
17
+ "url": "https://github.com/haim-rubin/core-services-sdk/issues"
18
+ },
19
+ "homepage": "https://github.com/haim-rubin/core-services-sdk#readme",
20
+ "description": "",
21
+ "dependencies": {
22
+ "amqplib": "^0.10.8",
23
+ "uuid": "^11.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "@vitest/coverage-v8": "^3.2.4",
27
+ "path": "^0.12.7",
28
+ "url": "^0.11.4",
29
+ "vitest": "^3.2.4"
30
+ }
31
+ }
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './rabbit-mq/index.js'
@@ -0,0 +1,181 @@
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<amqp.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
+ * @param {{
51
+ * channel: amqp.Channel,
52
+ * queue: string,
53
+ * onReceive: (data: any) => Promise<void>,
54
+ * log: Log,
55
+ * nackOnError?: boolean
56
+ * }} options
57
+ * @returns {Promise<void>}
58
+ */
59
+ export const subscribeToQueue = async ({
60
+ channel,
61
+ queue,
62
+ onReceive,
63
+ log,
64
+ nackOnError = false,
65
+ }) => {
66
+ try {
67
+ await channel.assertQueue(queue, { durable: true })
68
+
69
+ channel.consume(queue, async (msgInfo) => {
70
+ if (!msgInfo) return
71
+
72
+ try {
73
+ const { msgId, data } = parseMessage(msgInfo)
74
+ log.info(`Handling message from '${queue}' msgId: ${msgId}`)
75
+ await onReceive(data)
76
+ channel.ack(msgInfo)
77
+ } catch (error) {
78
+ const { msgId } = parseMessage(msgInfo)
79
+ log.error(`Error handling message: ${msgId} on queue '${queue}'`)
80
+ log.error(error)
81
+ nackOnError ? channel.nack(msgInfo) : channel.ack(msgInfo)
82
+ }
83
+ })
84
+ } catch (error) {
85
+ console.error('Failed to subscribe to queue:', error)
86
+ throw error
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Initializes RabbitMQ integration with publish and subscribe support.
92
+ *
93
+ * @param {Object} options
94
+ * @param {string} options.host - RabbitMQ connection URI (e.g., 'amqp://user:pass@localhost:5672')
95
+ * @param {Log} options.log - Logging utility with `info()` and `error()` methods
96
+ *
97
+ * @returns {Promise<{
98
+ * publish: (queue: string, data: any) => Promise<boolean>,
99
+ * subscribe: (options: {
100
+ * queue: string,
101
+ * onReceive: (data: any) => Promise<void>,
102
+ * nackOnError?: boolean
103
+ * }) => Promise<void>,
104
+ * channel: amqp.Channel
105
+ * }>}
106
+ *
107
+ * @example
108
+ * const rabbit = await initializeQueue({ host, log });
109
+ * await rabbit.publish('jobs', { task: 'sendEmail' });
110
+ * await rabbit.subscribe({
111
+ * queue: 'jobs',
112
+ * onReceive: async (data) => { console.log(data); },
113
+ * });
114
+ */
115
+ export const initializeQueue = async ({ host, log }) => {
116
+ const channel = await createChannel({ host })
117
+
118
+ /**
119
+ * Publishes a message to a queue.
120
+ * @param {string} queue
121
+ * @param {any} data
122
+ * @returns {Promise<boolean>}
123
+ */
124
+ const publish = async (queue, data) => {
125
+ const msgId = uuidv4()
126
+ try {
127
+ await channel.assertQueue(queue, { durable: true })
128
+ log.info(`Publishing to '${queue}' msgId: ${msgId}`)
129
+ return channel.sendToQueue(
130
+ queue,
131
+ Buffer.from(JSON.stringify({ msgId, data })),
132
+ )
133
+ } catch (error) {
134
+ log.error(`Error publishing to '${queue}' msgId: ${msgId}`)
135
+ log.error(error)
136
+ throw error
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Subscribes to a queue.
142
+ * @param {{
143
+ * queue: string,
144
+ * onReceive: (data: any) => Promise<void>,
145
+ * nackOnError?: boolean
146
+ * }} options
147
+ * @returns {Promise<void>}
148
+ */
149
+ const subscribe = async ({ queue, onReceive, nackOnError = false }) => {
150
+ return subscribeToQueue({ channel, queue, onReceive, log, nackOnError })
151
+ }
152
+
153
+ return {
154
+ channel,
155
+ publish,
156
+ subscribe,
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Builds RabbitMQ URI from environment variables.
162
+ * @param {{
163
+ * RABBIT_HOST: string,
164
+ * RABBIT_PORT: string | number,
165
+ * RABBIT_USERNAME: string,
166
+ * RABBIT_PASSWORD: string,
167
+ * RABBIT_PROTOCOL?: string
168
+ * }} env
169
+ * @returns {string}
170
+ */
171
+ export const rabbitUriFromEnv = (env) => {
172
+ const {
173
+ RABBIT_HOST,
174
+ RABBIT_PORT,
175
+ RABBIT_USERNAME,
176
+ RABBIT_PASSWORD,
177
+ RABBIT_PROTOCOL = 'amqp',
178
+ } = env
179
+
180
+ return `${RABBIT_PROTOCOL}://${RABBIT_USERNAME}:${RABBIT_PASSWORD}@${RABBIT_HOST}:${RABBIT_PORT}`
181
+ }
@@ -0,0 +1,22 @@
1
+ import { initializeQueue, rabbitUriFromEnv } from './rabbit.js'
2
+
3
+ const log = {
4
+ info: console.log,
5
+ error: console.error,
6
+ }
7
+
8
+ const start = async () => {
9
+ const host = rabbitUriFromEnv(process.env)
10
+ const rabbit = await initializeQueue({ host, log })
11
+
12
+ await rabbit.subscribe({
13
+ queue: 'testQueue',
14
+ onReceive: async (data) => {
15
+ console.log('Received:', data)
16
+ },
17
+ })
18
+
19
+ await rabbit.publish('testQueue', { hello: 'world' })
20
+ }
21
+
22
+ start()
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest'
2
+ import { initializeQueue, rabbitUriFromEnv } from '../src/rabbit-mq/index.js'
3
+
4
+ const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
5
+
6
+ const testLog = {
7
+ info: () => {},
8
+ error: console.error,
9
+ }
10
+
11
+ describe('RabbitMQ SDK', () => {
12
+ const testQueue = 'testQueue'
13
+ const host = 'amqp://botq:botq@0.0.0.0:5672'
14
+ const testMessage = { text: 'Hello Rabbit' }
15
+
16
+ let sdk
17
+ let received = null
18
+
19
+ beforeAll(async () => {
20
+ sdk = await initializeQueue({ host, log: testLog })
21
+
22
+ await sdk.subscribe({
23
+ queue: testQueue,
24
+ onReceive: async (data) => {
25
+ received = data
26
+ },
27
+ })
28
+ })
29
+
30
+ it('should publish and receive message from queue', async () => {
31
+ await sdk.publish(testQueue, testMessage)
32
+ await sleep(300) // let the consumer process the message
33
+ expect(received).toEqual(testMessage)
34
+ })
35
+ })
36
+
37
+ describe('rabbitUriFromEnv', () => {
38
+ it('should build valid amqp URI from env', () => {
39
+ const env = {
40
+ RABBIT_HOST: '0.0.0.0',
41
+ RABBIT_PORT: 5672,
42
+ RABBIT_USERNAME: 'botq',
43
+ RABBIT_PASSWORD: 'botq',
44
+ _RABBIT_HOST: '0.0.0.0',
45
+ RABBIT_PROTOCOL: 'amqp',
46
+ }
47
+
48
+ const uri = rabbitUriFromEnv(env)
49
+ expect(uri).toBe('amqp://botq:botq@0.0.0.0:5672')
50
+ })
51
+
52
+ it('should fallback to amqp when protocol is missing', () => {
53
+ const env = {
54
+ RABBIT_HOST: '0.0.0.0',
55
+ RABBIT_PORT: 5672,
56
+ RABBIT_USERNAME: 'botq',
57
+ RABBIT_PASSWORD: 'botq',
58
+ }
59
+
60
+ const uri = rabbitUriFromEnv(env)
61
+ expect(uri).toBe('amqp://botq:botq@0.0.0.0:5672')
62
+ })
63
+ })