@cqrs-ddd/pipeline-correlation 0.5.0
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/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +32 -0
- package/dist/constants/correlation.constants.d.ts +5 -0
- package/dist/constants/correlation.constants.d.ts.map +1 -0
- package/dist/constants/correlation.constants.js +6 -0
- package/dist/constants/correlation.constants.js.map +1 -0
- package/dist/correlation.store.d.ts +174 -0
- package/dist/correlation.store.d.ts.map +1 -0
- package/dist/correlation.store.js +176 -0
- package/dist/correlation.store.js.map +1 -0
- package/dist/decorators/with-correlation.decorator.d.ts +184 -0
- package/dist/decorators/with-correlation.decorator.d.ts.map +1 -0
- package/dist/decorators/with-correlation.decorator.js +85 -0
- package/dist/decorators/with-correlation.decorator.js.map +1 -0
- package/dist/helpers/method-decorator.d.ts +13 -0
- package/dist/helpers/method-decorator.d.ts.map +1 -0
- package/dist/helpers/method-decorator.js +24 -0
- package/dist/helpers/method-decorator.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/middlewares/http-correlation.middleware.d.ts +25 -0
- package/dist/middlewares/http-correlation.middleware.d.ts.map +1 -0
- package/dist/middlewares/http-correlation.middleware.js +63 -0
- package/dist/middlewares/http-correlation.middleware.js.map +1 -0
- package/dist/options/correlation.options.d.ts +91 -0
- package/dist/options/correlation.options.d.ts.map +1 -0
- package/dist/options/correlation.options.js +10 -0
- package/dist/options/correlation.options.js.map +1 -0
- package/dist/types/safe-typing.d.ts +15 -0
- package/dist/types/safe-typing.d.ts.map +1 -0
- package/dist/types/safe-typing.js +6 -0
- package/dist/types/safe-typing.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @cqrs-ddd/pipeline-correlation
|
|
2
|
+
|
|
3
|
+
Correlation id propagation for any Node.js application: an HTTP middleware
|
|
4
|
+
(`httpCorrelation()`, for Node's `http` server, Express or Connect), the
|
|
5
|
+
`@WithCorrelation()` method decorator for queue and message consumers and cron jobs
|
|
6
|
+
(both TypeScript decorator modes), a gRPC extraction preset (`CorrelationFrom.grpc()`), and
|
|
7
|
+
`correlationSource` for `createPipeline({ sources })` of `@cqrs-ddd/pipeline`.
|
|
8
|
+
|
|
9
|
+
**Documentation:** [guide](https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-correlation/) · [API reference](https://aristoteliss.github.io/ddd-cqrs/api/cqrs-ddd/pipeline-correlation/) · [all packages](https://aristoteliss.github.io/ddd-cqrs/)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @cqrs-ddd/pipeline-correlation
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node.js 22.12 or later. Published as an ES module; a CommonJS application loads
|
|
18
|
+
it with `require()`.
|
|
19
|
+
|
|
20
|
+
## Example
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { getCorrelationId, httpCorrelation } from '@cqrs-ddd/pipeline-correlation';
|
|
24
|
+
|
|
25
|
+
app.use(httpCorrelation());
|
|
26
|
+
app.get('/ping', (_req, res) => res.send(getCorrelationId()));
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
Dual-licensed under **AGPLv3** and a **Commercial License**. See `LICENSE` and
|
|
32
|
+
`COMMERCIAL_LICENSE.txt`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation.constants.d.ts","sourceRoot":"","sources":["../../src/constants/correlation.constants.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,0BAA0B,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation.constants.js","sourceRoot":"","sources":["../../src/constants/correlation.constants.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE;;GAEG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The correlation id as a context source, for `createPipeline({ sources })`
|
|
3
|
+
* of `@cqrs-ddd/pipeline` and the job context registration of
|
|
4
|
+
* `@cqrs-ddd/pipeline-job-context`. Unlike {@link getCorrelationId}, `current()`
|
|
5
|
+
* generates nothing: it returns `undefined` when no id is set, so a pipeline
|
|
6
|
+
* generates one and runs with it, and {@link getCorrelationId} inside the
|
|
7
|
+
* handler returns that id. `create()` makes a new id, the one way this
|
|
8
|
+
* package, pipelines and jobs generate one. `accepts(id)` tells whether an id received from
|
|
9
|
+
* outside, such as in a job payload, is well formed: at most
|
|
10
|
+
* {@link DEFAULT_CORRELATION_ID_MAX_LENGTH} characters matching
|
|
11
|
+
* {@link DEFAULT_CORRELATION_ID_PATTERN}.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* createPipeline({ sources: { correlationId: correlationSource } });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare const correlationSource: {
|
|
19
|
+
readonly current: () => string | undefined;
|
|
20
|
+
readonly run: <T>(value: string | undefined, fn: () => T) => T;
|
|
21
|
+
readonly create: () => string;
|
|
22
|
+
readonly accepts: (id: string) => boolean;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Run a callback within a correlation context.
|
|
26
|
+
*
|
|
27
|
+
* Use this in **non-HTTP entry points** (queue workers, message consumers,
|
|
28
|
+
* WebSocket handlers, scheduled jobs) to propagate an external correlation ID
|
|
29
|
+
* into everything the callback runs, wrapped operations included.
|
|
30
|
+
*
|
|
31
|
+
* If `correlationId` is falsy (`undefined` or empty), the id is resolved via
|
|
32
|
+
* {@link getCorrelationId} (the current one, or a new one from
|
|
33
|
+
* `correlationSource.create()`), so
|
|
34
|
+
* `fn` always runs with a correlation id. A pipeline dispatched inside `fn`
|
|
35
|
+
* takes it as its `context.correlationId`.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* // BullMQ worker
|
|
40
|
+
* new Worker('emails', (job) =>
|
|
41
|
+
* runWithCorrelationId(job.data.correlationId, () => sendEmail(job.data)),
|
|
42
|
+
* );
|
|
43
|
+
*
|
|
44
|
+
* // RabbitMQ consumer (amqplib)
|
|
45
|
+
* channel.consume('users', (message) =>
|
|
46
|
+
* runWithCorrelationId(message.properties.correlationId, () =>
|
|
47
|
+
* onUserCreated(JSON.parse(message.content.toString())),
|
|
48
|
+
* ),
|
|
49
|
+
* );
|
|
50
|
+
*
|
|
51
|
+
* // Scheduled job: no incoming id, so a new one is made
|
|
52
|
+
* setInterval(() => runWithCorrelationId(undefined, () => hourlySync()), 3_600_000);
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @param correlationId - The correlation ID to propagate. If falsy, falls back to {@link getCorrelationId}.
|
|
56
|
+
* @param fn - The callback to execute within the correlation context.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runWithCorrelationId<T>(correlationId: string | undefined, fn: () => T): T;
|
|
59
|
+
/**
|
|
60
|
+
* Read the current correlation ID.
|
|
61
|
+
*
|
|
62
|
+
* It is the correlation id set by {@link runWithCorrelationId}, the HTTP
|
|
63
|
+
* middleware, or a running pipeline. Outside any, a new UUIDv7 is generated on
|
|
64
|
+
* each call, so it always returns a string.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* import { getCorrelationId } from '@cqrs-ddd/pipeline-correlation';
|
|
69
|
+
*
|
|
70
|
+
* class EmailWorker {
|
|
71
|
+
* @WithCorrelation()
|
|
72
|
+
* async process(job: Job) {
|
|
73
|
+
* console.log(`Processing with correlation: ${getCorrelationId()}`);
|
|
74
|
+
* await sendEmail(job.data);
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function getCorrelationId(): string;
|
|
80
|
+
/**
|
|
81
|
+
* Utility type that adds a `correlationId` field to any data shape.
|
|
82
|
+
*
|
|
83
|
+
* Use it to type BullMQ job data, RabbitMQ payloads, Kafka messages, etc.
|
|
84
|
+
* so that the correlation ID is part of the contract.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* interface WelcomeEmailJobData {
|
|
89
|
+
* userId: string;
|
|
90
|
+
* email: string;
|
|
91
|
+
* }
|
|
92
|
+
*
|
|
93
|
+
* // Queue typed as Queue<WithCorrelationId<WelcomeEmailJobData>>
|
|
94
|
+
* await queue.add('send', addCorrelationId({ userId, email }));
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export type WithCorrelationId<T = Record<string, unknown>> = T & {
|
|
98
|
+
correlationId: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Stamp the current correlation ID onto a data object.
|
|
102
|
+
*
|
|
103
|
+
* This is the **producer-side** counterpart to {@link WithCorrelation}:
|
|
104
|
+
* - `addCorrelationId(data)` → stamps the ID when **enqueuing** a job / publishing a message
|
|
105
|
+
* - `@WithCorrelation()` → extracts the ID when **processing** the job / handling the message
|
|
106
|
+
*
|
|
107
|
+
* Both default to the `'correlationId'` key, so they work together out of the box.
|
|
108
|
+
*
|
|
109
|
+
* @throws {TypeError} If `data` is not a plain object.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* // Bull / BullMQ
|
|
114
|
+
* await queue.add('send-email', addCorrelationId({ userId, email }));
|
|
115
|
+
*
|
|
116
|
+
* // RabbitMQ (ClientProxy)
|
|
117
|
+
* this.client.emit('user.created', addCorrelationId(payload));
|
|
118
|
+
*
|
|
119
|
+
* // Kafka
|
|
120
|
+
* await this.producer.send({
|
|
121
|
+
* topic: 'orders',
|
|
122
|
+
* messages: [{ value: JSON.stringify(addCorrelationId(order)) }],
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* // PostgreSQL NOTIFY
|
|
126
|
+
* await sql`SELECT pg_notify('events', ${JSON.stringify(addCorrelationId(data))})`;
|
|
127
|
+
*
|
|
128
|
+
* // ⚠️ Arrays must be wrapped — passing one directly throws:
|
|
129
|
+
* // addCorrelationId([item1, item2]); // ❌ TypeError
|
|
130
|
+
* addCorrelationId({ items: [item1, item2] }); // ✅
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* @param data - The payload to enrich. Must be a plain object, **not** an array.
|
|
134
|
+
* A shallow copy is returned; the original is not mutated.
|
|
135
|
+
* @returns A new object with `correlationId` added.
|
|
136
|
+
*/
|
|
137
|
+
export declare function addCorrelationId<T extends Record<string, unknown>>(data: T): WithCorrelationId<T>;
|
|
138
|
+
/**
|
|
139
|
+
* Return a headers object stamped with the current correlation ID.
|
|
140
|
+
*
|
|
141
|
+
* Use this for **header-based** transports (Kafka, NATS, gRPC metadata, HTTP)
|
|
142
|
+
* where the correlation ID belongs in message headers / metadata — **not** in
|
|
143
|
+
* the data payload.
|
|
144
|
+
*
|
|
145
|
+
* For transports without native headers (Bull/BullMQ, PostgreSQL NOTIFY), use
|
|
146
|
+
* {@link addCorrelationId} instead.
|
|
147
|
+
*
|
|
148
|
+
* @param key - Header name. Defaults to `'x-correlation-id'`.
|
|
149
|
+
* @returns `{ [key]: correlationId }` — spread into your transport's headers.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* // Kafka
|
|
154
|
+
* await producer.send({
|
|
155
|
+
* topic: 'orders',
|
|
156
|
+
* messages: [{ value: JSON.stringify(order), headers: correlationHeaders() }],
|
|
157
|
+
* });
|
|
158
|
+
*
|
|
159
|
+
* // RabbitMQ (amqplib) — AMQP has a first-class correlationId property:
|
|
160
|
+
* channel.publish(exchange, key, buffer, { correlationId: getCorrelationId() });
|
|
161
|
+
* // …or in headers:
|
|
162
|
+
* channel.publish(exchange, key, buffer, { headers: correlationHeaders() });
|
|
163
|
+
*
|
|
164
|
+
* // NATS
|
|
165
|
+
* const headers = nats.headers();
|
|
166
|
+
* Object.entries(correlationHeaders()).forEach(([k, v]) => headers.set(k, v));
|
|
167
|
+
* nc.publish(subject, payload, { headers });
|
|
168
|
+
*
|
|
169
|
+
* // HTTP (outgoing)
|
|
170
|
+
* await fetch(url, { headers: { ...correlationHeaders(), 'content-type': 'application/json' } });
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export declare function correlationHeaders(key?: string): Record<string, string>;
|
|
174
|
+
//# sourceMappingURL=correlation.store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation.store.d.ts","sourceRoot":"","sources":["../src/correlation.store.ts"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB;aAC5B,OAAO,QAAM,MAAM,GAAG,SAAS;aAC/B,GAAG,GAAG,CAAC,SAAS,MAAM,GAAG,SAAS,MAAM,MAAM,CAAC,KAAG,CAAC;aAEnD,MAAM,QAAM,MAAM;aAClB,OAAO,OAAO,MAAM,KAAG,OAAO;CAGtB,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,EAAE,EAAE,MAAM,CAAC,GACV,CAAC,CAEH;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG;IAC/D,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChE,IAAI,EAAE,CAAC,GACN,iBAAiB,CAAC,CAAC,CAAC,CAYtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,SAA6B,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAExB"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { uuidv7 } from '@cqrs-ddd/uuidv7';
|
|
4
|
+
import { DEFAULT_CORRELATION_HEADER } from './constants/correlation.constants.js';
|
|
5
|
+
import { DEFAULT_CORRELATION_ID_MAX_LENGTH, DEFAULT_CORRELATION_ID_PATTERN, } from './options/correlation.options.js';
|
|
6
|
+
const correlation = new AsyncLocalStorage();
|
|
7
|
+
/**
|
|
8
|
+
* The correlation id as a context source, for `createPipeline({ sources })`
|
|
9
|
+
* of `@cqrs-ddd/pipeline` and the job context registration of
|
|
10
|
+
* `@cqrs-ddd/pipeline-job-context`. Unlike {@link getCorrelationId}, `current()`
|
|
11
|
+
* generates nothing: it returns `undefined` when no id is set, so a pipeline
|
|
12
|
+
* generates one and runs with it, and {@link getCorrelationId} inside the
|
|
13
|
+
* handler returns that id. `create()` makes a new id, the one way this
|
|
14
|
+
* package, pipelines and jobs generate one. `accepts(id)` tells whether an id received from
|
|
15
|
+
* outside, such as in a job payload, is well formed: at most
|
|
16
|
+
* {@link DEFAULT_CORRELATION_ID_MAX_LENGTH} characters matching
|
|
17
|
+
* {@link DEFAULT_CORRELATION_ID_PATTERN}.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* createPipeline({ sources: { correlationId: correlationSource } });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const correlationSource = {
|
|
25
|
+
current: () => correlation.getStore(),
|
|
26
|
+
run: (value, fn) => correlation.run(value, fn),
|
|
27
|
+
create: () => uuidv7(),
|
|
28
|
+
accepts: (id) => id.length <= DEFAULT_CORRELATION_ID_MAX_LENGTH &&
|
|
29
|
+
DEFAULT_CORRELATION_ID_PATTERN.test(id),
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Run a callback within a correlation context.
|
|
33
|
+
*
|
|
34
|
+
* Use this in **non-HTTP entry points** (queue workers, message consumers,
|
|
35
|
+
* WebSocket handlers, scheduled jobs) to propagate an external correlation ID
|
|
36
|
+
* into everything the callback runs, wrapped operations included.
|
|
37
|
+
*
|
|
38
|
+
* If `correlationId` is falsy (`undefined` or empty), the id is resolved via
|
|
39
|
+
* {@link getCorrelationId} (the current one, or a new one from
|
|
40
|
+
* `correlationSource.create()`), so
|
|
41
|
+
* `fn` always runs with a correlation id. A pipeline dispatched inside `fn`
|
|
42
|
+
* takes it as its `context.correlationId`.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* // BullMQ worker
|
|
47
|
+
* new Worker('emails', (job) =>
|
|
48
|
+
* runWithCorrelationId(job.data.correlationId, () => sendEmail(job.data)),
|
|
49
|
+
* );
|
|
50
|
+
*
|
|
51
|
+
* // RabbitMQ consumer (amqplib)
|
|
52
|
+
* channel.consume('users', (message) =>
|
|
53
|
+
* runWithCorrelationId(message.properties.correlationId, () =>
|
|
54
|
+
* onUserCreated(JSON.parse(message.content.toString())),
|
|
55
|
+
* ),
|
|
56
|
+
* );
|
|
57
|
+
*
|
|
58
|
+
* // Scheduled job: no incoming id, so a new one is made
|
|
59
|
+
* setInterval(() => runWithCorrelationId(undefined, () => hourlySync()), 3_600_000);
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @param correlationId - The correlation ID to propagate. If falsy, falls back to {@link getCorrelationId}.
|
|
63
|
+
* @param fn - The callback to execute within the correlation context.
|
|
64
|
+
*/
|
|
65
|
+
export function runWithCorrelationId(correlationId, fn) {
|
|
66
|
+
return correlation.run(correlationId || getCorrelationId(), fn);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read the current correlation ID.
|
|
70
|
+
*
|
|
71
|
+
* It is the correlation id set by {@link runWithCorrelationId}, the HTTP
|
|
72
|
+
* middleware, or a running pipeline. Outside any, a new UUIDv7 is generated on
|
|
73
|
+
* each call, so it always returns a string.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { getCorrelationId } from '@cqrs-ddd/pipeline-correlation';
|
|
78
|
+
*
|
|
79
|
+
* class EmailWorker {
|
|
80
|
+
* @WithCorrelation()
|
|
81
|
+
* async process(job: Job) {
|
|
82
|
+
* console.log(`Processing with correlation: ${getCorrelationId()}`);
|
|
83
|
+
* await sendEmail(job.data);
|
|
84
|
+
* }
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function getCorrelationId() {
|
|
89
|
+
return correlation.getStore() || correlationSource.create();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Stamp the current correlation ID onto a data object.
|
|
93
|
+
*
|
|
94
|
+
* This is the **producer-side** counterpart to {@link WithCorrelation}:
|
|
95
|
+
* - `addCorrelationId(data)` → stamps the ID when **enqueuing** a job / publishing a message
|
|
96
|
+
* - `@WithCorrelation()` → extracts the ID when **processing** the job / handling the message
|
|
97
|
+
*
|
|
98
|
+
* Both default to the `'correlationId'` key, so they work together out of the box.
|
|
99
|
+
*
|
|
100
|
+
* @throws {TypeError} If `data` is not a plain object.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* // Bull / BullMQ
|
|
105
|
+
* await queue.add('send-email', addCorrelationId({ userId, email }));
|
|
106
|
+
*
|
|
107
|
+
* // RabbitMQ (ClientProxy)
|
|
108
|
+
* this.client.emit('user.created', addCorrelationId(payload));
|
|
109
|
+
*
|
|
110
|
+
* // Kafka
|
|
111
|
+
* await this.producer.send({
|
|
112
|
+
* topic: 'orders',
|
|
113
|
+
* messages: [{ value: JSON.stringify(addCorrelationId(order)) }],
|
|
114
|
+
* });
|
|
115
|
+
*
|
|
116
|
+
* // PostgreSQL NOTIFY
|
|
117
|
+
* await sql`SELECT pg_notify('events', ${JSON.stringify(addCorrelationId(data))})`;
|
|
118
|
+
*
|
|
119
|
+
* // ⚠️ Arrays must be wrapped — passing one directly throws:
|
|
120
|
+
* // addCorrelationId([item1, item2]); // ❌ TypeError
|
|
121
|
+
* addCorrelationId({ items: [item1, item2] }); // ✅
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @param data - The payload to enrich. Must be a plain object, **not** an array.
|
|
125
|
+
* A shallow copy is returned; the original is not mutated.
|
|
126
|
+
* @returns A new object with `correlationId` added.
|
|
127
|
+
*/
|
|
128
|
+
export function addCorrelationId(data) {
|
|
129
|
+
const prototype = data !== null && typeof data === 'object'
|
|
130
|
+
? Object.getPrototypeOf(data)
|
|
131
|
+
: undefined;
|
|
132
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
133
|
+
throw new TypeError('addCorrelationId(data) requires a plain object payload. ' +
|
|
134
|
+
'Wrap arrays and class instances in a plain object first.');
|
|
135
|
+
}
|
|
136
|
+
return { ...data, correlationId: getCorrelationId() };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Return a headers object stamped with the current correlation ID.
|
|
140
|
+
*
|
|
141
|
+
* Use this for **header-based** transports (Kafka, NATS, gRPC metadata, HTTP)
|
|
142
|
+
* where the correlation ID belongs in message headers / metadata — **not** in
|
|
143
|
+
* the data payload.
|
|
144
|
+
*
|
|
145
|
+
* For transports without native headers (Bull/BullMQ, PostgreSQL NOTIFY), use
|
|
146
|
+
* {@link addCorrelationId} instead.
|
|
147
|
+
*
|
|
148
|
+
* @param key - Header name. Defaults to `'x-correlation-id'`.
|
|
149
|
+
* @returns `{ [key]: correlationId }` — spread into your transport's headers.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* // Kafka
|
|
154
|
+
* await producer.send({
|
|
155
|
+
* topic: 'orders',
|
|
156
|
+
* messages: [{ value: JSON.stringify(order), headers: correlationHeaders() }],
|
|
157
|
+
* });
|
|
158
|
+
*
|
|
159
|
+
* // RabbitMQ (amqplib) — AMQP has a first-class correlationId property:
|
|
160
|
+
* channel.publish(exchange, key, buffer, { correlationId: getCorrelationId() });
|
|
161
|
+
* // …or in headers:
|
|
162
|
+
* channel.publish(exchange, key, buffer, { headers: correlationHeaders() });
|
|
163
|
+
*
|
|
164
|
+
* // NATS
|
|
165
|
+
* const headers = nats.headers();
|
|
166
|
+
* Object.entries(correlationHeaders()).forEach(([k, v]) => headers.set(k, v));
|
|
167
|
+
* nc.publish(subject, payload, { headers });
|
|
168
|
+
*
|
|
169
|
+
* // HTTP (outgoing)
|
|
170
|
+
* await fetch(url, { headers: { ...correlationHeaders(), 'content-type': 'application/json' } });
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export function correlationHeaders(key = DEFAULT_CORRELATION_HEADER) {
|
|
174
|
+
return { [key]: getCorrelationId() };
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=correlation.store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation.store.js","sourceRoot":"","sources":["../src/correlation.store.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAClF,OAAO,EACL,iCAAiC,EACjC,8BAA8B,GAC/B,MAAM,kCAAkC,CAAC;AAE1C,MAAM,WAAW,GAAG,IAAI,iBAAiB,EAAsB,CAAC;AAEhE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,OAAO,EAAE,GAAuB,EAAE,CAAC,WAAW,CAAC,QAAQ,EAAE;IACzD,GAAG,EAAE,CAAI,KAAyB,EAAE,EAAW,EAAK,EAAE,CACpD,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC;IAC5B,MAAM,EAAE,GAAW,EAAE,CAAC,MAAM,EAAE;IAC9B,OAAO,EAAE,CAAC,EAAU,EAAW,EAAE,CAC/B,EAAE,CAAC,MAAM,IAAI,iCAAiC;QAC9C,8BAA8B,CAAC,IAAI,CAAC,EAAE,CAAC;CACjC,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,oBAAoB,CAClC,aAAiC,EACjC,EAAW;IAEX,OAAO,WAAW,CAAC,GAAG,CAAC,aAAa,IAAI,gBAAgB,EAAE,EAAE,EAAE,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,WAAW,CAAC,QAAQ,EAAE,IAAI,iBAAiB,CAAC,MAAM,EAAE,CAAC;AAC9D,CAAC;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAO;IAEP,MAAM,SAAS,GACb,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QACvC,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC;QAC7B,CAAC,CAAC,SAAS,CAAC;IAChB,IAAI,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACzD,MAAM,IAAI,SAAS,CACjB,0DAA0D;YACxD,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,GAAG,IAAI,EAAE,aAAa,EAAE,gBAAgB,EAAE,EAAE,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAG,GAAG,0BAA0B;IAEhC,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,gBAAgB,EAAE,EAAE,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { type DualMethodDecorator } from '../helpers/method-decorator.js';
|
|
2
|
+
/**
|
|
3
|
+
* A function that extracts the correlation ID from the method arguments.
|
|
4
|
+
*
|
|
5
|
+
* Receives the same arguments the decorated method receives.
|
|
6
|
+
* Return `undefined` to keep the current id, or get a new one from
|
|
7
|
+
* `correlationSource.create()` when there is none.
|
|
8
|
+
*/
|
|
9
|
+
export type CorrelationExtractor = (...args: unknown[]) => string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Options for the {@link WithCorrelation} decorator.
|
|
12
|
+
*/
|
|
13
|
+
export interface CorrelationDecoratorOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Property path (dot-notation) to the correlation ID in the **first argument**.
|
|
16
|
+
*
|
|
17
|
+
* - For Bull: `'data.correlationId'` (default)
|
|
18
|
+
* - For a flat object: `'correlationId'`
|
|
19
|
+
* - For deeply nested: `'metadata.tracing.correlationId'`
|
|
20
|
+
*
|
|
21
|
+
* Ignored when `extract` is provided.
|
|
22
|
+
*
|
|
23
|
+
* @default 'data.correlationId'
|
|
24
|
+
*/
|
|
25
|
+
path?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Custom extractor function. Receives all method arguments.
|
|
28
|
+
* Takes precedence over `path`.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // RabbitMQ
|
|
33
|
+
* extract: (data, ctx) => ctx.getMessage().properties.correlationId
|
|
34
|
+
*
|
|
35
|
+
* // Kafka
|
|
36
|
+
* extract: (data, ctx) =>
|
|
37
|
+
* ctx.getMessage().headers?.['x-correlation-id']?.toString()
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
extract?: CorrelationExtractor;
|
|
41
|
+
/**
|
|
42
|
+
* Log level for the start of the method execution.
|
|
43
|
+
*
|
|
44
|
+
* - Any {@link LogLevel} value routes to the logger method of that name (`'log'`, `'debug'`, `'verbose'`, `'warn'`, `'error'`).
|
|
45
|
+
* - `'none'` suppresses the log message entirely.
|
|
46
|
+
*
|
|
47
|
+
* A pino-backed logger maps the levels as `'verbose'` → `trace`, `'debug'` → `debug`,
|
|
48
|
+
* `'log'` → `info`, `'warn'` → `warn`, `'error'` → `error`, `'fatal'` → `fatal`.
|
|
49
|
+
*
|
|
50
|
+
* @default 'debug'
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* // Uses default 'debug' level
|
|
55
|
+
* @WithCorrelation()
|
|
56
|
+
*
|
|
57
|
+
* // Log at 'verbose' level
|
|
58
|
+
* @WithCorrelation({ logLevel: 'verbose' })
|
|
59
|
+
*
|
|
60
|
+
* // Suppress the log message
|
|
61
|
+
* @WithCorrelation({ logLevel: 'none' })
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
logLevel?: LogLevel | 'none';
|
|
65
|
+
/**
|
|
66
|
+
* Logger to write through instead of the decorated instance's `logger` property or
|
|
67
|
+
* `console`.
|
|
68
|
+
*
|
|
69
|
+
* Accepts any object with the methods of {@link CorrelationLogger}
|
|
70
|
+
* (e.g. a Pino or Winston adapter).
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* @WithCorrelation({ logger: myPinoLogger })
|
|
75
|
+
* async handleJob(job: Job) { ... }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
logger?: CorrelationLogger;
|
|
79
|
+
}
|
|
80
|
+
/** The levels `@WithCorrelation` logs at, named as NestJS names them. */
|
|
81
|
+
export type LogLevel = 'log' | 'error' | 'warn' | 'debug' | 'verbose' | 'fatal';
|
|
82
|
+
/**
|
|
83
|
+
* The logger `@WithCorrelation` writes through; `console`, a NestJS `LoggerService` and
|
|
84
|
+
* pino or winston adapters satisfy it.
|
|
85
|
+
*/
|
|
86
|
+
export interface CorrelationLogger {
|
|
87
|
+
log(message: unknown, ...optionalParams: unknown[]): unknown;
|
|
88
|
+
warn(message: unknown, ...optionalParams: unknown[]): unknown;
|
|
89
|
+
debug?(message: unknown, ...optionalParams: unknown[]): unknown;
|
|
90
|
+
verbose?(message: unknown, ...optionalParams: unknown[]): unknown;
|
|
91
|
+
error?(message: unknown, ...optionalParams: unknown[]): unknown;
|
|
92
|
+
fatal?(message: unknown, ...optionalParams: unknown[]): unknown;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Queue-agnostic `MethodDecorator` that propagates a correlation ID into the
|
|
96
|
+
* pipeline's async-local correlation context.
|
|
97
|
+
*
|
|
98
|
+
* Place it **under** any transport or scheduling decorator a framework adds. It does
|
|
99
|
+
* not replace or interfere with them.
|
|
100
|
+
*
|
|
101
|
+
* It extracts the correlation ID from the method arguments (via `path` or
|
|
102
|
+
* `extract`) and runs the method inside {@link runWithCorrelationId}, which
|
|
103
|
+
* keeps the current id, or makes one with `correlationSource.create()`, when
|
|
104
|
+
* no ID is found.
|
|
105
|
+
*
|
|
106
|
+
* **⚠️ Array payloads:**
|
|
107
|
+
* When using the default dot-path extraction, the first argument must be an
|
|
108
|
+
* object (e.g. a Bull `Job`). If it is an array, the dot-path cannot resolve
|
|
109
|
+
* and the decorator logs a warning at runtime. For transports that
|
|
110
|
+
* deliver an array as the first argument, use the `extract` option:
|
|
111
|
+
* ```ts
|
|
112
|
+
* @WithCorrelation({ extract: (items) => items?.[0]?.correlationId })
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* Inside the method body, read the active ID with:
|
|
116
|
+
* ```ts
|
|
117
|
+
* import { getCorrelationId } from '@cqrs-ddd/pipeline-correlation';
|
|
118
|
+
* const id = getCorrelationId();
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* class Consumers {
|
|
124
|
+
* // BullMQ job (default path: data.correlationId)
|
|
125
|
+
* @WithCorrelation()
|
|
126
|
+
* async sendEmail(job: Job) {
|
|
127
|
+
* await mailer.send(job.data);
|
|
128
|
+
* }
|
|
129
|
+
*
|
|
130
|
+
* // Another key in the job data
|
|
131
|
+
* @WithCorrelation('data.x-request-id')
|
|
132
|
+
* async sendSms(job: Job) {}
|
|
133
|
+
*
|
|
134
|
+
* // RabbitMQ message (amqplib)
|
|
135
|
+
* @WithCorrelation({ extract: (message) => message.properties.correlationId })
|
|
136
|
+
* async onUserCreated(message: ConsumeMessage) {}
|
|
137
|
+
*
|
|
138
|
+
* // Kafka message (kafkajs)
|
|
139
|
+
* @WithCorrelation({
|
|
140
|
+
* extract: ({ message }) => message.headers?.['x-correlation-id']?.toString(),
|
|
141
|
+
* })
|
|
142
|
+
* async onOrderPlaced(payload: EachMessagePayload) {}
|
|
143
|
+
*
|
|
144
|
+
* // PostgreSQL LISTEN/NOTIFY
|
|
145
|
+
* @WithCorrelation({ path: 'correlationId' })
|
|
146
|
+
* async onNotification(notification: { correlationId?: string }) {}
|
|
147
|
+
*
|
|
148
|
+
* // Scheduled job: no id in the arguments, so a new one is made
|
|
149
|
+
* @WithCorrelation()
|
|
150
|
+
* async hourlySync() {}
|
|
151
|
+
* }
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export declare function WithCorrelation(): DualMethodDecorator;
|
|
155
|
+
export declare function WithCorrelation(path: string): DualMethodDecorator;
|
|
156
|
+
export declare function WithCorrelation(options: CorrelationDecoratorOptions): DualMethodDecorator;
|
|
157
|
+
/**
|
|
158
|
+
* Pre-built extraction presets for {@link WithCorrelation}, for transports whose
|
|
159
|
+
* handlers receive the correlation id in metadata rather than in the payload.
|
|
160
|
+
*
|
|
161
|
+
* For transports that carry it in the payload (BullMQ, PostgreSQL NOTIFY), use
|
|
162
|
+
* the bare `@WithCorrelation()` or a `path`; for any other shape, an `extract`
|
|
163
|
+
* function.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* class Users {
|
|
168
|
+
* // gRPC (@grpc/grpc-js): (request, metadata)
|
|
169
|
+
* @WithCorrelation(CorrelationFrom.grpc())
|
|
170
|
+
* async findOne(request: FindOneRequest, metadata: Metadata) {}
|
|
171
|
+
* }
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export declare const CorrelationFrom: {
|
|
175
|
+
/**
|
|
176
|
+
* gRPC — extracts from gRPC `Metadata` (second argument).
|
|
177
|
+
*
|
|
178
|
+
* In gRPC handlers the signature is `(data, metadata, call)`.
|
|
179
|
+
*
|
|
180
|
+
* @param key - Metadata key. Defaults to `'x-correlation-id'`.
|
|
181
|
+
*/
|
|
182
|
+
readonly grpc: (key?: string) => CorrelationDecoratorOptions;
|
|
183
|
+
};
|
|
184
|
+
//# sourceMappingURL=with-correlation.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-correlation.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/with-correlation.decorator.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,gCAAgC,CAAC;AAGxC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAE/B;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,QAAQ,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAE7B;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED,yEAAyE;AACzE,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhF;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAC9D,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAChE,OAAO,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAClE,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAChE,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;CACjE;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,eAAe,IAAI,mBAAmB,CAAC;AACvD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAAC;AACnE,wBAAgB,eAAe,CAC7B,OAAO,EAAE,2BAA2B,GACnC,mBAAmB,CAAC;AA+CvB;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;;;OAMG;qCACuC,2BAA2B;CAM7D,CAAC"}
|