core-services-sdk 1.3.55 → 1.3.57
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/package.json +2 -1
- package/src/core/case-mapper.js +60 -0
- package/src/core/index.js +1 -0
- package/src/http/helpers/create-request-logger.js +38 -0
- package/src/http/index.js +1 -0
- package/src/util/context.js +64 -32
- package/tests/core/case-mapper.unit.test.js +111 -0
- package/tests/http/helpers/create-request-logger.test.js +113 -0
- package/types/core/case-mapper.d.ts +44 -0
- package/types/core/index.d.ts +1 -0
- package/types/http/helpers/create-request-logger.d.ts +4 -0
- package/types/http/index.d.ts +1 -0
- package/types/util/context.d.ts +93 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-services-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.57",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"google-libphonenumber": "^3.2.42",
|
|
35
35
|
"http-status": "^2.1.0",
|
|
36
36
|
"knex": "^3.1.0",
|
|
37
|
+
"lodash-es": "^4.17.21",
|
|
37
38
|
"mongodb": "^6.18.0",
|
|
38
39
|
"nodemailer": "^7.0.5",
|
|
39
40
|
"p-retry": "^7.0.0",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mapKeys, snakeCase, camelCase } from 'lodash-es'
|
|
2
|
+
import { sanitizeObjectAllowProps } from './sanitize-objects.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts object keys from camelCase to snake_case.
|
|
6
|
+
*
|
|
7
|
+
* Optionally restricts the conversion to a whitelist of allowed properties.
|
|
8
|
+
* If no allowed properties are provided, all object keys are converted.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} obj - The source object with camelCase keys.
|
|
11
|
+
* @param {...string|string[]} allowedProps - Optional list (or arrays) of allowed property names.
|
|
12
|
+
* @returns {Object} A new object with keys converted to snake_case.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* toSnakeCase({ userId: '1', createdAt: 'now' })
|
|
16
|
+
* // { user_id: '1', created_at: 'now' }
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* toSnakeCase({ userId: '1', createdAt: 'now' }, ['userId'])
|
|
20
|
+
* // { user_id: '1' }
|
|
21
|
+
*/
|
|
22
|
+
export function toSnakeCase(obj, ...allowedProps) {
|
|
23
|
+
const allowedPropsFixed = allowedProps.flat()
|
|
24
|
+
const objFiltered = allowedPropsFixed.length
|
|
25
|
+
? sanitizeObjectAllowProps(obj, allowedPropsFixed)
|
|
26
|
+
: obj
|
|
27
|
+
|
|
28
|
+
return mapKeys(objFiltered, (_value, key) => {
|
|
29
|
+
return snakeCase(key)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Converts object keys from snake_case to camelCase.
|
|
35
|
+
*
|
|
36
|
+
* Optionally restricts the conversion to a whitelist of allowed properties.
|
|
37
|
+
* If no allowed properties are provided, all object keys are converted.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} obj - The source object with snake_case keys.
|
|
40
|
+
* @param {...string|string[]} allowedProps - Optional list (or arrays) of allowed property names.
|
|
41
|
+
* @returns {Object} A new object with keys converted to camelCase.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* toCamelCase({ user_id: '1', created_at: 'now' })
|
|
45
|
+
* // { userId: '1', createdAt: 'now' }
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* toCamelCase({ user_id: '1', created_at: 'now' }, ['user_id'])
|
|
49
|
+
* // { userId: '1' }
|
|
50
|
+
*/
|
|
51
|
+
export function toCamelCase(obj, ...allowedProps) {
|
|
52
|
+
const allowedPropsFixed = allowedProps.flat()
|
|
53
|
+
const objFiltered = allowedPropsFixed.length
|
|
54
|
+
? sanitizeObjectAllowProps(obj, allowedPropsFixed)
|
|
55
|
+
: obj
|
|
56
|
+
|
|
57
|
+
return mapKeys(objFiltered, (_value, key) => {
|
|
58
|
+
return camelCase(key)
|
|
59
|
+
})
|
|
60
|
+
}
|
package/src/core/index.js
CHANGED
|
@@ -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/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
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { toSnakeCase, toCamelCase } from '../../src/core/case-mapper.js'
|
|
4
|
+
|
|
5
|
+
describe('toSnakeCase', () => {
|
|
6
|
+
it('converts all keys from camelCase to snake_case', () => {
|
|
7
|
+
const input = {
|
|
8
|
+
userId: '1',
|
|
9
|
+
createdAt: 'now',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const result = toSnakeCase(input)
|
|
13
|
+
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
user_id: '1',
|
|
16
|
+
created_at: 'now',
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('converts only allowed properties when allowedProps are provided', () => {
|
|
21
|
+
const input = {
|
|
22
|
+
userId: '1',
|
|
23
|
+
createdAt: 'now',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = toSnakeCase(input, ['userId'])
|
|
27
|
+
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
user_id: '1',
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('supports allowedProps passed as multiple arguments', () => {
|
|
34
|
+
const input = {
|
|
35
|
+
userId: '1',
|
|
36
|
+
createdAt: 'now',
|
|
37
|
+
updatedAt: 'later',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = toSnakeCase(input, 'userId', 'updatedAt')
|
|
41
|
+
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
user_id: '1',
|
|
44
|
+
updated_at: 'later',
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns an empty object when allowedProps do not match any keys', () => {
|
|
49
|
+
const input = {
|
|
50
|
+
userId: '1',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = toSnakeCase(input, ['nonExistingKey'])
|
|
54
|
+
|
|
55
|
+
expect(result).toEqual({})
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('toCamelCase', () => {
|
|
60
|
+
it('converts all keys from snake_case to camelCase', () => {
|
|
61
|
+
const input = {
|
|
62
|
+
user_id: '1',
|
|
63
|
+
created_at: 'now',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = toCamelCase(input)
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual({
|
|
69
|
+
userId: '1',
|
|
70
|
+
createdAt: 'now',
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('converts only allowed properties when allowedProps are provided', () => {
|
|
75
|
+
const input = {
|
|
76
|
+
user_id: '1',
|
|
77
|
+
created_at: 'now',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = toCamelCase(input, ['user_id'])
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
userId: '1',
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('supports allowedProps passed as multiple arguments', () => {
|
|
88
|
+
const input = {
|
|
89
|
+
user_id: '1',
|
|
90
|
+
created_at: 'now',
|
|
91
|
+
updated_at: 'later',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const result = toCamelCase(input, 'user_id', 'updated_at')
|
|
95
|
+
|
|
96
|
+
expect(result).toEqual({
|
|
97
|
+
userId: '1',
|
|
98
|
+
updatedAt: 'later',
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('returns an empty object when allowedProps do not match any keys', () => {
|
|
103
|
+
const input = {
|
|
104
|
+
user_id: '1',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = toCamelCase(input, ['non_existing_key'])
|
|
108
|
+
|
|
109
|
+
expect(result).toEqual({})
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { createRequestLogger } from '../../../src/http/helpers/create-request-logger.js'
|
|
5
|
+
import { Context } from '../../../src/util/context.js'
|
|
6
|
+
//@ts-ignore
|
|
7
|
+
describe('createRequestLogger', () => {
|
|
8
|
+
let baseLogger
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
baseLogger = {
|
|
12
|
+
child: vi.fn(),
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('creates a child logger with context and request data', () => {
|
|
17
|
+
const request = {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
url: '/test',
|
|
20
|
+
raw: { url: '/raw-test' },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const childLogger = {}
|
|
24
|
+
baseLogger.child.mockReturnValue(childLogger)
|
|
25
|
+
|
|
26
|
+
Context.run(
|
|
27
|
+
{
|
|
28
|
+
correlationId: 'corr-123',
|
|
29
|
+
ip: '127.0.0.1',
|
|
30
|
+
userAgent: 'vitest-agent',
|
|
31
|
+
},
|
|
32
|
+
() => {
|
|
33
|
+
const result = createRequestLogger(request, baseLogger)
|
|
34
|
+
|
|
35
|
+
expect(baseLogger.child).toHaveBeenCalledOnce()
|
|
36
|
+
expect(baseLogger.child).toHaveBeenCalledWith({
|
|
37
|
+
ip: '127.0.0.1',
|
|
38
|
+
userAgent: 'vitest-agent',
|
|
39
|
+
correlationId: 'corr-123',
|
|
40
|
+
op: 'GET /test',
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(result).toBe(childLogger)
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('falls back to routeOptions.url when request.url is missing', () => {
|
|
49
|
+
const request = {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
routeOptions: { url: '/route-url' },
|
|
52
|
+
raw: { url: '/raw-url' },
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Context.run(
|
|
56
|
+
{
|
|
57
|
+
correlationId: 'corr-456',
|
|
58
|
+
ip: '10.0.0.1',
|
|
59
|
+
userAgent: 'agent-x',
|
|
60
|
+
},
|
|
61
|
+
() => {
|
|
62
|
+
createRequestLogger(request, baseLogger)
|
|
63
|
+
|
|
64
|
+
expect(baseLogger.child).toHaveBeenCalledWith(
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
op: 'POST /route-url',
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('falls back to request.raw.url when neither url nor routeOptions.url exist', () => {
|
|
74
|
+
const request = {
|
|
75
|
+
method: 'PUT',
|
|
76
|
+
raw: { url: '/raw-only' },
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Context.run(
|
|
80
|
+
{
|
|
81
|
+
correlationId: 'corr-789',
|
|
82
|
+
ip: '192.168.1.1',
|
|
83
|
+
userAgent: 'agent-y',
|
|
84
|
+
},
|
|
85
|
+
() => {
|
|
86
|
+
createRequestLogger(request, baseLogger)
|
|
87
|
+
|
|
88
|
+
expect(baseLogger.child).toHaveBeenCalledWith(
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
op: 'PUT /raw-only',
|
|
91
|
+
}),
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('handles missing context gracefully when no Context is active', () => {
|
|
98
|
+
const request = {
|
|
99
|
+
method: 'DELETE',
|
|
100
|
+
url: '/delete',
|
|
101
|
+
raw: { url: '/delete' },
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
createRequestLogger(request, baseLogger)
|
|
105
|
+
|
|
106
|
+
expect(baseLogger.child).toHaveBeenCalledWith({
|
|
107
|
+
ip: undefined,
|
|
108
|
+
userAgent: undefined,
|
|
109
|
+
correlationId: undefined,
|
|
110
|
+
op: 'DELETE /delete',
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts object keys from camelCase to snake_case.
|
|
3
|
+
*
|
|
4
|
+
* Optionally restricts the conversion to a whitelist of allowed properties.
|
|
5
|
+
* If no allowed properties are provided, all object keys are converted.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} obj - The source object with camelCase keys.
|
|
8
|
+
* @param {...string|string[]} allowedProps - Optional list (or arrays) of allowed property names.
|
|
9
|
+
* @returns {Object} A new object with keys converted to snake_case.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* toSnakeCase({ userId: '1', createdAt: 'now' })
|
|
13
|
+
* // { user_id: '1', created_at: 'now' }
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* toSnakeCase({ userId: '1', createdAt: 'now' }, ['userId'])
|
|
17
|
+
* // { user_id: '1' }
|
|
18
|
+
*/
|
|
19
|
+
export function toSnakeCase(
|
|
20
|
+
obj: any,
|
|
21
|
+
...allowedProps: (string | string[])[]
|
|
22
|
+
): any
|
|
23
|
+
/**
|
|
24
|
+
* Converts object keys from snake_case to camelCase.
|
|
25
|
+
*
|
|
26
|
+
* Optionally restricts the conversion to a whitelist of allowed properties.
|
|
27
|
+
* If no allowed properties are provided, all object keys are converted.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} obj - The source object with snake_case keys.
|
|
30
|
+
* @param {...string|string[]} allowedProps - Optional list (or arrays) of allowed property names.
|
|
31
|
+
* @returns {Object} A new object with keys converted to camelCase.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* toCamelCase({ user_id: '1', created_at: 'now' })
|
|
35
|
+
* // { userId: '1', createdAt: 'now' }
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* toCamelCase({ user_id: '1', created_at: 'now' }, ['user_id'])
|
|
39
|
+
* // { userId: '1' }
|
|
40
|
+
*/
|
|
41
|
+
export function toCamelCase(
|
|
42
|
+
obj: any,
|
|
43
|
+
...allowedProps: (string | string[])[]
|
|
44
|
+
): any
|
package/types/core/index.d.ts
CHANGED
package/types/http/index.d.ts
CHANGED
package/types/util/context.d.ts
CHANGED
|
@@ -1,55 +1,124 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Represents the data stored in the async context for a single execution flow.
|
|
3
|
+
*
|
|
4
|
+
* This object is propagated automatically across async boundaries
|
|
5
|
+
* using Node.js AsyncLocalStorage.
|
|
6
|
+
*
|
|
7
|
+
* It defines the contract between producers (who set values)
|
|
8
|
+
* and consumers (who read values) of the context.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Object} ContextStore
|
|
11
|
+
*
|
|
12
|
+
* @property {string} [correlationId] - Unique identifier for request or operation tracing.
|
|
13
|
+
* @property {string} [ip] - Client IP address.
|
|
14
|
+
* @property {string} [userAgent] - Client user agent string.
|
|
15
|
+
* @property {string} [tenantId] - Active tenant identifier.
|
|
16
|
+
* @property {string} [userId] - Authenticated user identifier.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Async execution context manager built on top of Node.js AsyncLocalStorage.
|
|
20
|
+
*
|
|
21
|
+
* This class provides a thin, static API for storing and accessing
|
|
22
|
+
* request-scoped (or async-chain-scoped) metadata such as correlation IDs,
|
|
23
|
+
* user information, tenant identifiers, and similar data.
|
|
24
|
+
*
|
|
25
|
+
* The context is bound to the current async execution chain using
|
|
26
|
+
* AsyncLocalStorage and is automatically propagated across `await` boundaries.
|
|
27
|
+
*
|
|
28
|
+
* This class is intentionally static and acts as a singleton wrapper
|
|
29
|
+
* around AsyncLocalStorage.
|
|
30
|
+
*/
|
|
31
|
+
export class Context {
|
|
2
32
|
/**
|
|
3
|
-
* Run a callback within a given context store.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
33
|
+
* Run a callback within a given async context store.
|
|
34
|
+
*
|
|
35
|
+
* All asynchronous operations spawned inside the callback
|
|
36
|
+
* will have access to the provided store via {@link Context.get},
|
|
37
|
+
* {@link Context.set}, or {@link Context.all}.
|
|
6
38
|
*
|
|
7
39
|
* @template T
|
|
8
|
-
* @param {
|
|
40
|
+
* @param {ContextStore} store - Initial context store for this execution.
|
|
9
41
|
* @param {() => T} callback - Function to execute inside the context.
|
|
10
42
|
* @returns {T} The return value of the callback (sync or async).
|
|
11
43
|
*
|
|
12
44
|
* @example
|
|
13
45
|
* Context.run(
|
|
14
|
-
* { correlationId: 'abc123' },
|
|
46
|
+
* { correlationId: 'abc123', userId: 'usr_1' },
|
|
15
47
|
* async () => {
|
|
16
48
|
* console.log(Context.get('correlationId')) // "abc123"
|
|
17
49
|
* }
|
|
18
50
|
* )
|
|
19
51
|
*/
|
|
20
|
-
|
|
52
|
+
static run<T>(store: ContextStore, callback: () => T): T
|
|
21
53
|
/**
|
|
22
54
|
* Retrieve a single value from the current async context store.
|
|
23
55
|
*
|
|
24
|
-
* @
|
|
25
|
-
*
|
|
26
|
-
*
|
|
56
|
+
* If called outside of an active {@link Context.run},
|
|
57
|
+
* this method returns `undefined`.
|
|
58
|
+
*
|
|
59
|
+
* @template {keyof ContextStore} K
|
|
60
|
+
* @param {K} key - Context property name.
|
|
61
|
+
* @returns {ContextStore[K] | undefined} The stored value, if present.
|
|
27
62
|
*
|
|
28
63
|
* @example
|
|
29
|
-
* const
|
|
64
|
+
* const tenantId = Context.get('tenantId')
|
|
30
65
|
*/
|
|
31
|
-
|
|
66
|
+
static get<K extends keyof ContextStore>(key: K): ContextStore[K] | undefined
|
|
32
67
|
/**
|
|
33
|
-
* Set a single key-value pair
|
|
34
|
-
*
|
|
35
|
-
*
|
|
68
|
+
* Set a single key-value pair on the current async context store.
|
|
69
|
+
*
|
|
70
|
+
* If called outside of an active {@link Context.run},
|
|
71
|
+
* this method is a no-op.
|
|
36
72
|
*
|
|
37
|
-
* @
|
|
38
|
-
* @param {
|
|
73
|
+
* @template {keyof ContextStore} K
|
|
74
|
+
* @param {K} key - Context property name.
|
|
75
|
+
* @param {ContextStore[K]} value - Value to store.
|
|
39
76
|
*
|
|
40
77
|
* @example
|
|
41
78
|
* Context.set('tenantId', 'tnt_1234')
|
|
42
79
|
*/
|
|
43
|
-
|
|
80
|
+
static set<K extends keyof ContextStore>(key: K, value: ContextStore[K]): void
|
|
44
81
|
/**
|
|
45
|
-
* Get the
|
|
82
|
+
* Get the full context store for the current async execution.
|
|
46
83
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
84
|
+
* If no context is active, an empty object is returned.
|
|
85
|
+
*
|
|
86
|
+
* @returns {ContextStore}
|
|
49
87
|
*
|
|
50
88
|
* @example
|
|
51
|
-
* const
|
|
52
|
-
* console.log(
|
|
89
|
+
* const ctx = Context.all()
|
|
90
|
+
* console.log(ctx.correlationId)
|
|
91
|
+
*/
|
|
92
|
+
static all(): ContextStore
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Represents the data stored in the async context for a single execution flow.
|
|
96
|
+
*
|
|
97
|
+
* This object is propagated automatically across async boundaries
|
|
98
|
+
* using Node.js AsyncLocalStorage.
|
|
99
|
+
*
|
|
100
|
+
* It defines the contract between producers (who set values)
|
|
101
|
+
* and consumers (who read values) of the context.
|
|
102
|
+
*/
|
|
103
|
+
export type ContextStore = {
|
|
104
|
+
/**
|
|
105
|
+
* - Unique identifier for request or operation tracing.
|
|
106
|
+
*/
|
|
107
|
+
correlationId?: string
|
|
108
|
+
/**
|
|
109
|
+
* - Client IP address.
|
|
110
|
+
*/
|
|
111
|
+
ip?: string
|
|
112
|
+
/**
|
|
113
|
+
* - Client user agent string.
|
|
114
|
+
*/
|
|
115
|
+
userAgent?: string
|
|
116
|
+
/**
|
|
117
|
+
* - Active tenant identifier.
|
|
118
|
+
*/
|
|
119
|
+
tenantId?: string
|
|
120
|
+
/**
|
|
121
|
+
* - Authenticated user identifier.
|
|
53
122
|
*/
|
|
54
|
-
|
|
123
|
+
userId?: string
|
|
55
124
|
}
|