core-services-sdk 1.3.56 → 1.3.59
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/.claude/settings.local.json +22 -0
- package/.prettierrc.json +0 -1
- package/package.json +4 -2
- package/src/http/helpers/create-request-logger.js +38 -0
- package/src/http/index.js +1 -0
- package/src/mongodb/index.js +1 -0
- package/src/postgresql/index.js +1 -0
- package/src/postgresql/paginate.js +61 -0
- package/src/postgresql/start-stop-postgres-docker.js +1 -1
- package/src/rabbit-mq/index.js +1 -0
- package/src/rabbit-mq/rabbit-mq.js +102 -21
- package/src/rabbit-mq/start-stop-rabbitmq.js +152 -0
- package/src/util/context.js +64 -32
- package/tests/http/helpers/create-request-logger.test.js +113 -0
- package/tests/postgresql/paginate.integration.test.js +165 -0
- package/tests/rabbit-mq/rabbit-mq.test.js +121 -51
- package/tests/resources/docker-mongo-test.js +3 -5
- package/types/http/helpers/create-request-logger.d.ts +4 -0
- package/types/http/index.d.ts +1 -0
- package/types/mongodb/index.d.ts +1 -0
- package/types/postgresql/index.d.ts +1 -0
- package/types/postgresql/paginate.d.ts +26 -0
- package/types/rabbit-mq/index.d.ts +1 -0
- package/types/rabbit-mq/rabbit-mq.d.ts +1 -1
- package/types/rabbit-mq/start-stop-rabbitmq.d.ts +74 -0
- package/types/util/context.d.ts +93 -24
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npm test:*)",
|
|
5
|
+
"Bash(docker rm:*)",
|
|
6
|
+
"Bash(docker run:*)",
|
|
7
|
+
"Bash(mongosh:*)",
|
|
8
|
+
"Bash(for:*)",
|
|
9
|
+
"Bash(do if mongosh --port 27099 --eval \"db.runCommand({ ping: 1 })\")",
|
|
10
|
+
"Bash(then echo \"Connected after $i attempts\")",
|
|
11
|
+
"Bash(break)",
|
|
12
|
+
"Bash(fi)",
|
|
13
|
+
"Bash(echo:*)",
|
|
14
|
+
"Bash(done)",
|
|
15
|
+
"Bash(docker logs:*)",
|
|
16
|
+
"Bash(docker system:*)",
|
|
17
|
+
"Bash(docker volume prune:*)",
|
|
18
|
+
"Bash(docker image prune:*)",
|
|
19
|
+
"Bash(docker builder prune:*)"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
package/.prettierrc.json
CHANGED
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-services-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.59",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"lint": "eslint .",
|
|
9
9
|
"lint:fix": "eslint . --fix",
|
|
10
|
-
"test": "vitest run --coverage",
|
|
11
10
|
"format": "prettier --write .",
|
|
11
|
+
"test": "vitest run --coverage",
|
|
12
|
+
"check-format": "prettier --check .",
|
|
12
13
|
"bump": "node ./scripts/bump-version.js",
|
|
14
|
+
"check": "npm run lint && npm run check-format && npm run test",
|
|
13
15
|
"build:types": "tsc --project tsconfig.json && prettier --write ."
|
|
14
16
|
},
|
|
15
17
|
"repository": {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context } from '../../util/context.js'
|
|
2
|
+
/**
|
|
3
|
+
* Creates a request-scoped logger enriched with contextual metadata.
|
|
4
|
+
*
|
|
5
|
+
* The logger is derived from the provided Pino logger and augmented with:
|
|
6
|
+
* - correlationId (from Context)
|
|
7
|
+
* - client IP (from Context)
|
|
8
|
+
* - user agent (from Context)
|
|
9
|
+
* - operation identifier in the form: "<METHOD> <URL>"
|
|
10
|
+
*
|
|
11
|
+
* Intended to be used per incoming Fastify request, typically at the beginning
|
|
12
|
+
* of a request lifecycle, so all subsequent logs automatically include
|
|
13
|
+
* request-specific context.
|
|
14
|
+
*
|
|
15
|
+
* @param {import('fastify').FastifyRequest} request
|
|
16
|
+
* The Fastify request object.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('pino').Logger} log
|
|
19
|
+
* Base Pino logger instance.
|
|
20
|
+
*
|
|
21
|
+
* @returns {import('pino').Logger}
|
|
22
|
+
* A child Pino logger enriched with request and context metadata.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const requestLog = createRequestLogger(request, log, Context)
|
|
26
|
+
* requestLog.info('Handling request')
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const createRequestLogger = (request, log) => {
|
|
30
|
+
const { correlationId, ip, userAgent } = Context?.all() || {}
|
|
31
|
+
|
|
32
|
+
return log.child({
|
|
33
|
+
ip,
|
|
34
|
+
userAgent,
|
|
35
|
+
correlationId,
|
|
36
|
+
op: `${request.method} ${request?.url || request?.routeOptions?.url || request.raw.url}`,
|
|
37
|
+
})
|
|
38
|
+
}
|
package/src/http/index.js
CHANGED
package/src/mongodb/index.js
CHANGED
package/src/postgresql/index.js
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { toSnakeCase } from '../core/case-mapper.js'
|
|
2
|
+
import { normalizeNumberOrDefault } from '../core/normalize-premitives-types-or-default.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic pagination utility.
|
|
6
|
+
*
|
|
7
|
+
* @async
|
|
8
|
+
* @param {Object} params
|
|
9
|
+
* @param {Object} params.db
|
|
10
|
+
* @param {string} params.tableName
|
|
11
|
+
* @param {number} [params.page=1]
|
|
12
|
+
* @param {number} [params.limit=10]
|
|
13
|
+
* @param {Object} [params.filter={}]
|
|
14
|
+
* @param {Object} [params.orderBy]
|
|
15
|
+
* @param {string} params.orderBy.column
|
|
16
|
+
* @param {'asc'|'desc'} [params.orderBy.direction='asc']
|
|
17
|
+
* @returns {Promise<{
|
|
18
|
+
* list: any[],
|
|
19
|
+
* totalCount: number,
|
|
20
|
+
* totalPages: number,
|
|
21
|
+
* currentPage: number,
|
|
22
|
+
* hasNext: boolean,
|
|
23
|
+
* hasPrevious: boolean
|
|
24
|
+
* }>}
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const sqlPaginate = async ({
|
|
28
|
+
db,
|
|
29
|
+
mapRow,
|
|
30
|
+
orderBy,
|
|
31
|
+
page = 1,
|
|
32
|
+
limit = 10,
|
|
33
|
+
tableName,
|
|
34
|
+
filter = {},
|
|
35
|
+
} = {}) => {
|
|
36
|
+
const offset = (page - 1) * limit
|
|
37
|
+
|
|
38
|
+
const query = orderBy?.column
|
|
39
|
+
? db(tableName)
|
|
40
|
+
.select('*')
|
|
41
|
+
.where(toSnakeCase(filter))
|
|
42
|
+
.orderBy(orderBy.column, orderBy.direction || 'asc')
|
|
43
|
+
: db(tableName).select('*').where(toSnakeCase(filter))
|
|
44
|
+
|
|
45
|
+
const [rows, countResult] = await Promise.all([
|
|
46
|
+
query.limit(limit).offset(offset),
|
|
47
|
+
db(tableName).where(toSnakeCase(filter)).count('* as count').first(),
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
const totalCount = normalizeNumberOrDefault(countResult?.count || 0)
|
|
51
|
+
const totalPages = Math.ceil(totalCount / limit)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
list: mapRow ? rows.map(mapRow) : rows,
|
|
55
|
+
totalCount,
|
|
56
|
+
totalPages,
|
|
57
|
+
currentPage: page,
|
|
58
|
+
hasPrevious: page > 1,
|
|
59
|
+
hasNext: page < totalPages,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -124,7 +124,7 @@ export function isPostgresReady(containerName, user, db) {
|
|
|
124
124
|
export function waitForPostgres(containerName, user, db) {
|
|
125
125
|
console.log(`[PgTest] Waiting for PostgreSQL to be ready...`)
|
|
126
126
|
|
|
127
|
-
const maxRetries =
|
|
127
|
+
const maxRetries = 60
|
|
128
128
|
let retries = 0
|
|
129
129
|
let ready = false
|
|
130
130
|
|
package/src/rabbit-mq/index.js
CHANGED
|
@@ -89,17 +89,97 @@ const parseMessage = (msgInfo) => {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
92
|
+
* Creates an unsubscribe function for a RabbitMQ consumer.
|
|
93
|
+
*
|
|
94
|
+
* This is a higher-order function that captures the RabbitMQ channel,
|
|
95
|
+
* consumerTag, and logger in a closure, and returns an async function
|
|
96
|
+
* that cancels the consumer when invoked.
|
|
97
|
+
*
|
|
98
|
+
* The returned function is idempotent and safe to call multiple times.
|
|
99
|
+
* If the channel or consumer is already closed, the call is silently ignored.
|
|
100
|
+
*
|
|
101
|
+
* Usage:
|
|
102
|
+
* const unsubscribe = createUnsubscribe({ channel, consumerTag, logger })
|
|
103
|
+
* await unsubscribe()
|
|
104
|
+
*
|
|
105
|
+
* @param {Object} params
|
|
106
|
+
* @param {import('amqplib').Channel} params.channel
|
|
107
|
+
* The RabbitMQ channel on which the consumer was created.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} params.consumerTag
|
|
110
|
+
* The consumer tag returned by `channel.consume`, uniquely identifying
|
|
111
|
+
* the active consumer within the channel.
|
|
112
|
+
*
|
|
113
|
+
* @param {import('pino').Logger} params.logger
|
|
114
|
+
* Base logger instance used to create a child logger for unsubscribe logs.
|
|
115
|
+
*
|
|
116
|
+
* @returns {() => Promise<void>}
|
|
117
|
+
* An async function that cancels this specific RabbitMQ consumer.
|
|
118
|
+
*
|
|
119
|
+
* @throws {Error}
|
|
120
|
+
* Throws for unexpected errors during consumer cancellation.
|
|
121
|
+
* Errors caused by an already closed channel are silently ignored.
|
|
122
|
+
*/
|
|
123
|
+
const unsubscribe =
|
|
124
|
+
({ channel, consumerTag, logger }) =>
|
|
125
|
+
async () => {
|
|
126
|
+
if (!channel || channel.closed) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const t0 = Date.now()
|
|
131
|
+
const child = logger.child({ op: 'unsubscribe', consumerTag })
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
child.debug('start')
|
|
135
|
+
await channel.cancel(consumerTag)
|
|
136
|
+
child.info({ event: 'ok', ms: Date.now() - t0 })
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err?.message?.includes('Channel closed')) {
|
|
139
|
+
child.debug({
|
|
140
|
+
event: 'already-closed',
|
|
141
|
+
reason: 'channel-already-closed',
|
|
142
|
+
})
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
child.error(err, {
|
|
147
|
+
event: 'error',
|
|
148
|
+
ms: Date.now() - t0,
|
|
149
|
+
})
|
|
150
|
+
throw err
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Subscribes to a RabbitMQ queue and returns an unsubscribe function
|
|
156
|
+
* that cancels this specific consumer.
|
|
157
|
+
*
|
|
158
|
+
* Each call creates an independent consumer with its own consumerTag.
|
|
159
|
+
* Calling the returned unsubscribe function affects only this consumer
|
|
160
|
+
* and does not impact other consumers or services.
|
|
93
161
|
*
|
|
94
162
|
* @param {Object} options
|
|
95
|
-
* @param {import('amqplib').Channel} options.channel
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* @param {
|
|
99
|
-
*
|
|
100
|
-
* @param {number} [options.prefetch=1] - Max unacked messages per consumer (default: 1)
|
|
163
|
+
* @param {import('amqplib').Channel} options.channel
|
|
164
|
+
* RabbitMQ channel used to create the consumer.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} options.queue
|
|
167
|
+
* Queue name to subscribe to.
|
|
101
168
|
*
|
|
102
|
-
* @
|
|
169
|
+
* @param {(data: any, correlationId?: string) => Promise<void>} options.onReceive
|
|
170
|
+
* Async handler invoked for each received message payload.
|
|
171
|
+
*
|
|
172
|
+
* @param {import('pino').Logger} options.log
|
|
173
|
+
* Base logger instance.
|
|
174
|
+
*
|
|
175
|
+
* @param {boolean} [options.nackOnError=false]
|
|
176
|
+
* Whether to nack the message on handler error instead of ack.
|
|
177
|
+
*
|
|
178
|
+
* @param {number} [options.prefetch=1]
|
|
179
|
+
* Maximum number of unacknowledged messages for this consumer.
|
|
180
|
+
*
|
|
181
|
+
* @returns {Promise<() => Promise<void>>}
|
|
182
|
+
* Resolves to an unsubscribe function that cancels this specific consumer.
|
|
103
183
|
*/
|
|
104
184
|
export const subscribeToQueue = async ({
|
|
105
185
|
log,
|
|
@@ -113,14 +193,17 @@ export const subscribeToQueue = async ({
|
|
|
113
193
|
|
|
114
194
|
try {
|
|
115
195
|
await channel.assertQueue(queue, { durable: true })
|
|
116
|
-
|
|
196
|
+
|
|
197
|
+
if (prefetch) {
|
|
198
|
+
await channel.prefetch(prefetch)
|
|
199
|
+
}
|
|
117
200
|
|
|
118
201
|
const { consumerTag } = await channel.consume(queue, async (msgInfo) => {
|
|
119
202
|
if (!msgInfo) {
|
|
120
203
|
return
|
|
121
204
|
}
|
|
122
|
-
const t0 = Date.now()
|
|
123
205
|
|
|
206
|
+
const t0 = Date.now()
|
|
124
207
|
const { msgId, data, correlationId } = parseMessage(msgInfo)
|
|
125
208
|
const child = logger.child({ msgId, correlationId })
|
|
126
209
|
|
|
@@ -131,28 +214,26 @@ export const subscribeToQueue = async ({
|
|
|
131
214
|
await onReceive(data, correlationId)
|
|
132
215
|
channel.ack(msgInfo)
|
|
133
216
|
|
|
134
|
-
child.info({
|
|
135
|
-
event: 'ok',
|
|
136
|
-
ms: Date.now() - t0,
|
|
137
|
-
})
|
|
138
|
-
return
|
|
217
|
+
child.info({ event: 'ok', ms: Date.now() - t0 })
|
|
139
218
|
} catch (err) {
|
|
140
|
-
|
|
219
|
+
if (nackOnError) {
|
|
220
|
+
channel.nack(msgInfo)
|
|
221
|
+
} else {
|
|
222
|
+
channel.ack(msgInfo)
|
|
223
|
+
}
|
|
141
224
|
|
|
142
225
|
child.error(err, {
|
|
143
226
|
event: 'error',
|
|
144
227
|
ms: Date.now() - t0,
|
|
145
228
|
})
|
|
146
|
-
return
|
|
147
229
|
}
|
|
148
230
|
})
|
|
149
231
|
|
|
150
232
|
logger.debug({ consumerTag }, 'consumer-started')
|
|
151
|
-
|
|
233
|
+
|
|
234
|
+
return unsubscribe({ channel, consumerTag, logger })
|
|
152
235
|
} catch (err) {
|
|
153
|
-
logger.error(err, {
|
|
154
|
-
event: 'error',
|
|
155
|
-
})
|
|
236
|
+
logger.error(err, { event: 'error' })
|
|
156
237
|
throw err
|
|
157
238
|
}
|
|
158
239
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Starts a RabbitMQ Docker container for testing purposes.
|
|
5
|
+
*
|
|
6
|
+
* If a container with the same name already exists, it will be removed first.
|
|
7
|
+
* The container is started with the RabbitMQ Management plugin enabled and
|
|
8
|
+
* waits until the health check reports the container as healthy.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} options
|
|
11
|
+
* @param {string} options.containerName
|
|
12
|
+
* Docker container name.
|
|
13
|
+
*
|
|
14
|
+
* @param {number} options.amqpPort
|
|
15
|
+
* Host port mapped to RabbitMQ AMQP port (5672).
|
|
16
|
+
*
|
|
17
|
+
* @param {number} options.uiPort
|
|
18
|
+
* Host port mapped to RabbitMQ Management UI port (15672).
|
|
19
|
+
*
|
|
20
|
+
* @param {string} options.user
|
|
21
|
+
* Default RabbitMQ username.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} options.pass
|
|
24
|
+
* Default RabbitMQ password.
|
|
25
|
+
*
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*
|
|
28
|
+
* @throws {Error}
|
|
29
|
+
* Throws if the RabbitMQ container fails to become healthy within the timeout.
|
|
30
|
+
*/
|
|
31
|
+
export function startRabbit({ containerName, ...rest }) {
|
|
32
|
+
console.log(`[RabbitTest] Starting RabbitMQ...`)
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' })
|
|
36
|
+
} catch {}
|
|
37
|
+
|
|
38
|
+
execSync(
|
|
39
|
+
`docker run -d \
|
|
40
|
+
--name ${containerName} \
|
|
41
|
+
-e RABBITMQ_DEFAULT_USER=${rest.user} \
|
|
42
|
+
-e RABBITMQ_DEFAULT_PASS=${rest.pass} \
|
|
43
|
+
-p ${rest.amqpPort}:5672 \
|
|
44
|
+
-p ${rest.uiPort}:15672 \
|
|
45
|
+
--health-cmd="rabbitmq-diagnostics -q ping" \
|
|
46
|
+
--health-interval=5s \
|
|
47
|
+
--health-timeout=5s \
|
|
48
|
+
--health-retries=10 \
|
|
49
|
+
rabbitmq:3-management`,
|
|
50
|
+
{ stdio: 'inherit' },
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
waitForRabbitHealthy(containerName)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Stops and removes a RabbitMQ Docker container.
|
|
58
|
+
*
|
|
59
|
+
* This function is safe to call even if the container does not exist.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} [containerName='rabbit-test']
|
|
62
|
+
* Docker container name.
|
|
63
|
+
*
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
export function stopRabbit(containerName = 'rabbit-test') {
|
|
67
|
+
console.log(`[RabbitTest] Stopping RabbitMQ...`)
|
|
68
|
+
try {
|
|
69
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' })
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(`[RabbitTest] Failed to stop RabbitMQ: ${error}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Waits until the RabbitMQ Docker container reports a healthy status.
|
|
77
|
+
*
|
|
78
|
+
* Polls the container health status using `docker inspect` and retries
|
|
79
|
+
* for a fixed amount of time before failing.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} containerName
|
|
82
|
+
* Docker container name.
|
|
83
|
+
*
|
|
84
|
+
* @returns {void}
|
|
85
|
+
*
|
|
86
|
+
* @throws {Error}
|
|
87
|
+
* Throws if the container does not become healthy within the timeout.
|
|
88
|
+
*/
|
|
89
|
+
function waitForRabbitHealthy(containerName) {
|
|
90
|
+
console.log(`[RabbitTest] Waiting for RabbitMQ to be healthy...`)
|
|
91
|
+
|
|
92
|
+
const maxRetries = 60
|
|
93
|
+
let retries = 0
|
|
94
|
+
|
|
95
|
+
while (retries < maxRetries) {
|
|
96
|
+
try {
|
|
97
|
+
const output = execSync(
|
|
98
|
+
`docker inspect --format='{{.State.Health.Status}}' ${containerName}`,
|
|
99
|
+
{ encoding: 'utf8' },
|
|
100
|
+
).trim()
|
|
101
|
+
|
|
102
|
+
if (output === 'healthy') {
|
|
103
|
+
console.log(`[RabbitTest] RabbitMQ is ready.`)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (retries % 10 === 0 && retries > 0) {
|
|
108
|
+
console.log(
|
|
109
|
+
`[RabbitTest] Still waiting... Status: ${output} (${retries}/${maxRetries})`,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
if (retries % 10 === 0 && retries > 0) {
|
|
114
|
+
console.log(
|
|
115
|
+
`[RabbitTest] Container not ready yet (${retries}/${maxRetries})`,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
retries++
|
|
121
|
+
execSync(`sleep 1`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('[RabbitTest] Failed to start. Container logs:')
|
|
125
|
+
try {
|
|
126
|
+
execSync(`docker logs --tail 30 ${containerName}`, { stdio: 'inherit' })
|
|
127
|
+
} catch {}
|
|
128
|
+
|
|
129
|
+
throw new Error(
|
|
130
|
+
`[RabbitTest] RabbitMQ failed to become healthy within ${maxRetries} seconds`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Builds a RabbitMQ AMQP connection URI for local testing.
|
|
136
|
+
*
|
|
137
|
+
* @param {Object} options
|
|
138
|
+
* @param {string} options.user
|
|
139
|
+
* RabbitMQ username.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} options.pass
|
|
142
|
+
* RabbitMQ password.
|
|
143
|
+
*
|
|
144
|
+
* @param {number} options.port
|
|
145
|
+
* Host port mapped to RabbitMQ AMQP port.
|
|
146
|
+
*
|
|
147
|
+
* @returns {string}
|
|
148
|
+
* RabbitMQ AMQP connection URI.
|
|
149
|
+
*/
|
|
150
|
+
export function buildRabbitUri({ user, pass, port }) {
|
|
151
|
+
return `amqp://${user}:${pass}@localhost:${port}`
|
|
152
|
+
}
|
package/src/util/context.js
CHANGED
|
@@ -3,79 +3,111 @@ import { AsyncLocalStorage } from 'node:async_hooks'
|
|
|
3
3
|
const als = new AsyncLocalStorage()
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Represents the data stored in the async context for a single execution flow.
|
|
7
|
+
*
|
|
8
|
+
* This object is propagated automatically across async boundaries
|
|
9
|
+
* using Node.js AsyncLocalStorage.
|
|
10
|
+
*
|
|
11
|
+
* It defines the contract between producers (who set values)
|
|
12
|
+
* and consumers (who read values) of the context.
|
|
13
|
+
*
|
|
14
|
+
* @typedef {Object} ContextStore
|
|
15
|
+
*
|
|
16
|
+
* @property {string} [correlationId] - Unique identifier for request or operation tracing.
|
|
17
|
+
* @property {string} [ip] - Client IP address.
|
|
18
|
+
* @property {string} [userAgent] - Client user agent string.
|
|
19
|
+
* @property {string} [tenantId] - Active tenant identifier.
|
|
20
|
+
* @property {string} [userId] - Authenticated user identifier.
|
|
10
21
|
*/
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Async execution context manager built on top of Node.js AsyncLocalStorage.
|
|
25
|
+
*
|
|
26
|
+
* This class provides a thin, static API for storing and accessing
|
|
27
|
+
* request-scoped (or async-chain-scoped) metadata such as correlation IDs,
|
|
28
|
+
* user information, tenant identifiers, and similar data.
|
|
29
|
+
*
|
|
30
|
+
* The context is bound to the current async execution chain using
|
|
31
|
+
* AsyncLocalStorage and is automatically propagated across `await` boundaries.
|
|
32
|
+
*
|
|
33
|
+
* This class is intentionally static and acts as a singleton wrapper
|
|
34
|
+
* around AsyncLocalStorage.
|
|
35
|
+
*/
|
|
36
|
+
export class Context {
|
|
13
37
|
/**
|
|
14
|
-
* Run a callback within a given context store.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
38
|
+
* Run a callback within a given async context store.
|
|
39
|
+
*
|
|
40
|
+
* All asynchronous operations spawned inside the callback
|
|
41
|
+
* will have access to the provided store via {@link Context.get},
|
|
42
|
+
* {@link Context.set}, or {@link Context.all}.
|
|
17
43
|
*
|
|
18
44
|
* @template T
|
|
19
|
-
* @param {
|
|
45
|
+
* @param {ContextStore} store - Initial context store for this execution.
|
|
20
46
|
* @param {() => T} callback - Function to execute inside the context.
|
|
21
47
|
* @returns {T} The return value of the callback (sync or async).
|
|
22
48
|
*
|
|
23
49
|
* @example
|
|
24
50
|
* Context.run(
|
|
25
|
-
* { correlationId: 'abc123' },
|
|
51
|
+
* { correlationId: 'abc123', userId: 'usr_1' },
|
|
26
52
|
* async () => {
|
|
27
53
|
* console.log(Context.get('correlationId')) // "abc123"
|
|
28
54
|
* }
|
|
29
55
|
* )
|
|
30
56
|
*/
|
|
31
|
-
run(store, callback) {
|
|
57
|
+
static run(store, callback) {
|
|
32
58
|
return als.run(store, callback)
|
|
33
|
-
}
|
|
59
|
+
}
|
|
34
60
|
|
|
35
61
|
/**
|
|
36
62
|
* Retrieve a single value from the current async context store.
|
|
37
63
|
*
|
|
38
|
-
* @
|
|
39
|
-
*
|
|
40
|
-
*
|
|
64
|
+
* If called outside of an active {@link Context.run},
|
|
65
|
+
* this method returns `undefined`.
|
|
66
|
+
*
|
|
67
|
+
* @template {keyof ContextStore} K
|
|
68
|
+
* @param {K} key - Context property name.
|
|
69
|
+
* @returns {ContextStore[K] | undefined} The stored value, if present.
|
|
41
70
|
*
|
|
42
71
|
* @example
|
|
43
|
-
* const
|
|
72
|
+
* const tenantId = Context.get('tenantId')
|
|
44
73
|
*/
|
|
45
|
-
get(key) {
|
|
74
|
+
static get(key) {
|
|
46
75
|
const store = als.getStore()
|
|
47
76
|
return store?.[key]
|
|
48
|
-
}
|
|
77
|
+
}
|
|
49
78
|
|
|
50
79
|
/**
|
|
51
|
-
* Set a single key-value pair
|
|
52
|
-
*
|
|
53
|
-
*
|
|
80
|
+
* Set a single key-value pair on the current async context store.
|
|
81
|
+
*
|
|
82
|
+
* If called outside of an active {@link Context.run},
|
|
83
|
+
* this method is a no-op.
|
|
54
84
|
*
|
|
55
|
-
* @
|
|
56
|
-
* @param {
|
|
85
|
+
* @template {keyof ContextStore} K
|
|
86
|
+
* @param {K} key - Context property name.
|
|
87
|
+
* @param {ContextStore[K]} value - Value to store.
|
|
57
88
|
*
|
|
58
89
|
* @example
|
|
59
90
|
* Context.set('tenantId', 'tnt_1234')
|
|
60
91
|
*/
|
|
61
|
-
set(key, value) {
|
|
92
|
+
static set(key, value) {
|
|
62
93
|
const store = als.getStore()
|
|
63
94
|
if (store) {
|
|
64
95
|
store[key] = value
|
|
65
96
|
}
|
|
66
|
-
}
|
|
97
|
+
}
|
|
67
98
|
|
|
68
99
|
/**
|
|
69
|
-
* Get the
|
|
100
|
+
* Get the full context store for the current async execution.
|
|
101
|
+
*
|
|
102
|
+
* If no context is active, an empty object is returned.
|
|
70
103
|
*
|
|
71
|
-
* @returns {
|
|
72
|
-
* or an empty object if no store exists.
|
|
104
|
+
* @returns {ContextStore}
|
|
73
105
|
*
|
|
74
106
|
* @example
|
|
75
|
-
* const
|
|
76
|
-
* console.log(
|
|
107
|
+
* const ctx = Context.all()
|
|
108
|
+
* console.log(ctx.correlationId)
|
|
77
109
|
*/
|
|
78
|
-
all() {
|
|
110
|
+
static all() {
|
|
79
111
|
return als.getStore() || {}
|
|
80
|
-
}
|
|
112
|
+
}
|
|
81
113
|
}
|