@evanion/nestjs-correlation-id 1.0.4 → 2.0.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/README.md +100 -34
  3. package/dist/constants.d.ts +16 -1
  4. package/dist/constants.d.ts.map +1 -0
  5. package/dist/constants.js +16 -5
  6. package/dist/correlation-id.middleware.d.ts +5 -4
  7. package/dist/correlation-id.middleware.d.ts.map +1 -0
  8. package/dist/correlation-id.middleware.js +35 -30
  9. package/dist/correlation.module.d.ts +2 -1
  10. package/dist/correlation.module.d.ts.map +1 -0
  11. package/dist/correlation.module.js +16 -19
  12. package/dist/correlation.service.d.ts +34 -3
  13. package/dist/correlation.service.d.ts.map +1 -0
  14. package/dist/correlation.service.js +49 -26
  15. package/dist/index.d.ts +7 -5
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +6 -21
  18. package/dist/interfaces/correlation-config.interface.d.ts +2 -0
  19. package/dist/interfaces/correlation-config.interface.d.ts.map +1 -0
  20. package/dist/interfaces/correlation-config.interface.js +1 -2
  21. package/dist/withCorrelation.function.d.ts +16 -74
  22. package/dist/withCorrelation.function.d.ts.map +1 -0
  23. package/dist/withCorrelation.function.js +41 -22
  24. package/package.json +76 -62
  25. package/src/constants.ts +19 -0
  26. package/src/correlation-id.middleware.ts +55 -0
  27. package/src/correlation.module.ts +32 -0
  28. package/src/correlation.service.ts +69 -0
  29. package/src/index.ts +6 -0
  30. package/src/interfaces/correlation-config.interface.ts +5 -0
  31. package/src/withCorrelation.function.ts +60 -0
  32. package/CONTRIBUTING.md +0 -31
  33. package/dist/correlation-id.middleware.spec.d.ts +0 -1
  34. package/dist/correlation-id.middleware.spec.js +0 -62
  35. package/dist/correlation.service.spec.d.ts +0 -1
  36. package/dist/correlation.service.spec.js +0 -35
package/dist/index.js CHANGED
@@ -1,21 +1,6 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./correlation-id.middleware"), exports);
18
- __exportStar(require("./correlation.service"), exports);
19
- __exportStar(require("./correlation.module"), exports);
20
- __exportStar(require("./withCorrelation.function"), exports);
21
- __exportStar(require("./constants"), exports);
1
+ export * from './correlation-id.middleware.js';
2
+ export * from './correlation.service.js';
3
+ export * from './correlation.module.js';
4
+ export * from './withCorrelation.function.js';
5
+ export * from './constants.js';
6
+ export * from './interfaces/correlation-config.interface.js';
@@ -1,4 +1,6 @@
1
1
  export interface CorrelationConfig {
2
2
  header: string;
3
3
  generator: () => string;
4
+ validate?: (value: string) => boolean;
4
5
  }
6
+ //# sourceMappingURL=correlation-config.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlation-config.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/correlation-config.interface.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACvC"}
@@ -1,2 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};
@@ -1,74 +1,16 @@
1
- import { HttpModuleOptions } from '@nestjs/axios';
2
- import { CorrelationModule } from './correlation.module';
3
- import { CorrelationService } from './correlation.service';
4
- export declare const withCorrelation: (config?: HttpModuleOptions) => {
5
- imports: (typeof CorrelationModule)[];
6
- useFactory: (correlationService: CorrelationService) => Promise<{
7
- headers: {
8
- "X-Correlation-Id": string;
9
- } | {
10
- "X-Correlation-Id": string;
11
- Accept?: import("axios").AxiosHeaderValue;
12
- "Content-Length"?: import("axios").AxiosHeaderValue;
13
- "User-Agent"?: import("axios").AxiosHeaderValue;
14
- "Content-Encoding"?: import("axios").AxiosHeaderValue;
15
- Authorization?: import("axios").AxiosHeaderValue;
16
- 'Content-Type'?: import("axios").AxiosHeaderValue;
17
- link?: import("axios").AxiosHeaders;
18
- head?: import("axios").AxiosHeaders;
19
- options?: import("axios").AxiosHeaders;
20
- get?: import("axios").AxiosHeaders;
21
- post?: import("axios").AxiosHeaders;
22
- put?: import("axios").AxiosHeaders;
23
- delete?: import("axios").AxiosHeaders;
24
- patch?: import("axios").AxiosHeaders;
25
- purge?: import("axios").AxiosHeaders;
26
- unlink?: import("axios").AxiosHeaders;
27
- common?: import("axios").AxiosHeaders;
28
- };
29
- url?: string;
30
- method?: string;
31
- baseURL?: string;
32
- transformRequest?: import("axios").AxiosRequestTransformer | import("axios").AxiosRequestTransformer[];
33
- transformResponse?: import("axios").AxiosResponseTransformer | import("axios").AxiosResponseTransformer[];
34
- params?: any;
35
- paramsSerializer?: import("axios").CustomParamsSerializer | import("axios").ParamsSerializerOptions;
36
- data?: any;
37
- timeout?: number;
38
- timeoutErrorMessage?: string;
39
- withCredentials?: boolean;
40
- adapter?: (string | import("axios").AxiosAdapter) | (string | import("axios").AxiosAdapter)[];
41
- auth?: import("axios").AxiosBasicCredentials;
42
- responseType?: import("axios").ResponseType;
43
- responseEncoding?: string;
44
- xsrfCookieName?: string;
45
- xsrfHeaderName?: string;
46
- onUploadProgress?: (progressEvent: import("axios").AxiosProgressEvent) => void;
47
- onDownloadProgress?: (progressEvent: import("axios").AxiosProgressEvent) => void;
48
- maxContentLength?: number;
49
- validateStatus?: (status: number) => boolean;
50
- maxBodyLength?: number;
51
- maxRedirects?: number;
52
- maxRate?: number | [number, number];
53
- beforeRedirect?: (options: Record<string, any>, responseDetails: {
54
- headers: Record<string, string>;
55
- }) => void;
56
- socketPath?: string;
57
- transport?: any;
58
- httpAgent?: any;
59
- httpsAgent?: any;
60
- proxy?: false | import("axios").AxiosProxyConfig;
61
- cancelToken?: import("axios").CancelToken;
62
- decompress?: boolean;
63
- transitional?: import("axios").TransitionalOptions;
64
- signal?: import("axios").GenericAbortSignal;
65
- insecureHTTPParser?: boolean;
66
- env?: {
67
- FormData?: new (...args: any[]) => object;
68
- };
69
- formSerializer?: import("axios").FormSerializerOptions;
70
- family?: 4 | 6;
71
- lookup?: ((hostname: string, options: object, cb: (err: Error, address: string, family: number) => void) => void) | ((hostname: string, options: object) => Promise<string | [address: string, family: number]>);
72
- }>;
73
- inject: (typeof CorrelationService)[];
74
- };
1
+ import type { HttpModuleAsyncOptions, HttpModuleOptions } from '@nestjs/axios';
2
+ /**
3
+ * Options for `HttpModule.registerAsync` that forward the current correlation
4
+ * id on every outgoing request.
5
+ *
6
+ * Requires `CorrelationModule.forRoot()` somewhere in the application. It is a
7
+ * global module, so importing it once in the root module is enough; without it
8
+ * Nest fails with `Nest can't resolve dependencies of the HTTP_MODULE_OPTIONS`.
9
+ *
10
+ * The id is read by an axios request interceptor, at the moment the request is
11
+ * made. It used to be baked into the options object by a factory injecting the
12
+ * then request-scoped `CorrelationService`, which made `HttpService` -- and
13
+ * every provider holding it -- request-scoped too.
14
+ */
15
+ export declare const withCorrelation: (config?: HttpModuleOptions) => HttpModuleAsyncOptions;
16
+ //# sourceMappingURL=withCorrelation.function.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withCorrelation.function.d.ts","sourceRoot":"","sources":["../src/withCorrelation.function.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,iBAAiB,EAClB,MAAM,eAAe,CAAC;AAiBvB;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,GAC1B,SAAS,iBAAiB,KACzB,sBAwBD,CAAC"}
@@ -1,23 +1,42 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.withCorrelation = void 0;
13
- const constants_1 = require("./constants");
14
- const correlation_module_1 = require("./correlation.module");
15
- const correlation_service_1 = require("./correlation.service");
16
- const withCorrelation = (config) => ({
17
- imports: [correlation_module_1.CorrelationModule],
18
- useFactory: (correlationService) => __awaiter(void 0, void 0, void 0, function* () {
19
- return (Object.assign(Object.assign({}, config), { headers: Object.assign(Object.assign({}, ((config === null || config === void 0 ? void 0 : config.headers) && config.headers)), { [constants_1.CORRELATION_ID_HEADER]: correlationService.getCorrelationId() }) }));
20
- }),
21
- inject: [correlation_service_1.CorrelationService],
1
+ import { CORRELATION_AXIOS_INTERCEPTOR, CORRELATION_CONFIG_TOKEN, } from './constants.js';
2
+ import { CorrelationService } from './correlation.service.js';
3
+ /**
4
+ * @nestjs/axios' own token for the axios instance HttpModule creates. It is a
5
+ * plain string and is not re-exported from the package root, so it is inlined
6
+ * here -- which also keeps @nestjs/axios a type-only import, and therefore
7
+ * genuinely optional at runtime.
8
+ */
9
+ const AXIOS_INSTANCE_TOKEN = 'AXIOS_INSTANCE_TOKEN';
10
+ /**
11
+ * Options for `HttpModule.registerAsync` that forward the current correlation
12
+ * id on every outgoing request.
13
+ *
14
+ * Requires `CorrelationModule.forRoot()` somewhere in the application. It is a
15
+ * global module, so importing it once in the root module is enough; without it
16
+ * Nest fails with `Nest can't resolve dependencies of the HTTP_MODULE_OPTIONS`.
17
+ *
18
+ * The id is read by an axios request interceptor, at the moment the request is
19
+ * made. It used to be baked into the options object by a factory injecting the
20
+ * then request-scoped `CorrelationService`, which made `HttpService` -- and
21
+ * every provider holding it -- request-scoped too.
22
+ */
23
+ export const withCorrelation = (config) => ({
24
+ useFactory: () => ({ ...config }),
25
+ extraProviders: [
26
+ {
27
+ provide: CORRELATION_AXIOS_INTERCEPTOR,
28
+ useFactory: (axiosInstance, correlationService, correlationConfig) => axiosInstance.interceptors.request.use((request) => {
29
+ const correlationId = correlationService.getCorrelationId();
30
+ if (correlationId !== undefined) {
31
+ request.headers.set(correlationConfig.header, correlationId);
32
+ }
33
+ return request;
34
+ }),
35
+ inject: [
36
+ AXIOS_INSTANCE_TOKEN,
37
+ CorrelationService,
38
+ CORRELATION_CONFIG_TOKEN,
39
+ ],
40
+ },
41
+ ],
22
42
  });
23
- exports.withCorrelation = withCorrelation;
package/package.json CHANGED
@@ -1,78 +1,92 @@
1
1
  {
2
2
  "name": "@evanion/nestjs-correlation-id",
3
- "version": "1.0.4",
4
- "description": "Transparently forward or add correlation id to all requests",
5
- "author": "Mikael Pettersson <evanion@icloud.com>",
6
- "license": "MIT",
7
- "readmeFilename": "README.md",
8
- "main": "dist/index.js",
9
- "files": [
10
- "dist/**/*",
11
- "*.md"
12
- ],
13
- "scripts": {
14
- "start:dev": "tsc -w",
15
- "build": "tsc",
16
- "prepare": "npm run build",
17
- "format": "prettier --write \"src/**/*.ts\"",
18
- "lint": "eslint \"src/**/*.ts\"",
19
- "lint:fix": "eslint --fix \"src/**/*.ts\"",
20
- "test": "jest",
21
- "test:watch": "jest --watch",
22
- "test:cov": "jest --coverage",
23
- "test:e2e": "jest --config ./test/jest-e2e.json",
24
- "release": "release-it"
25
- },
3
+ "version": "2.0.0",
4
+ "description": "Correlation ID middleware for NestJS. Propagates a request-scoped correlation id across incoming requests, outgoing HTTP calls and logs.",
26
5
  "keywords": [
27
6
  "nestjs",
28
- "nestjs-middleware",
29
- "middleware",
30
- "correlation",
31
7
  "correlation-id",
32
- "request",
33
- "request-id"
8
+ "request-id",
9
+ "tracing",
10
+ "middleware",
11
+ "observability",
12
+ "typescript"
34
13
  ],
35
- "publishConfig": {
36
- "access": "public"
14
+ "homepage": "https://github.com/Evanion/libraries/tree/main/nest/correlation-id#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/Evanion/libraries/issues"
37
17
  },
38
18
  "repository": {
39
19
  "type": "git",
40
- "url": "https://github.com/evanion/nestjs-correlation-id"
20
+ "url": "git+https://github.com/Evanion/libraries.git",
21
+ "directory": "nest/correlation-id"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Mikael Pettersson",
25
+ "sideEffects": false,
26
+ "type": "module",
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ "./package.json": "./package.json",
34
+ ".": {
35
+ "@evanion/source": "./src/index.ts",
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "!dist/**/*.tsbuildinfo",
43
+ "src",
44
+ "!src/**/*.test.*",
45
+ "!src/**/*.spec.*",
46
+ "!src/**/*.test-d.*",
47
+ "README.md",
48
+ "LICENSE",
49
+ "CHANGELOG.md"
50
+ ],
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "provenance": true
41
54
  },
42
- "bugs": "https://github.com/evanion/nestjs-correlation-id/issues",
43
55
  "peerDependencies": {
44
- "@nestjs/axios": "^0.1.0 || ^1.0.0 || ^2.0.0",
45
- "@nestjs/common": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
56
+ "@nestjs/axios": "^12.0.0",
57
+ "@nestjs/common": "^12.0.0"
46
58
  },
47
- "dependencies": {
48
- "uuid": "^9.0.0"
59
+ "peerDependenciesMeta": {
60
+ "@nestjs/axios": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "nx": {
65
+ "targets": {
66
+ "build": {
67
+ "executor": "nx:run-commands",
68
+ "outputs": [
69
+ "{projectRoot}/dist"
70
+ ],
71
+ "options": {
72
+ "cwd": "nest/correlation-id",
73
+ "command": "tsc --build tsconfig.lib.json"
74
+ }
75
+ }
76
+ }
49
77
  },
50
78
  "devDependencies": {
51
- "@nestjs/axios": "^2.0.0",
52
- "@nestjs/common": "^9.4.1",
53
- "@nestjs/core": "^9.4.1",
54
- "@nestjs/platform-express": "^9.4.1",
55
- "@nestjs/testing": "9.4.1",
56
- "@types/express": "4.17.17",
57
- "@types/jest": "29.5.1",
58
- "@types/supertest": "2.0.12",
59
- "@typescript-eslint/eslint-plugin": "^5.59.6",
60
- "@typescript-eslint/parser": "^5.59.6",
61
- "axios": "^1.4.0",
62
- "eslint": "^8.41.0",
63
- "eslint-config-prettier": "^8.8.0",
64
- "eslint-plugin-prettier": "^4.2.1",
65
- "jest": "29.5.0",
66
- "prettier": "^2.8.8",
67
- "reflect-metadata": "^0.1.13",
68
- "release-it": "^15.10.3",
69
- "rxjs": "^7.8.1",
70
- "supertest": "6.3.3",
71
- "ts-jest": "29.1.0",
72
- "ts-node": "10.9.1",
73
- "tsc-watch": "6.0.4",
74
- "tsconfig-paths": "4.2.0",
75
- "typescript": "5.0.4",
76
- "uuid": "^9.0.0"
79
+ "@nestjs/axios": "^12.0.0",
80
+ "@nestjs/common": "^12.0.1",
81
+ "@nestjs/core": "^12.0.1",
82
+ "@nestjs/platform-express": "^12.0.1",
83
+ "@nestjs/testing": "^12.0.1",
84
+ "@types/express": "^5.0.0",
85
+ "express": "^5.0.0",
86
+ "reflect-metadata": "^0.2.2",
87
+ "rxjs": "^7.8.2"
88
+ },
89
+ "dependencies": {
90
+ "tslib": "^2.8.1"
77
91
  }
78
92
  }
@@ -0,0 +1,19 @@
1
+ export const CORRELATION_ID_HEADER = 'X-Correlation-Id';
2
+ /**
3
+ * Namespaced because the provider is registered in a `global: true` module: a
4
+ * bare 'CORRELATION_CONFIG' would collide silently with any other package that
5
+ * happened to pick the same string. Deliberately a string and not a Symbol --
6
+ * a bare `Symbol()` is not stable across duplicate copies of a module, which
7
+ * would reintroduce exactly the hazard the single ESM build removes.
8
+ */
9
+ export const CORRELATION_CONFIG_TOKEN =
10
+ '@evanion/nestjs-correlation-id:CORRELATION_CONFIG';
11
+ /**
12
+ * Token of the provider that attaches the outgoing-request interceptor to the
13
+ * axios instance `withCorrelation` configures. Nothing injects it; it exists so
14
+ * Nest instantiates the factory that registers the interceptor.
15
+ */
16
+ export const CORRELATION_AXIOS_INTERCEPTOR =
17
+ '@evanion/nestjs-correlation-id:AXIOS_INTERCEPTOR';
18
+ export const DEFAULT_CORRELATION_ID_VALIDATOR = (value: string): boolean =>
19
+ /^[\w.:-]{1,128}$/.test(value);
@@ -0,0 +1,55 @@
1
+ import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
2
+ import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { CORRELATION_CONFIG_TOKEN, DEFAULT_CORRELATION_ID_VALIDATOR } from './constants.js';
4
+ import { CorrelationService } from './correlation.service.js';
5
+ // Must be `import type`: with isolatedModules and emitDecoratorMetadata,
6
+ // a type referenced in a decorated signature cannot be a value import.
7
+ import type { CorrelationConfig } from './interfaces/correlation-config.interface.js';
8
+
9
+ /**
10
+ * Node normalises repeated request headers into a single comma-joined string
11
+ * for everything except set-cookie, which stays an array. Joining an array the
12
+ * same way keeps both shapes on one code path -- and the default validator
13
+ * rejects the result, which is the wanted behaviour for a duplicated
14
+ * correlation id.
15
+ */
16
+ const singleValue = (
17
+ value: string | string[] | undefined,
18
+ ): string | undefined => (Array.isArray(value) ? value.join(', ') : value);
19
+
20
+ @Injectable()
21
+ export class CorrelationIdMiddleware implements NestMiddleware {
22
+ constructor(
23
+ private correlationService: CorrelationService,
24
+ @Inject(CORRELATION_CONFIG_TOKEN)
25
+ private correlationConfig: CorrelationConfig,
26
+ ) {}
27
+
28
+ use(
29
+ req: IncomingMessage,
30
+ res: ServerResponse,
31
+ next: (error?: unknown) => void,
32
+ ) {
33
+ const {
34
+ header,
35
+ validate = DEFAULT_CORRELATION_ID_VALIDATOR,
36
+ } = this.correlationConfig;
37
+ const key = header.toLowerCase();
38
+ const incoming = singleValue(req.headers[key]);
39
+ // The generator runs only when nothing usable arrived, so a counter- or
40
+ // sequence-backed generator is not advanced for an id that gets discarded.
41
+ const correlationId =
42
+ incoming && validate(incoming)
43
+ ? incoming
44
+ : this.correlationService.generate();
45
+
46
+ if (!req.headers[key]) req.headers[key] = correlationId;
47
+ // setHeader preserves the casing it is given, so the configured casing is
48
+ // what goes out on the wire.
49
+ if (res.getHeader(header) === undefined) res.setHeader(header, correlationId);
50
+
51
+ // Everything downstream of next() -- guards, interceptors, the controller,
52
+ // and anything they await -- runs inside this context.
53
+ this.correlationService.run(correlationId, next);
54
+ }
55
+ }
@@ -0,0 +1,32 @@
1
+ import { DynamicModule, Module, Provider } from '@nestjs/common';
2
+ import { randomUUID } from 'node:crypto';
3
+ import {
4
+ CORRELATION_CONFIG_TOKEN,
5
+ CORRELATION_ID_HEADER,
6
+ DEFAULT_CORRELATION_ID_VALIDATOR,
7
+ } from './constants.js';
8
+ import { CorrelationService } from './correlation.service.js';
9
+ // Must be `import type`: with isolatedModules and emitDecoratorMetadata,
10
+ // a type referenced in a decorated signature cannot be a value import.
11
+ import type { CorrelationConfig } from './interfaces/correlation-config.interface.js';
12
+
13
+ @Module({})
14
+ export class CorrelationModule {
15
+ static forRoot(config?: Partial<CorrelationConfig>): DynamicModule {
16
+ const correlationConfigProvider: Provider = {
17
+ provide: CORRELATION_CONFIG_TOKEN,
18
+ useValue: {
19
+ ...config,
20
+ header: config?.header || CORRELATION_ID_HEADER,
21
+ generator: config?.generator || randomUUID,
22
+ validate: config?.validate || DEFAULT_CORRELATION_ID_VALIDATOR,
23
+ },
24
+ };
25
+ return {
26
+ global: true,
27
+ module: CorrelationModule,
28
+ providers: [correlationConfigProvider, CorrelationService],
29
+ exports: [correlationConfigProvider, CorrelationService],
30
+ };
31
+ }
32
+ }
@@ -0,0 +1,69 @@
1
+ import { Inject, Injectable } from '@nestjs/common';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import { CORRELATION_CONFIG_TOKEN } from './constants.js';
4
+ // Must be `import type`: with isolatedModules and emitDecoratorMetadata,
5
+ // a type referenced in a decorated signature cannot be a value import.
6
+ import type { CorrelationConfig } from './interfaces/correlation-config.interface.js';
7
+
8
+ interface CorrelationStore {
9
+ correlationId: string;
10
+ }
11
+
12
+ /**
13
+ * A plain singleton over AsyncLocalStorage.
14
+ *
15
+ * It used to be `@Injectable({ scope: Scope.REQUEST })`. Nest propagates scope
16
+ * upward through the injection graph, so every provider that reached this one
17
+ * -- including `HttpService`, via `withCorrelation` -- silently became
18
+ * request-scoped: re-instantiated per request, and never given `onModuleInit`.
19
+ * AsyncLocalStorage gives the same per-request isolation with none of that.
20
+ */
21
+ @Injectable()
22
+ export class CorrelationService {
23
+ private readonly storage = new AsyncLocalStorage<CorrelationStore>();
24
+
25
+ constructor(
26
+ @Inject(CORRELATION_CONFIG_TOKEN)
27
+ private readonly correlationConfig: CorrelationConfig,
28
+ ) {}
29
+
30
+ /**
31
+ * Runs `callback` in a correlation context. Everything it awaits, schedules
32
+ * or calls sees `correlationId`, and overlapping contexts stay isolated.
33
+ *
34
+ * The middleware does this per request. Call it directly for work that has no
35
+ * request behind it -- queue consumers, cron jobs, scripts.
36
+ */
37
+ run<T>(correlationId: string, callback: () => T): T {
38
+ return this.storage.run({ correlationId }, callback);
39
+ }
40
+
41
+ /**
42
+ * The id of the surrounding correlation context, or `undefined` when there is
43
+ * none. Outside a context there is genuinely no correlation id, so this does
44
+ * not invent one.
45
+ */
46
+ getCorrelationId(): string | undefined {
47
+ return this.storage.getStore()?.correlationId;
48
+ }
49
+
50
+ /** Replaces the id of the surrounding correlation context. */
51
+ setCorrelationId(correlationId: string): void {
52
+ const store = this.storage.getStore();
53
+ if (!store) {
54
+ throw new Error(
55
+ 'setCorrelationId() was called outside a correlation context. Apply CorrelationIdMiddleware, or wrap the work in CorrelationService.run().',
56
+ );
57
+ }
58
+ store.correlationId = correlationId;
59
+ }
60
+
61
+ /**
62
+ * A fresh id from the configured generator. Called only when no usable id
63
+ * arrived with the request, so a counter- or sequence-backed generator is not
64
+ * advanced for ids that get discarded.
65
+ */
66
+ generate(): string {
67
+ return this.correlationConfig.generator();
68
+ }
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './correlation-id.middleware.js';
2
+ export * from './correlation.service.js';
3
+ export * from './correlation.module.js';
4
+ export * from './withCorrelation.function.js';
5
+ export * from './constants.js';
6
+ export * from './interfaces/correlation-config.interface.js';
@@ -0,0 +1,5 @@
1
+ export interface CorrelationConfig {
2
+ header: string;
3
+ generator: () => string;
4
+ validate?: (value: string) => boolean;
5
+ }
@@ -0,0 +1,60 @@
1
+ import type {
2
+ HttpModuleAsyncOptions,
3
+ HttpModuleOptions,
4
+ } from '@nestjs/axios';
5
+ import type { AxiosInstance } from 'axios';
6
+ import {
7
+ CORRELATION_AXIOS_INTERCEPTOR,
8
+ CORRELATION_CONFIG_TOKEN,
9
+ } from './constants.js';
10
+ import { CorrelationService } from './correlation.service.js';
11
+ import type { CorrelationConfig } from './interfaces/correlation-config.interface.js';
12
+
13
+ /**
14
+ * @nestjs/axios' own token for the axios instance HttpModule creates. It is a
15
+ * plain string and is not re-exported from the package root, so it is inlined
16
+ * here -- which also keeps @nestjs/axios a type-only import, and therefore
17
+ * genuinely optional at runtime.
18
+ */
19
+ const AXIOS_INSTANCE_TOKEN = 'AXIOS_INSTANCE_TOKEN';
20
+
21
+ /**
22
+ * Options for `HttpModule.registerAsync` that forward the current correlation
23
+ * id on every outgoing request.
24
+ *
25
+ * Requires `CorrelationModule.forRoot()` somewhere in the application. It is a
26
+ * global module, so importing it once in the root module is enough; without it
27
+ * Nest fails with `Nest can't resolve dependencies of the HTTP_MODULE_OPTIONS`.
28
+ *
29
+ * The id is read by an axios request interceptor, at the moment the request is
30
+ * made. It used to be baked into the options object by a factory injecting the
31
+ * then request-scoped `CorrelationService`, which made `HttpService` -- and
32
+ * every provider holding it -- request-scoped too.
33
+ */
34
+ export const withCorrelation = (
35
+ config?: HttpModuleOptions,
36
+ ): HttpModuleAsyncOptions => ({
37
+ useFactory: () => ({ ...config }),
38
+ extraProviders: [
39
+ {
40
+ provide: CORRELATION_AXIOS_INTERCEPTOR,
41
+ useFactory: (
42
+ axiosInstance: AxiosInstance,
43
+ correlationService: CorrelationService,
44
+ correlationConfig: CorrelationConfig,
45
+ ) =>
46
+ axiosInstance.interceptors.request.use((request) => {
47
+ const correlationId = correlationService.getCorrelationId();
48
+ if (correlationId !== undefined) {
49
+ request.headers.set(correlationConfig.header, correlationId);
50
+ }
51
+ return request;
52
+ }),
53
+ inject: [
54
+ AXIOS_INSTANCE_TOKEN,
55
+ CorrelationService,
56
+ CORRELATION_CONFIG_TOKEN,
57
+ ],
58
+ },
59
+ ],
60
+ });
package/CONTRIBUTING.md DELETED
@@ -1,31 +0,0 @@
1
- # Contributing
2
-
3
- 1. [Fork it](https://help.github.com/articles/fork-a-repo/)
4
- 2. Install dependencies (`npm install`)
5
- 3. Create your feature branch (`git checkout -b my-new-feature`)
6
- 4. Commit your changes (`git commit -am 'Added some feature'`)
7
- 5. Test your changes (`npm test`)
8
- 6. Push to the branch (`git push origin my-new-feature`)
9
- 7. [Create new Pull Request](https://help.github.com/articles/creating-a-pull-request/)
10
-
11
- ## Testing
12
-
13
- We use [Jest](https://github.com/facebook/jest) to write tests. Run our test suite with this command:
14
-
15
- ```
16
- npm test
17
- ```
18
-
19
- ## Code Style
20
-
21
- We use [Prettier](https://prettier.io/) and tslint to maintain code style and best practices.
22
- Please make sure your PR adheres to the guides by running:
23
-
24
- ```
25
- npm run format
26
- ```
27
-
28
- and
29
- ```
30
- npm run lint
31
- ```
@@ -1 +0,0 @@
1
- export {};