core-services-sdk 1.3.20 → 1.3.22
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/.eslintrc.json +14 -0
- package/.vscode/settings.json +6 -1
- package/eslint.config.js +21 -0
- package/package.json +12 -3
- package/scripts/bump-version.js +68 -0
- package/src/core/normalize-phone-number.js +7 -4
- package/src/core/regex-utils.js +6 -2
- package/src/http/http.js +6 -2
- package/src/ids/generators.js +13 -11
- package/src/ids/prefixes.js +2 -2
- package/src/mongodb/validate-mongo-uri.js +1 -1
- package/src/rabbit-mq/rabbit-mq.js +134 -63
- package/tests/http/http-method.unit.test.js +1 -1
- package/tests/ids/generators.unit.test.js +9 -9
- package/tests/ids/prefixes.unit.test.js +1 -1
- package/tests/rabbit-mq/rabbit-mq.test.js +4 -0
- package/tests/templates/template-loader.unit.test.js +0 -4
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"es2021": true,
|
|
4
|
+
"node": true
|
|
5
|
+
},
|
|
6
|
+
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
|
|
7
|
+
"plugins": ["prettier"],
|
|
8
|
+
"rules": {
|
|
9
|
+
"prettier/prettier": "error",
|
|
10
|
+
"no-unused-vars": "warn",
|
|
11
|
+
"no-console": "off"
|
|
12
|
+
},
|
|
13
|
+
"ignores": ["node_modules", "dist", ".next"]
|
|
14
|
+
}
|
package/.vscode/settings.json
CHANGED
|
@@ -12,5 +12,10 @@
|
|
|
12
12
|
},
|
|
13
13
|
"javascript.preferences.importModuleSpecifierEnding": "js",
|
|
14
14
|
"js/ts.implicitProjectConfig.checkJs": true,
|
|
15
|
-
"javascript.validate.enable": true
|
|
15
|
+
"javascript.validate.enable": true,
|
|
16
|
+
"eslint.validate": ["javascript"],
|
|
17
|
+
|
|
18
|
+
"editor.codeActionsOnSave": {
|
|
19
|
+
"source.fixAll.eslint": "explicit"
|
|
20
|
+
}
|
|
16
21
|
}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import eslintPluginPrettier from 'eslint-plugin-prettier'
|
|
2
|
+
|
|
3
|
+
/** @type {import("eslint").Linter.FlatConfig} */
|
|
4
|
+
export default [
|
|
5
|
+
{
|
|
6
|
+
files: ['**/*.js'],
|
|
7
|
+
languageOptions: {
|
|
8
|
+
sourceType: 'module',
|
|
9
|
+
ecmaVersion: 'latest',
|
|
10
|
+
},
|
|
11
|
+
plugins: {
|
|
12
|
+
prettier: eslintPluginPrettier,
|
|
13
|
+
},
|
|
14
|
+
rules: {
|
|
15
|
+
'no-console': 'off',
|
|
16
|
+
'no-unused-vars': 'warn',
|
|
17
|
+
'curly': ['error', 'all'],
|
|
18
|
+
'prettier/prettier': 'error',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
]
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-services-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.22",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
7
|
+
"lint": "eslint .",
|
|
8
|
+
"lint:fix": "eslint . --fix",
|
|
9
|
+
"test": "vitest run --coverage",
|
|
10
|
+
"format": "prettier --write .",
|
|
11
|
+
"bump": "node ./scripts/bump-version.js"
|
|
8
12
|
},
|
|
9
13
|
"repository": {
|
|
10
14
|
"type": "git",
|
|
@@ -30,13 +34,18 @@
|
|
|
30
34
|
"mongodb": "^6.18.0",
|
|
31
35
|
"nodemailer": "^7.0.5",
|
|
32
36
|
"pino": "^9.7.0",
|
|
37
|
+
"ulid": "^3.0.1",
|
|
33
38
|
"uuid": "^11.1.0",
|
|
34
39
|
"xml2js": "^0.6.2"
|
|
35
40
|
},
|
|
36
41
|
"devDependencies": {
|
|
37
42
|
"@vitest/coverage-v8": "^3.2.4",
|
|
43
|
+
"eslint": "^9.30.0",
|
|
44
|
+
"eslint-config-prettier": "^10.1.5",
|
|
45
|
+
"eslint-plugin-prettier": "^5.5.1",
|
|
38
46
|
"path": "^0.12.7",
|
|
47
|
+
"prettier": "^3.6.2",
|
|
39
48
|
"url": "^0.11.4",
|
|
40
49
|
"vitest": "^3.2.4"
|
|
41
50
|
}
|
|
42
|
-
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
|
|
5
|
+
const type = process.argv[2] // "major" | "minor" | "patch" | "-major" | "-minor" | "-patch"
|
|
6
|
+
|
|
7
|
+
if (!['major', 'minor', 'patch', '-major', '-minor', '-patch'].includes(type)) {
|
|
8
|
+
console.error(`Usage: npm run bump <major|minor|patch|-major|-minor|-patch>`)
|
|
9
|
+
process.exit(1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json')
|
|
13
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
14
|
+
|
|
15
|
+
let [major, minor, patch] = pkg.version.split('.').map(Number)
|
|
16
|
+
|
|
17
|
+
const bump = () => {
|
|
18
|
+
switch (type) {
|
|
19
|
+
case 'major':
|
|
20
|
+
return [major + 1, 0, 0]
|
|
21
|
+
case 'minor':
|
|
22
|
+
return [major, minor + 1, 0]
|
|
23
|
+
case 'patch':
|
|
24
|
+
return [major, minor, patch + 1]
|
|
25
|
+
case '-major':
|
|
26
|
+
if (major === 0) {
|
|
27
|
+
throw new Error('Cannot decrement major below 0')
|
|
28
|
+
}
|
|
29
|
+
return [major - 1, 0, 0]
|
|
30
|
+
case '-minor':
|
|
31
|
+
if (minor === 0) {
|
|
32
|
+
if (major === 0) {
|
|
33
|
+
throw new Error('Cannot decrement minor below 0.0')
|
|
34
|
+
}
|
|
35
|
+
return [major - 1, 0, 0]
|
|
36
|
+
}
|
|
37
|
+
return [major, minor - 1, 0]
|
|
38
|
+
case '-patch':
|
|
39
|
+
if (patch === 0) {
|
|
40
|
+
if (minor === 0) {
|
|
41
|
+
if (major === 0) {
|
|
42
|
+
throw new Error('Cannot decrement below 0.0.0')
|
|
43
|
+
}
|
|
44
|
+
return [major - 1, 0, 0]
|
|
45
|
+
}
|
|
46
|
+
return [major, minor - 1, 0]
|
|
47
|
+
}
|
|
48
|
+
return [major, minor, patch - 1]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [newMajor, newMinor, newPatch] = bump()
|
|
53
|
+
const newVersion = `${newMajor}.${newMinor}.${newPatch}`
|
|
54
|
+
|
|
55
|
+
pkg.version = newVersion
|
|
56
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
57
|
+
|
|
58
|
+
console.log(`✔ Updated version to ${newVersion}`)
|
|
59
|
+
|
|
60
|
+
// update package-lock.json if exists
|
|
61
|
+
if (fs.existsSync('./package-lock.json')) {
|
|
62
|
+
try {
|
|
63
|
+
execSync('npm install --package-lock-only', { stdio: 'inherit' })
|
|
64
|
+
console.log('✔ package-lock.json updated')
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('Failed to update package-lock.json', err)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -7,10 +7,9 @@ import * as raw from 'google-libphonenumber'
|
|
|
7
7
|
export function getLib() {
|
|
8
8
|
// Prefer direct (CJS-style or ESM w/ named), else default
|
|
9
9
|
// e.g. raw.PhoneNumberUtil OR raw.default.PhoneNumberUtil
|
|
10
|
-
// eslint-disable-next-line no-unused-vars
|
|
11
10
|
/** @type {any} */
|
|
12
11
|
const anyRaw = raw
|
|
13
|
-
const lib = anyRaw.PhoneNumberUtil ? anyRaw : anyRaw.default ?? anyRaw
|
|
12
|
+
const lib = anyRaw.PhoneNumberUtil ? anyRaw : (anyRaw.default ?? anyRaw)
|
|
14
13
|
|
|
15
14
|
if (!lib || !lib.PhoneNumberUtil || !lib.PhoneNumberFormat) {
|
|
16
15
|
throw new Error('google-libphonenumber failed to load (exports not found)')
|
|
@@ -71,7 +70,9 @@ export function normalizePhoneOrThrowIntl(input) {
|
|
|
71
70
|
try {
|
|
72
71
|
const util = phoneUtil()
|
|
73
72
|
const parsed = util.parseAndKeepRawInput(clean(input))
|
|
74
|
-
if (!util.isValidNumber(parsed))
|
|
73
|
+
if (!util.isValidNumber(parsed)) {
|
|
74
|
+
throw new Error('Phone number failed validation')
|
|
75
|
+
}
|
|
75
76
|
return toResult(parsed)
|
|
76
77
|
} catch (e) {
|
|
77
78
|
const err = new Error('Invalid phone number')
|
|
@@ -91,7 +92,9 @@ export function normalizePhoneOrThrowWithRegion(input, defaultRegion) {
|
|
|
91
92
|
try {
|
|
92
93
|
const util = phoneUtil()
|
|
93
94
|
const parsed = util.parseAndKeepRawInput(clean(input), defaultRegion)
|
|
94
|
-
if (!util.isValidNumber(parsed))
|
|
95
|
+
if (!util.isValidNumber(parsed)) {
|
|
96
|
+
throw new Error('Phone number failed validation')
|
|
97
|
+
}
|
|
95
98
|
return toResult(parsed)
|
|
96
99
|
} catch (e) {
|
|
97
100
|
const err = new Error('Invalid phone number')
|
package/src/core/regex-utils.js
CHANGED
|
@@ -16,8 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
export const isValidRegex = (pattern) => {
|
|
19
|
-
if (pattern instanceof RegExp)
|
|
20
|
-
|
|
19
|
+
if (pattern instanceof RegExp) {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
if (typeof pattern !== 'string') {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
21
25
|
try {
|
|
22
26
|
new RegExp(pattern)
|
|
23
27
|
return true
|
package/src/http/http.js
CHANGED
|
@@ -83,7 +83,9 @@ const tryGetJsonResponse = async (response) => {
|
|
|
83
83
|
jsonText = await getTextResponse(response)
|
|
84
84
|
return tryConvertJsonResponse(jsonText)
|
|
85
85
|
} catch (error) {
|
|
86
|
-
if (!jsonText)
|
|
86
|
+
if (!jsonText) {
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
87
89
|
return jsonText
|
|
88
90
|
}
|
|
89
91
|
}
|
|
@@ -100,7 +102,9 @@ const tryGetXmlResponse = async (response) => {
|
|
|
100
102
|
xmlText = await getTextResponse(response)
|
|
101
103
|
return await parseStringPromise(xmlText)
|
|
102
104
|
} catch (error) {
|
|
103
|
-
if (!xmlText)
|
|
105
|
+
if (!xmlText) {
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
104
108
|
return xmlText
|
|
105
109
|
}
|
|
106
110
|
}
|
package/src/ids/generators.js
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ulid } from 'ulid'
|
|
2
2
|
import { ID_PREFIXES } from './prefixes.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Generates a new
|
|
5
|
+
* Generates a new ULID string.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* ULIDs are 26-character, lexicographically sortable identifiers.
|
|
8
|
+
*
|
|
9
|
+
* @returns {string} A new ULID.
|
|
8
10
|
*
|
|
9
11
|
* @example
|
|
10
|
-
* generateId() // '
|
|
12
|
+
* generateId() // '01HZY3M7K4FJ9A8Q4Y1ZB5NX3T'
|
|
11
13
|
*/
|
|
12
14
|
export const generateId = () => {
|
|
13
|
-
return
|
|
15
|
+
return ulid()
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Generates a unique ID with the given prefix.
|
|
18
20
|
*
|
|
19
|
-
* @param {string} prefix - A prefix string to prepend to the
|
|
20
|
-
* @returns {string} A unique ID in the format `${prefix}_${
|
|
21
|
+
* @param {string} prefix - A prefix string to prepend to the ULID.
|
|
22
|
+
* @returns {string} A unique ID in the format `${prefix}_${ulid}`.
|
|
21
23
|
*
|
|
22
24
|
* @example
|
|
23
|
-
* generatePrefixedId('usr') // '
|
|
25
|
+
* generatePrefixedId('usr') // 'usr_01HZY3M7K4FJ9A8Q4Y1ZB5NX3T'
|
|
24
26
|
*/
|
|
25
27
|
export const generatePrefixedId = (prefix) => {
|
|
26
28
|
return `${prefix}_${generateId()}`
|
|
@@ -73,9 +75,9 @@ export const generateRolePermissionsId = () =>
|
|
|
73
75
|
generatePrefixedId(ID_PREFIXES.ROLE_PERMISSIONS)
|
|
74
76
|
|
|
75
77
|
/**
|
|
76
|
-
* Generates
|
|
78
|
+
* Generates an onboarding ID with a `onb_` prefix.
|
|
77
79
|
*
|
|
78
|
-
* @returns {string}
|
|
80
|
+
* @returns {string} An onboarding ID.
|
|
79
81
|
*/
|
|
80
82
|
export const generateOnboardingId = () =>
|
|
81
83
|
generatePrefixedId(ID_PREFIXES.ONBOARDING)
|
|
@@ -83,6 +85,6 @@ export const generateOnboardingId = () =>
|
|
|
83
85
|
/**
|
|
84
86
|
* Generates a session ID with a `sess_` prefix.
|
|
85
87
|
*
|
|
86
|
-
* @returns {string} A
|
|
88
|
+
* @returns {string} A session ID.
|
|
87
89
|
*/
|
|
88
90
|
export const generateSessionId = () => generatePrefixedId(ID_PREFIXES.SESSION)
|
package/src/ids/prefixes.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mapping of entity types to their unique ID prefixes.
|
|
3
3
|
*
|
|
4
|
-
* These prefixes are prepended to
|
|
5
|
-
* For example: '
|
|
4
|
+
* These prefixes are prepended to ULIDs to create consistent and identifiable IDs across the system.
|
|
5
|
+
* For example: 'usr_01HZY3M7K4FJ9A8Q4Y1ZB5NX3T'
|
|
6
6
|
*
|
|
7
7
|
* @readonly
|
|
8
8
|
* @enum {string}
|
|
@@ -1,48 +1,83 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import * as amqp from 'amqplib'
|
|
3
|
-
import {
|
|
3
|
+
import { ulid } from 'ulid'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {Object} Log
|
|
7
|
-
* @property {(
|
|
8
|
-
* @property {(
|
|
7
|
+
* @property {(obj: any, msg?: string) => void} info
|
|
8
|
+
* @property {(obj: any, msg?: string) => void} error
|
|
9
|
+
* @property {(obj: any, msg?: string) => void} debug
|
|
9
10
|
*/
|
|
10
11
|
|
|
12
|
+
const generateMsgId = () => `rbt_${ulid()}`
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
|
-
* Connects to RabbitMQ server.
|
|
13
|
-
*
|
|
15
|
+
* Connects to a RabbitMQ server.
|
|
16
|
+
*
|
|
17
|
+
* @param {{ host: string, log: import('pino').Logger }} options
|
|
14
18
|
* @returns {Promise<amqp.Connection>}
|
|
15
19
|
*/
|
|
16
|
-
export const connectQueueService = async ({ host }) => {
|
|
20
|
+
export const connectQueueService = async ({ host, log }) => {
|
|
21
|
+
const t0 = Date.now()
|
|
22
|
+
const logger = log.child({ op: 'connectQueueService', host })
|
|
23
|
+
|
|
17
24
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
logger.debug('start')
|
|
26
|
+
const connection = await amqp.connect(host)
|
|
27
|
+
|
|
28
|
+
logger.info({
|
|
29
|
+
event: 'ok',
|
|
30
|
+
ms: Date.now() - t0,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return connection
|
|
34
|
+
} catch (err) {
|
|
35
|
+
logger.error(err, {
|
|
36
|
+
event: 'error',
|
|
37
|
+
ms: Date.now() - t0,
|
|
38
|
+
})
|
|
39
|
+
throw err
|
|
22
40
|
}
|
|
23
41
|
}
|
|
24
42
|
|
|
25
43
|
/**
|
|
26
|
-
* Creates a channel from RabbitMQ connection.
|
|
27
|
-
*
|
|
44
|
+
* Creates a channel from a RabbitMQ connection.
|
|
45
|
+
*
|
|
46
|
+
* @param {{ host: string, log: import('pino').Logger }} options
|
|
28
47
|
* @returns {Promise<amqp.Channel>}
|
|
29
48
|
*/
|
|
30
|
-
export const createChannel = async ({ host }) => {
|
|
49
|
+
export const createChannel = async ({ host, log }) => {
|
|
50
|
+
const t0 = Date.now()
|
|
51
|
+
const logger = log.child({ op: 'createChannel', host })
|
|
52
|
+
|
|
31
53
|
try {
|
|
54
|
+
logger.debug('start')
|
|
32
55
|
const connection = /** @type {amqp.Connection} */ (
|
|
33
|
-
await connectQueueService({ host })
|
|
56
|
+
await connectQueueService({ host, log })
|
|
34
57
|
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
const channel = await connection.createChannel()
|
|
59
|
+
|
|
60
|
+
logger.debug('channel-created')
|
|
61
|
+
logger.info({
|
|
62
|
+
event: 'ok',
|
|
63
|
+
ms: Date.now() - t0,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return channel
|
|
67
|
+
} catch (err) {
|
|
68
|
+
logger.error(err, {
|
|
69
|
+
event: 'error',
|
|
70
|
+
ms: Date.now() - t0,
|
|
71
|
+
})
|
|
72
|
+
throw err
|
|
39
73
|
}
|
|
40
74
|
}
|
|
41
75
|
|
|
42
76
|
/**
|
|
43
|
-
* Parses a RabbitMQ message.
|
|
77
|
+
* Parses a RabbitMQ message into a structured object.
|
|
78
|
+
*
|
|
44
79
|
* @param {amqp.ConsumeMessage} msgInfo
|
|
45
|
-
* @returns {{ msgId: string, data: any }}
|
|
80
|
+
* @returns {{ msgId: string, data: any, correlationId?: string }}
|
|
46
81
|
*/
|
|
47
82
|
const parseMessage = (msgInfo) => {
|
|
48
83
|
return JSON.parse(msgInfo.content.toString())
|
|
@@ -54,8 +89,8 @@ const parseMessage = (msgInfo) => {
|
|
|
54
89
|
* @param {Object} options
|
|
55
90
|
* @param {import('amqplib').Channel} options.channel - RabbitMQ channel
|
|
56
91
|
* @param {string} options.queue - Queue name to subscribe to
|
|
57
|
-
* @param {(data: any) => Promise<void>} options.onReceive - Async handler
|
|
58
|
-
* @param {
|
|
92
|
+
* @param {(data: any, correlationId?: string) => Promise<void>} options.onReceive - Async handler
|
|
93
|
+
* @param {import('pino').Logger} options.log - Base logger
|
|
59
94
|
* @param {boolean} [options.nackOnError=false] - Whether to nack the message on error (default: false)
|
|
60
95
|
* @param {number} [options.prefetch=1] - Max unacked messages per consumer (default: 1)
|
|
61
96
|
*
|
|
@@ -69,29 +104,48 @@ export const subscribeToQueue = async ({
|
|
|
69
104
|
onReceive,
|
|
70
105
|
nackOnError = false,
|
|
71
106
|
}) => {
|
|
107
|
+
const logger = log.child({ op: 'subscribeToQueue', queue })
|
|
108
|
+
|
|
72
109
|
try {
|
|
73
110
|
await channel.assertQueue(queue, { durable: true })
|
|
74
|
-
|
|
75
111
|
!!prefetch && (await channel.prefetch(prefetch))
|
|
76
112
|
|
|
77
113
|
channel.consume(queue, async (msgInfo) => {
|
|
78
|
-
if (!msgInfo)
|
|
114
|
+
if (!msgInfo) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
const t0 = Date.now()
|
|
118
|
+
|
|
119
|
+
const { msgId, data, correlationId } = parseMessage(msgInfo)
|
|
120
|
+
const child = logger.child({ msgId, correlationId })
|
|
79
121
|
|
|
80
122
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
123
|
+
child.debug('start')
|
|
124
|
+
child.info('message-received')
|
|
125
|
+
|
|
126
|
+
await onReceive(data, correlationId)
|
|
84
127
|
channel.ack(msgInfo)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
|
|
129
|
+
child.info({
|
|
130
|
+
event: 'ok',
|
|
131
|
+
ms: Date.now() - t0,
|
|
132
|
+
})
|
|
133
|
+
return
|
|
134
|
+
} catch (err) {
|
|
89
135
|
nackOnError ? channel.nack(msgInfo) : channel.ack(msgInfo)
|
|
136
|
+
|
|
137
|
+
child.error(err, {
|
|
138
|
+
event: 'error',
|
|
139
|
+
ms: Date.now() - t0,
|
|
140
|
+
})
|
|
141
|
+
return
|
|
90
142
|
}
|
|
91
143
|
})
|
|
92
|
-
} catch (
|
|
93
|
-
|
|
94
|
-
|
|
144
|
+
} catch (err) {
|
|
145
|
+
logger.error(err, {
|
|
146
|
+
event: 'error',
|
|
147
|
+
})
|
|
148
|
+
throw err
|
|
95
149
|
}
|
|
96
150
|
}
|
|
97
151
|
|
|
@@ -99,57 +153,73 @@ export const subscribeToQueue = async ({
|
|
|
99
153
|
* Initializes RabbitMQ integration with publish and subscribe support.
|
|
100
154
|
*
|
|
101
155
|
* @param {Object} options
|
|
102
|
-
* @param {string} options.host - RabbitMQ connection URI
|
|
103
|
-
* @param {
|
|
156
|
+
* @param {string} options.host - RabbitMQ connection URI
|
|
157
|
+
* @param {import('pino').Logger} options.log - Logger
|
|
104
158
|
*
|
|
105
159
|
* @returns {Promise<{
|
|
106
|
-
* publish: (queue: string, data: any) => Promise<boolean>,
|
|
160
|
+
* publish: (queue: string, data: any, correlationId?: string) => Promise<boolean>,
|
|
107
161
|
* subscribe: (options: {
|
|
108
162
|
* queue: string,
|
|
109
|
-
* onReceive: (data: any) => Promise<void>,
|
|
163
|
+
* onReceive: (data: any, correlationId?: string) => Promise<void>,
|
|
110
164
|
* nackOnError?: boolean
|
|
111
165
|
* }) => Promise<void>,
|
|
112
166
|
* channel: amqp.Channel
|
|
113
167
|
* }>}
|
|
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
168
|
*/
|
|
123
169
|
export const initializeQueue = async ({ host, log }) => {
|
|
124
|
-
const channel = await createChannel({ host })
|
|
170
|
+
const channel = await createChannel({ host, log })
|
|
171
|
+
const logger = log.child({ op: 'initializeQueue' })
|
|
125
172
|
|
|
126
173
|
/**
|
|
127
|
-
* Publishes a message to a queue.
|
|
128
|
-
*
|
|
129
|
-
* @param {
|
|
130
|
-
* @
|
|
174
|
+
* Publishes a message to a queue with a generated `rbt_<ulid>` ID.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} queue - Queue name
|
|
177
|
+
* @param {any} data - Payload to send
|
|
178
|
+
* @param {string} [correlationId] - Correlation ID for tracing
|
|
179
|
+
* @returns {Promise<boolean>} True if the message was sent successfully
|
|
131
180
|
*/
|
|
132
|
-
const publish = async (queue, data) => {
|
|
133
|
-
const msgId =
|
|
181
|
+
const publish = async (queue, data, correlationId) => {
|
|
182
|
+
const msgId = generateMsgId()
|
|
183
|
+
const t0 = Date.now()
|
|
184
|
+
const logChild = logger.child({
|
|
185
|
+
op: 'publish',
|
|
186
|
+
queue,
|
|
187
|
+
msgId,
|
|
188
|
+
correlationId,
|
|
189
|
+
})
|
|
190
|
+
|
|
134
191
|
try {
|
|
192
|
+
logChild.debug('start')
|
|
193
|
+
|
|
135
194
|
await channel.assertQueue(queue, { durable: true })
|
|
136
|
-
|
|
137
|
-
|
|
195
|
+
const payload = { msgId, data, correlationId }
|
|
196
|
+
const sent = channel.sendToQueue(
|
|
138
197
|
queue,
|
|
139
|
-
Buffer.from(JSON.stringify(
|
|
198
|
+
Buffer.from(JSON.stringify(payload)),
|
|
140
199
|
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
200
|
+
|
|
201
|
+
logChild.debug('message-sent')
|
|
202
|
+
logChild.info({
|
|
203
|
+
event: 'ok',
|
|
204
|
+
ms: Date.now() - t0,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
return sent
|
|
208
|
+
} catch (err) {
|
|
209
|
+
logChild.error(err, {
|
|
210
|
+
event: 'error',
|
|
211
|
+
ms: Date.now() - t0,
|
|
212
|
+
})
|
|
213
|
+
throw err
|
|
145
214
|
}
|
|
146
215
|
}
|
|
147
216
|
|
|
148
217
|
/**
|
|
149
|
-
* Subscribes to a queue.
|
|
218
|
+
* Subscribes to a queue for incoming messages.
|
|
219
|
+
*
|
|
150
220
|
* @param {{
|
|
151
221
|
* queue: string,
|
|
152
|
-
* onReceive: (data: any) => Promise<void>,
|
|
222
|
+
* onReceive: (data: any, correlationId?: string) => Promise<void>,
|
|
153
223
|
* nackOnError?: boolean
|
|
154
224
|
* }} options
|
|
155
225
|
* @returns {Promise<void>}
|
|
@@ -166,7 +236,8 @@ export const initializeQueue = async ({ host, log }) => {
|
|
|
166
236
|
}
|
|
167
237
|
|
|
168
238
|
/**
|
|
169
|
-
* Builds RabbitMQ URI from environment variables.
|
|
239
|
+
* Builds a RabbitMQ URI string from environment variables.
|
|
240
|
+
*
|
|
170
241
|
* @param {{
|
|
171
242
|
* RABBIT_HOST: string,
|
|
172
243
|
* RABBIT_PORT: string | number,
|
|
@@ -14,25 +14,25 @@ import {
|
|
|
14
14
|
} from '../../src/ids/generators.js'
|
|
15
15
|
import { ID_PREFIXES } from '../../src/ids/prefixes.js'
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// ULID is a 26-character Base32 string (no I, L, O, U).
|
|
18
|
+
const ULID_REGEX = /^[0-9A-HJKMNP-TV-Z]{26}$/
|
|
19
19
|
|
|
20
20
|
const testPrefixFunction = (fn, expectedPrefix) => {
|
|
21
21
|
const id = fn()
|
|
22
22
|
expect(typeof id).toBe('string')
|
|
23
|
-
const [prefix,
|
|
23
|
+
const [prefix, ulid] = id.split('_')
|
|
24
24
|
expect(prefix).toBe(expectedPrefix)
|
|
25
|
-
expect(
|
|
25
|
+
expect(ulid).toMatch(ULID_REGEX)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
describe('generateId', () => {
|
|
29
|
-
it('generates a valid
|
|
29
|
+
it('generates a valid ULID', () => {
|
|
30
30
|
const id = generateId()
|
|
31
31
|
expect(typeof id).toBe('string')
|
|
32
|
-
expect(id).toMatch(
|
|
32
|
+
expect(id).toMatch(ULID_REGEX)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
it('generates unique
|
|
35
|
+
it('generates unique ULIDs', () => {
|
|
36
36
|
const ids = new Set(Array.from({ length: 10 }, () => generateId()))
|
|
37
37
|
expect(ids.size).toBe(10)
|
|
38
38
|
})
|
|
@@ -41,9 +41,9 @@ describe('generateId', () => {
|
|
|
41
41
|
describe('generatePrefixedId', () => {
|
|
42
42
|
it('generates an ID with the correct prefix', () => {
|
|
43
43
|
const prefixed = generatePrefixedId('test')
|
|
44
|
-
const [prefix,
|
|
44
|
+
const [prefix, ulid] = prefixed.split('_')
|
|
45
45
|
expect(prefix).toBe('test')
|
|
46
|
-
expect(
|
|
46
|
+
expect(ulid).toMatch(ULID_REGEX)
|
|
47
47
|
})
|
|
48
48
|
})
|
|
49
49
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import dot from 'dot'
|
|
2
1
|
import * as fs from 'fs/promises'
|
|
3
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
4
3
|
|
|
@@ -7,9 +6,6 @@ import { loadTemplates } from '../../src/templates/template-loader.js'
|
|
|
7
6
|
vi.mock('fs/promises')
|
|
8
7
|
|
|
9
8
|
describe('loadTemplates', () => {
|
|
10
|
-
// @ts-ignore
|
|
11
|
-
const mockCompile = vi.spyOn(dot, 'compile')
|
|
12
|
-
|
|
13
9
|
beforeEach(() => {
|
|
14
10
|
vi.resetAllMocks()
|
|
15
11
|
})
|