@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.
- package/CHANGELOG.md +59 -1
- package/README.md +100 -34
- package/dist/constants.d.ts +16 -1
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +16 -5
- package/dist/correlation-id.middleware.d.ts +5 -4
- package/dist/correlation-id.middleware.d.ts.map +1 -0
- package/dist/correlation-id.middleware.js +35 -30
- package/dist/correlation.module.d.ts +2 -1
- package/dist/correlation.module.d.ts.map +1 -0
- package/dist/correlation.module.js +16 -19
- package/dist/correlation.service.d.ts +34 -3
- package/dist/correlation.service.d.ts.map +1 -0
- package/dist/correlation.service.js +49 -26
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -21
- package/dist/interfaces/correlation-config.interface.d.ts +2 -0
- package/dist/interfaces/correlation-config.interface.d.ts.map +1 -0
- package/dist/interfaces/correlation-config.interface.js +1 -2
- package/dist/withCorrelation.function.d.ts +16 -74
- package/dist/withCorrelation.function.d.ts.map +1 -0
- package/dist/withCorrelation.function.js +41 -22
- package/package.json +76 -62
- package/src/constants.ts +19 -0
- package/src/correlation-id.middleware.ts +55 -0
- package/src/correlation.module.ts +32 -0
- package/src/correlation.service.ts +69 -0
- package/src/index.ts +6 -0
- package/src/interfaces/correlation-config.interface.ts +5 -0
- package/src/withCorrelation.function.ts +60 -0
- package/CONTRIBUTING.md +0 -31
- package/dist/correlation-id.middleware.spec.d.ts +0 -1
- package/dist/correlation-id.middleware.spec.js +0 -62
- package/dist/correlation.service.spec.d.ts +0 -1
- package/dist/correlation.service.spec.js +0 -35
package/dist/index.js
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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';
|
|
@@ -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
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
|
@@ -1,74 +1,16 @@
|
|
|
1
|
-
import { HttpModuleOptions } from '@nestjs/axios';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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": "
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
8
|
+
"request-id",
|
|
9
|
+
"tracing",
|
|
10
|
+
"middleware",
|
|
11
|
+
"observability",
|
|
12
|
+
"typescript"
|
|
34
13
|
],
|
|
35
|
-
"
|
|
36
|
-
|
|
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/
|
|
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": "^
|
|
45
|
-
"@nestjs/common": "^
|
|
56
|
+
"@nestjs/axios": "^12.0.0",
|
|
57
|
+
"@nestjs/common": "^12.0.0"
|
|
46
58
|
},
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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": "^
|
|
52
|
-
"@nestjs/common": "^
|
|
53
|
-
"@nestjs/core": "^
|
|
54
|
-
"@nestjs/platform-express": "^
|
|
55
|
-
"@nestjs/testing": "
|
|
56
|
-
"@types/express": "
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"
|
|
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
|
}
|
package/src/constants.ts
ADDED
|
@@ -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,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 {};
|