core-services-sdk 1.3.56 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.56",
3
+ "version": "1.3.57",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -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
@@ -2,3 +2,4 @@ export * from './http.js'
2
2
  export * from './HttpError.js'
3
3
  export * from './http-method.js'
4
4
  export * from './responseType.js'
5
+ export * from './helpers/create-request-logger.js'
@@ -3,79 +3,111 @@ import { AsyncLocalStorage } from 'node:async_hooks'
3
3
  const als = new AsyncLocalStorage()
4
4
 
5
5
  /**
6
- * Context utility built on top of Node.js AsyncLocalStorage.
7
- * Provides a per-request (or per-async-chain) storage mechanism that
8
- * allows passing metadata (like correlation IDs, user info, tenant ID, etc.)
9
- * without explicitly threading it through every function call.
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
- export const Context = {
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
- * Everything `await`ed or invoked inside this callback will have access
16
- * to the provided store via {@link Context.get}, {@link Context.set}, or {@link Context.all}.
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 {Record<string, any>} store
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
- * @template T
39
- * @param {string} key - The key of the value to retrieve.
40
- * @returns {T|undefined} The stored value, or `undefined` if no store exists or key not found.
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 userId = Context.get('userId')
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 in the current async context store.
52
- * If there is no active store (i.e. outside of a {@link Context.run}),
53
- * this function does nothing.
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
- * @param {string} key - The key under which to store the value.
56
- * @param {any} value - The value to store.
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 entire store object for the current async context.
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 {Record<string, any>} The current store object,
72
- * or an empty object if no store exists.
104
+ * @returns {ContextStore}
73
105
  *
74
106
  * @example
75
- * const all = Context.all()
76
- * console.log(all) // { correlationId: 'abc123', userId: 'usr_789' }
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,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,4 @@
1
+ export function createRequestLogger(
2
+ request: import('fastify').FastifyRequest,
3
+ log: import('pino').Logger,
4
+ ): import('pino').Logger
@@ -2,3 +2,4 @@ export * from './http.js'
2
2
  export * from './HttpError.js'
3
3
  export * from './http-method.js'
4
4
  export * from './responseType.js'
5
+ export * from './helpers/create-request-logger.js'
@@ -1,55 +1,124 @@
1
- export namespace Context {
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
- * Everything `await`ed or invoked inside this callback will have access
5
- * to the provided store via {@link Context.get}, {@link Context.set}, or {@link Context.all}.
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 {Record<string, any>} store
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
- function run<T>(store: Record<string, any>, callback: () => T): T
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
- * @template T
25
- * @param {string} key - The key of the value to retrieve.
26
- * @returns {T|undefined} The stored value, or `undefined` if no store exists or key not found.
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 userId = Context.get('userId')
64
+ * const tenantId = Context.get('tenantId')
30
65
  */
31
- function get<T>(key: string): T | undefined
66
+ static get<K extends keyof ContextStore>(key: K): ContextStore[K] | undefined
32
67
  /**
33
- * Set a single key-value pair in the current async context store.
34
- * If there is no active store (i.e. outside of a {@link Context.run}),
35
- * this function does nothing.
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
- * @param {string} key - The key under which to store the value.
38
- * @param {any} value - The value to store.
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
- function set(key: string, value: any): void
80
+ static set<K extends keyof ContextStore>(key: K, value: ContextStore[K]): void
44
81
  /**
45
- * Get the entire store object for the current async context.
82
+ * Get the full context store for the current async execution.
46
83
  *
47
- * @returns {Record<string, any>} The current store object,
48
- * or an empty object if no store exists.
84
+ * If no context is active, an empty object is returned.
85
+ *
86
+ * @returns {ContextStore}
49
87
  *
50
88
  * @example
51
- * const all = Context.all()
52
- * console.log(all) // { correlationId: 'abc123', userId: 'usr_789' }
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
- function all(): Record<string, any>
123
+ userId?: string
55
124
  }