core-services-sdk 1.3.21 → 1.3.23
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/.vscode/settings.json +1 -1
- package/package.json +11 -7
- package/src/ids/generators.js +13 -11
- package/src/ids/prefixes.js +2 -2
- package/src/index.js +1 -0
- package/src/rabbit-mq/rabbit-mq.js +131 -62
- package/src/util/index.js +12 -0
- package/src/util/mask-sensitive.js +78 -0
- package/tests/ids/generators.unit.test.js +9 -9
- package/tests/rabbit-mq/rabbit-mq.test.js +5 -0
- package/tests/util/mask-sensitive.unit.test.js +79 -0
- package/tsconfig.json +9 -0
- package/types/core/combine-unique-arrays.d.ts +1 -0
- package/types/core/index.d.ts +7 -0
- package/types/core/normalize-min-max.d.ts +10 -0
- package/types/core/normalize-phone-number.d.ts +48 -0
- package/types/core/normalize-to-array.d.ts +1 -0
- package/types/core/otp-generators.d.ts +56 -0
- package/types/core/regex-utils.d.ts +1 -0
- package/types/core/sanitize-objects.d.ts +4 -0
- package/types/crypto/crypto.d.ts +18 -0
- package/types/crypto/encryption.d.ts +6 -0
- package/types/crypto/index.d.ts +2 -0
- package/types/fastify/error-codes.d.ts +29 -0
- package/types/fastify/error-handlers/with-error-handling.d.ts +15 -0
- package/types/fastify/index.d.ts +2 -0
- package/types/http/HttpError.d.ts +82 -0
- package/types/http/http-method.d.ts +7 -0
- package/types/http/http.d.ts +36 -0
- package/types/http/index.d.ts +4 -0
- package/types/http/responseType.d.ts +20 -0
- package/types/ids/generators.d.ts +10 -0
- package/types/ids/index.d.ts +2 -0
- package/types/ids/prefixes.d.ts +34 -0
- package/types/index.d.ts +11 -0
- package/types/logger/get-logger.d.ts +23 -0
- package/types/logger/index.d.ts +1 -0
- package/types/mailer/index.d.ts +2 -0
- package/types/mailer/mailer.service.d.ts +21 -0
- package/types/mailer/transport.factory.d.ts +48 -0
- package/types/mongodb/connect.d.ts +4 -0
- package/types/mongodb/index.d.ts +3 -0
- package/types/mongodb/initialize-mongodb.d.ts +13 -0
- package/types/mongodb/validate-mongo-uri.d.ts +15 -0
- package/types/rabbit-mq/index.d.ts +1 -0
- package/types/rabbit-mq/rabbit-mq.d.ts +40 -0
- package/types/templates/index.d.ts +1 -0
- package/types/templates/template-loader.d.ts +3 -0
- package/types/util/index.d.ts +7 -0
- package/types/util/mask-sensitive.d.ts +2 -0
- package/vitest.config.js +13 -3
package/.vscode/settings.json
CHANGED
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-services-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.23",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"types": "types/index.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"lint": "eslint .",
|
|
8
9
|
"lint:fix": "eslint . --fix",
|
|
9
10
|
"test": "vitest run --coverage",
|
|
10
11
|
"format": "prettier --write .",
|
|
11
|
-
"bump": "node ./scripts/bump-version.js"
|
|
12
|
+
"bump": "node ./scripts/bump-version.js",
|
|
13
|
+
"build:types": "tsc --declaration --allowJs --emitDeclarationOnly --outDir types ./src/index.js"
|
|
12
14
|
},
|
|
13
15
|
"repository": {
|
|
14
16
|
"type": "git",
|
|
@@ -34,17 +36,19 @@
|
|
|
34
36
|
"mongodb": "^6.18.0",
|
|
35
37
|
"nodemailer": "^7.0.5",
|
|
36
38
|
"pino": "^9.7.0",
|
|
39
|
+
"ulid": "^3.0.1",
|
|
37
40
|
"uuid": "^11.1.0",
|
|
38
41
|
"xml2js": "^0.6.2"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@vitest/coverage-v8": "^3.2.4",
|
|
42
|
-
"path": "^0.12.7",
|
|
43
|
-
"url": "^0.11.4",
|
|
44
|
-
"vitest": "^3.2.4",
|
|
45
45
|
"eslint": "^9.30.0",
|
|
46
46
|
"eslint-config-prettier": "^10.1.5",
|
|
47
47
|
"eslint-plugin-prettier": "^5.5.1",
|
|
48
|
-
"
|
|
48
|
+
"path": "^0.12.7",
|
|
49
|
+
"prettier": "^3.6.2",
|
|
50
|
+
"typescript": "^5.9.2",
|
|
51
|
+
"url": "^0.11.4",
|
|
52
|
+
"vitest": "^3.2.4"
|
|
49
53
|
}
|
|
50
|
-
}
|
|
54
|
+
}
|
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}
|
package/src/index.js
CHANGED
|
@@ -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,31 +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
114
|
if (!msgInfo) {
|
|
79
115
|
return
|
|
80
116
|
}
|
|
117
|
+
const t0 = Date.now()
|
|
118
|
+
|
|
119
|
+
const { msgId, data, correlationId } = parseMessage(msgInfo)
|
|
120
|
+
const child = logger.child({ msgId, correlationId })
|
|
81
121
|
|
|
82
122
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
123
|
+
child.debug('start')
|
|
124
|
+
child.info('message-received')
|
|
125
|
+
|
|
126
|
+
await onReceive(data, correlationId)
|
|
86
127
|
channel.ack(msgInfo)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
|
|
129
|
+
child.info({
|
|
130
|
+
event: 'ok',
|
|
131
|
+
ms: Date.now() - t0,
|
|
132
|
+
})
|
|
133
|
+
return
|
|
134
|
+
} catch (err) {
|
|
91
135
|
nackOnError ? channel.nack(msgInfo) : channel.ack(msgInfo)
|
|
136
|
+
|
|
137
|
+
child.error(err, {
|
|
138
|
+
event: 'error',
|
|
139
|
+
ms: Date.now() - t0,
|
|
140
|
+
})
|
|
141
|
+
return
|
|
92
142
|
}
|
|
93
143
|
})
|
|
94
|
-
} catch (
|
|
95
|
-
|
|
96
|
-
|
|
144
|
+
} catch (err) {
|
|
145
|
+
logger.error(err, {
|
|
146
|
+
event: 'error',
|
|
147
|
+
})
|
|
148
|
+
throw err
|
|
97
149
|
}
|
|
98
150
|
}
|
|
99
151
|
|
|
@@ -101,57 +153,73 @@ export const subscribeToQueue = async ({
|
|
|
101
153
|
* Initializes RabbitMQ integration with publish and subscribe support.
|
|
102
154
|
*
|
|
103
155
|
* @param {Object} options
|
|
104
|
-
* @param {string} options.host - RabbitMQ connection URI
|
|
105
|
-
* @param {
|
|
156
|
+
* @param {string} options.host - RabbitMQ connection URI
|
|
157
|
+
* @param {import('pino').Logger} options.log - Logger
|
|
106
158
|
*
|
|
107
159
|
* @returns {Promise<{
|
|
108
|
-
* publish: (queue: string, data: any) => Promise<boolean>,
|
|
160
|
+
* publish: (queue: string, data: any, correlationId?: string) => Promise<boolean>,
|
|
109
161
|
* subscribe: (options: {
|
|
110
162
|
* queue: string,
|
|
111
|
-
* onReceive: (data: any) => Promise<void>,
|
|
163
|
+
* onReceive: (data: any, correlationId?: string) => Promise<void>,
|
|
112
164
|
* nackOnError?: boolean
|
|
113
165
|
* }) => Promise<void>,
|
|
114
166
|
* channel: amqp.Channel
|
|
115
167
|
* }>}
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* const rabbit = await initializeQueue({ host, log });
|
|
119
|
-
* await rabbit.publish('jobs', { task: 'sendEmail' });
|
|
120
|
-
* await rabbit.subscribe({
|
|
121
|
-
* queue: 'jobs',
|
|
122
|
-
* onReceive: async (data) => { console.log(data); },
|
|
123
|
-
* });
|
|
124
168
|
*/
|
|
125
169
|
export const initializeQueue = async ({ host, log }) => {
|
|
126
|
-
const channel = await createChannel({ host })
|
|
170
|
+
const channel = await createChannel({ host, log })
|
|
171
|
+
const logger = log.child({ op: 'initializeQueue' })
|
|
127
172
|
|
|
128
173
|
/**
|
|
129
|
-
* Publishes a message to a queue.
|
|
130
|
-
*
|
|
131
|
-
* @param {
|
|
132
|
-
* @
|
|
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
|
|
133
180
|
*/
|
|
134
|
-
const publish = async (queue, data) => {
|
|
135
|
-
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
|
+
|
|
136
191
|
try {
|
|
192
|
+
logChild.debug('start')
|
|
193
|
+
|
|
137
194
|
await channel.assertQueue(queue, { durable: true })
|
|
138
|
-
|
|
139
|
-
|
|
195
|
+
const payload = { msgId, data, correlationId }
|
|
196
|
+
const sent = channel.sendToQueue(
|
|
140
197
|
queue,
|
|
141
|
-
Buffer.from(JSON.stringify(
|
|
198
|
+
Buffer.from(JSON.stringify(payload)),
|
|
142
199
|
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
147
214
|
}
|
|
148
215
|
}
|
|
149
216
|
|
|
150
217
|
/**
|
|
151
|
-
* Subscribes to a queue.
|
|
218
|
+
* Subscribes to a queue for incoming messages.
|
|
219
|
+
*
|
|
152
220
|
* @param {{
|
|
153
221
|
* queue: string,
|
|
154
|
-
* onReceive: (data: any) => Promise<void>,
|
|
222
|
+
* onReceive: (data: any, correlationId?: string) => Promise<void>,
|
|
155
223
|
* nackOnError?: boolean
|
|
156
224
|
* }} options
|
|
157
225
|
* @returns {Promise<void>}
|
|
@@ -168,7 +236,8 @@ export const initializeQueue = async ({ host, log }) => {
|
|
|
168
236
|
}
|
|
169
237
|
|
|
170
238
|
/**
|
|
171
|
-
* Builds RabbitMQ URI from environment variables.
|
|
239
|
+
* Builds a RabbitMQ URI string from environment variables.
|
|
240
|
+
*
|
|
172
241
|
* @param {{
|
|
173
242
|
* RABBIT_HOST: string,
|
|
174
243
|
* RABBIT_PORT: string | number,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mask middle of a primitive value while keeping left/right edges.
|
|
3
|
+
* @param {string|number|boolean|null|undefined} value
|
|
4
|
+
* @param {string} [fill='•']
|
|
5
|
+
* @param {number} [maskLen=3]
|
|
6
|
+
* @param {number} [left=4]
|
|
7
|
+
* @param {number} [right=4]
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export const maskSingle = (
|
|
11
|
+
value,
|
|
12
|
+
fill = '•',
|
|
13
|
+
maskLen = 3,
|
|
14
|
+
left = 4,
|
|
15
|
+
right = 4,
|
|
16
|
+
) => {
|
|
17
|
+
if (value == null) {
|
|
18
|
+
return ''
|
|
19
|
+
}
|
|
20
|
+
const str = String(value)
|
|
21
|
+
if (str.length === 0) {
|
|
22
|
+
return ''
|
|
23
|
+
}
|
|
24
|
+
const m = Math.max(1, maskLen)
|
|
25
|
+
|
|
26
|
+
if (str.length <= left + right) {
|
|
27
|
+
if (str.length === 1) {
|
|
28
|
+
return fill
|
|
29
|
+
}
|
|
30
|
+
if (str.length === 2) {
|
|
31
|
+
return str[0] + fill.repeat(2) // "ab" -> "a••"
|
|
32
|
+
}
|
|
33
|
+
return str.slice(0, 1) + fill.repeat(m) + str.slice(-1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return str.slice(0, left) + fill.repeat(m) + str.slice(-right)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Recursively mask values in strings, numbers, booleans, arrays, and objects.
|
|
41
|
+
* @param {string|number|boolean|Array|Object|null|undefined} value
|
|
42
|
+
* @param {string} [fill='•']
|
|
43
|
+
* @param {number} [maskLen=3]
|
|
44
|
+
* @param {number} [left=4]
|
|
45
|
+
* @param {number} [right=4]
|
|
46
|
+
* @returns {string|Array|Object}
|
|
47
|
+
*/
|
|
48
|
+
export const mask = (value, fill = '•', maskLen = 3, left = 4, right = 4) => {
|
|
49
|
+
const type = typeof value
|
|
50
|
+
|
|
51
|
+
if (value instanceof Date) {
|
|
52
|
+
const isoDate = value.toISOString()
|
|
53
|
+
return maskSingle(isoDate, '', isoDate.length, 0, 0)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (value == null || (value && ['string', 'number'].includes(type))) {
|
|
57
|
+
return maskSingle(value, fill, maskLen, left, right)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (type === 'boolean') {
|
|
61
|
+
return maskSingle(value, '', 0, 0, 0)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
return value.map((aValue) => mask(aValue, fill, maskLen, left, right))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof value === 'object') {
|
|
69
|
+
return Object.fromEntries(
|
|
70
|
+
Object.entries(value).map(([prop, propValue]) => [
|
|
71
|
+
prop,
|
|
72
|
+
mask(propValue, fill, maskLen, left, right),
|
|
73
|
+
]),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return value
|
|
78
|
+
}
|
|
@@ -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
|
|
|
@@ -7,6 +7,10 @@ const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
|
|
|
7
7
|
const testLog = {
|
|
8
8
|
info: () => {},
|
|
9
9
|
error: console.error,
|
|
10
|
+
debug: console.debug,
|
|
11
|
+
child() {
|
|
12
|
+
return testLog
|
|
13
|
+
},
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
describe('RabbitMQ SDK', () => {
|
|
@@ -18,6 +22,7 @@ describe('RabbitMQ SDK', () => {
|
|
|
18
22
|
let received = null
|
|
19
23
|
|
|
20
24
|
beforeAll(async () => {
|
|
25
|
+
// @ts-ignore
|
|
21
26
|
sdk = await initializeQueue({ host, log: testLog })
|
|
22
27
|
|
|
23
28
|
await sdk.subscribe({
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { mask, maskSingle } from '../../src/util/index.js'
|
|
4
|
+
|
|
5
|
+
describe('maskSingle', () => {
|
|
6
|
+
it('masks middle of a regular string (length == left+right)', () => {
|
|
7
|
+
expect(maskSingle('abcdefgh')).toBe('a•••h')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('masks numbers with default settings', () => {
|
|
11
|
+
expect(maskSingle(12345678)).toBe('1•••8')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('masks booleans', () => {
|
|
15
|
+
expect(maskSingle(true)).toBe('t•••e')
|
|
16
|
+
expect(maskSingle(false)).toBe('f•••e')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('masks very short strings correctly', () => {
|
|
20
|
+
expect(maskSingle('ab')).toBe('a••b'.slice(0, 3)) // will produce 'a••'
|
|
21
|
+
expect(maskSingle('a')).toBe('•')
|
|
22
|
+
expect(maskSingle('')).toBe('')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('respects custom fill and mask length', () => {
|
|
26
|
+
expect(maskSingle('abcdefgh', '*', 5)).toBe('a*****h')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('ensures maskLen is at least 1', () => {
|
|
30
|
+
expect(maskSingle('abcdefgh', '*', 0)).toBe('a*h')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns empty string for null/undefined', () => {
|
|
34
|
+
expect(maskSingle(null)).toBe('')
|
|
35
|
+
expect(maskSingle(undefined)).toBe('')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('mask', () => {
|
|
40
|
+
it('masks primitives (string, number, boolean)', () => {
|
|
41
|
+
expect(mask('abcdefgh')).toBe('a•••h')
|
|
42
|
+
expect(mask(12345678)).toBe('1•••8')
|
|
43
|
+
expect(mask(true)).toBe('true')
|
|
44
|
+
expect(mask(false)).toBe('false')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns empty string for null/undefined', () => {
|
|
48
|
+
expect(mask(null)).toBe('')
|
|
49
|
+
expect(mask(undefined)).toBe('')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('masks arrays recursively', () => {
|
|
53
|
+
expect(mask(['abcdefgh', 12345678])).toEqual(['a•••h', '1•••8'])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('masks objects recursively', () => {
|
|
57
|
+
expect(mask({ a: 'abcdefgh', b: 12345678 })).toEqual({
|
|
58
|
+
a: 'a•••h',
|
|
59
|
+
b: '1•••8',
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('masks nested objects/arrays recursively', () => {
|
|
64
|
+
const input = { arr: ['abcdefgh', { num: 12345678 }] }
|
|
65
|
+
const expected = { arr: ['a•••h', { num: '1•••8' }] }
|
|
66
|
+
expect(mask(input)).toEqual(expected)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('handles Date instances by returning full ISO string', () => {
|
|
70
|
+
const d = new Date('2025-08-15T12:34:56.789Z')
|
|
71
|
+
expect(mask(d)).toBe(d.toISOString())
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('respects custom fill and mask length in recursive calls', () => {
|
|
75
|
+
const input = { val: 'abcdefgh' }
|
|
76
|
+
const expected = { val: 'a*****h' }
|
|
77
|
+
expect(mask(input, '*', 5)).toEqual(expected)
|
|
78
|
+
})
|
|
79
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function combineUniqueArrays(...lists: Array<any>[]): Array<any>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./regex-utils.js";
|
|
2
|
+
export * from "./otp-generators.js";
|
|
3
|
+
export * from "./sanitize-objects.js";
|
|
4
|
+
export * from "./normalize-min-max.js";
|
|
5
|
+
export * from "./normalize-to-array.js";
|
|
6
|
+
export * from "./combine-unique-arrays.js";
|
|
7
|
+
export * from "./normalize-phone-number.js";
|