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.
- package/.prettierignore +1 -0
- package/.prettierrc.json +15 -0
- package/.vscode/launch.json +20 -0
- package/.vscode/settings.json +16 -0
- package/package.json +31 -0
- package/src/index.js +1 -0
- package/src/rabbit-mq/index.js +181 -0
- package/src/rabbit-mq/use-how-to.js +22 -0
- package/tests/rabbit-mq.test.js +63 -0
package/.prettierignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.env
|
package/.prettierrc.json
ADDED
|
@@ -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
|
+
})
|