├── .eslintrc.js
├── .github
├── dependabot.yml
└── workflows
│ ├── release.yml
│ └── validate-pr.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── .release-it.js
├── .vscode
└── settings.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── jest.config.js
├── nest-cli.json
├── package.json
├── sonar-project.properties
├── src
├── constants.ts
├── correlation-id.middleware.spec.ts
├── correlation-id.middleware.ts
├── correlation.module.ts
├── correlation.service.spec.ts
├── correlation.service.ts
├── index.ts
├── interfaces
│ └── correlation-config.interface.ts
└── withCorrelation.function.ts
├── test
├── correlation.e2e-spec.ts
├── jest-e2e.json
└── test-module
│ ├── test.controller.ts
│ ├── test.module.ts
│ └── test.service.ts
├── tsconfig.build.json
├── tsconfig.e2e.json
├── tsconfig.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir: __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: [
19 | '.eslintrc.js',
20 | 'node_modules/',
21 | 'coverage/',
22 | 'dist/',
23 | 'jest.config.js',
24 | ],
25 | rules: {
26 | '@typescript-eslint/interface-name-prefix': 'off',
27 | '@typescript-eslint/explicit-function-return-type': 'off',
28 | '@typescript-eslint/explicit-module-boundary-types': 'off',
29 | '@typescript-eslint/no-explicit-any': 'off',
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | release:
10 | name: Release
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v3
16 | with:
17 | node-version: 16
18 | cache: 'yarn'
19 | cache-dependency-path: yarn.lock
20 | - run: yarn install --frozen-lockfile --silent
21 | - uses: JS-DevTools/npm-publish@v1
22 | with:
23 | token: ${{ secrets.NPM_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/validate-pr.yml:
--------------------------------------------------------------------------------
1 | name: Validate PR
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | types: [opened, synchronize, reopened]
11 |
12 | permissions:
13 | pull-requests: read
14 |
15 | jobs:
16 | Lint:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: actions/setup-node@v3
21 | with:
22 | node-version: 16
23 | cache: 'yarn'
24 | cache-dependency-path: yarn.lock
25 | - run: yarn install --frozen-lockfile --silent
26 | - run: yarn lint
27 | Build:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - uses: actions/checkout@v3
31 | - uses: actions/setup-node@v3
32 | with:
33 | node-version: 16
34 | cache: 'yarn'
35 | cache-dependency-path: yarn.lock
36 | - run: yarn install --frozen-lockfile --silent
37 | - run: yarn build
38 | Test:
39 | runs-on: ubuntu-latest
40 | steps:
41 | - uses: actions/checkout@v3
42 | - uses: actions/setup-node@v3
43 | with:
44 | node-version: 16
45 | cache: 'yarn'
46 | cache-dependency-path: yarn.lock
47 | - run: yarn install --frozen-lockfile --silent
48 | - run: yarn test:cov --ci
49 | - run: yarn test:e2e --ci
50 | - name: SonarCloud Scan
51 | uses: SonarSource/sonarcloud-github-action@master
52 | env:
53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 | SONAR_TOKEN: ${{ secrets. SONAR_TOKEN }}
55 | with:
56 | args: -Dsonar.projectKey=Evanion_nestjs-correlation-id
57 | -Dsonar.organization=evanion
58 | -Dsonar.projectVersion=${{github.run_number}}
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github
2 | .vscode
3 | src
4 | test
5 | .eslintrc.js
6 | .gitignore
7 | .npmignore
8 | .prettierrc
9 | .release-it.json
10 | jest.config.js
11 | nest-cli.json
12 | sonar-project.properties
13 | tsconfig.json
14 | tsconfig.build.json
15 | tsconfig.e2e.json
16 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/.release-it.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | git: {
3 | commitMessage: 'v${version}',
4 | tagName: 'v${version}',
5 | },
6 | github: {
7 | release: true,
8 | releaseName: 'v${version}',
9 | tokenRef: 'GITHUB_AUTH',
10 | },
11 | npm: {
12 | publish: false,
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "**/.git": true,
4 | "**/.svn": true,
5 | "**/.hg": true,
6 | "**/CVS": true,
7 | "**/.DS_Store": true,
8 | "**/Thumbs.db": true,
9 | "**/node_modules": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | RELEASE 1.0.0
2 |
3 | - Initial release
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
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 | ```
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Mikael Pettersson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Nest.js Correlation ID middleware
2 |
3 | Transparently include correlation IDs in all requests
4 |
5 |
10 |
11 | ### Why?
12 |
13 | When debugging an issue in your applications logs, it helps to be able to follow a specific request up and down your whole stack. This is usually done by including a `correlation-id` (aka `Request-id`) header in all your requests, and forwarding the same id across all your microservices.
14 |
15 | ### Installation
16 |
17 | ```bash
18 | yarn add @evanion/nestjs-correlation-id
19 | ```
20 |
21 | ```bash
22 | npm install @evanion/nestjs-correlation-id
23 | ```
24 |
25 | ### How to use
26 |
27 | Add the middleware to your `AppModule`
28 |
29 | ```ts
30 | import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
31 | import {
32 | CorrelationIdMiddleware,
33 | CorrelationModule,
34 | } from '@evanion/nestjs-correlation-id';
35 |
36 | @Module({
37 | imports: [CorrelationModule.forRoot()],
38 | })
39 | export class AppModule implements NestModule {
40 | configure(consumer: MiddlewareConsumer) {
41 | consumer.apply(CorrelationIdMiddleware).forRoutes('*');
42 | }
43 | }
44 | ```
45 |
46 | And then just inject the correlation middleware in your HttpService by calling the `registerAsync` method with the `withCorrelation` function.
47 |
48 | ```ts
49 | import { HttpModule } from '@nestjs/axios';
50 | import { withCorrelation } from '@evanion/nestjs-correlation-id';
51 |
52 | @Module({
53 | imports: [HttpModule.registerAsync(withCorrelation())],
54 | controllers: [UsersController],
55 | providers: [UsersService],
56 | })
57 | export class UsersModule {}
58 | ```
59 |
60 | You can now use the `HttpService` as usual in your `UsersService` and `UsersController`
61 |
62 | ### Customize
63 |
64 | You can easily customize the header and ID by including a config when you register the module
65 |
66 | ```ts
67 | @Module({
68 | imports: [CorrelationModule.forRoot({
69 | header: string
70 | generator: () => string
71 | })]
72 | })
73 | export class AppModule implements NestModule {
74 | configure(consumer: MiddlewareConsumer) {
75 | consumer.apply(CorrelationIdMiddleware).forRoutes('*');
76 | }
77 | }
78 | ```
79 |
80 | #### Add `correlationId` to logs
81 |
82 | In order to add the correlation ID to your logs, you can use the `CorrelationService` service to get the current correlationId.
83 |
84 | In the following example, we are using the [@ntegral/nestjs-sentry](https://github.com/ntegral/nestjs-sentry) package, but you can use any package or provider you like.
85 |
86 | ```ts
87 | import { CorrelationService } from '@evanion/nestjs-correlation-id';
88 | import { Injectable, NestMiddleware } from '@nestjs/common';
89 | import { InjectSentry, SentryService } from '@ntegral/nestjs-sentry';
90 | import { NextFunction, Request, Response } from 'express';
91 |
92 | @Injectable()
93 | export class SentryMiddleware implements NestMiddleware {
94 | constructor(
95 | private readonly correlationService: CorrelationService,
96 | @InjectSentry() private readonly sentryService: SentryService,
97 | ) {}
98 |
99 | async use(_req: Request, _res: Response, next: NextFunction) {
100 | const correlationId = await this.correlationService.getCorrelationId();
101 | this.sentryService.instance().configureScope((scope) => {
102 | scope.setTag('correlationId', correlationId);
103 | });
104 | next();
105 | }
106 | }
107 | ```
108 |
109 | Then add it to your `AppModule`
110 |
111 | ```ts
112 | import { Module } from '@nestjs-common';
113 | import { SentryModule } from '@ntegral/nestjs-sentry';
114 | import { CorrelationModule } from '@evanion/nestjs-correlation-id';
115 | import { SentryMiddleware } from './middleware/sentry.middleware';
116 |
117 | @Module({
118 | imports: [
119 | CorrelationModule.forRoot(),
120 | SentryModule.forRoot({
121 | // ... your config
122 | }),
123 | ],
124 | })
125 | export class AppModule implements NestModule {
126 | configure(consumer: MiddlewareConsumer) {
127 | consumer.apply(CorrelationIdMiddleware).forRoutes('*');
128 | consumer.apply(SentryMiddleware).forRoutes('*');
129 | }
130 | }
131 | ```
132 |
133 | If you need to manually set the correlationId anywhere in your application. You can use the `CorrelationService` service to set the correlationId.
134 |
135 | ```ts
136 | this.correlationService.setCorrelationId('some_correlation_id');
137 | ```
138 |
139 | see [e2e tests](/test) for a fully working example
140 |
141 | ## Change Log
142 |
143 | See [Changelog](CHANGELOG.md) for more information.
144 |
145 | ## Contributing
146 |
147 | Contributions welcome! See [Contributing](CONTRIBUTING.md).
148 |
149 | ## Author
150 |
151 | **Mikael Pettersson (Evanion on [Discord](https://discord.gg/G7Qnnhy))**
152 |
153 | ## License
154 |
155 | Licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
156 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: ['js', 'json', 'ts'],
3 | rootDir: 'src',
4 | testRegex: '.spec.ts$',
5 | transform: {
6 | '^.+\\.(t|j)s$': 'ts-jest',
7 | },
8 | coverageDirectory: '../coverage',
9 | coveragePathIgnorePatterns: [
10 | '.eslintrc.js',
11 | 'jest.config',
12 | 'node_modules',
13 | '/coverage',
14 | '/dist',
15 | 'test',
16 | '.interface.ts',
17 | '.module.ts',
18 | '.spec.ts',
19 | 'index.ts',
20 | ],
21 | testEnvironment: 'node',
22 | };
23 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "ts",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
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 ",
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 | },
26 | "keywords": [
27 | "nestjs",
28 | "nestjs-middleware",
29 | "middleware",
30 | "correlation",
31 | "correlation-id",
32 | "request",
33 | "request-id"
34 | ],
35 | "publishConfig": {
36 | "access": "public"
37 | },
38 | "repository": {
39 | "type": "git",
40 | "url": "https://github.com/evanion/nestjs-correlation-id"
41 | },
42 | "bugs": "https://github.com/evanion/nestjs-correlation-id/issues",
43 | "peerDependencies": {
44 | "@nestjs/axios": "^0.1.0 || ^1.0.0 || ^2.0.0 || ^3.00",
45 | "@nestjs/common": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
46 | },
47 | "dependencies": {
48 | "uuid": "^9.0.0"
49 | },
50 | "devDependencies": {
51 | "@nestjs/axios": "^3.0.0",
52 | "@nestjs/common": "^10.0.5",
53 | "@nestjs/core": "^10.0.5",
54 | "@nestjs/platform-express": "^10.0.5",
55 | "@nestjs/testing": "10.0.5",
56 | "@types/express": "4.17.17",
57 | "@types/jest": "29.5.3",
58 | "@types/supertest": "2.0.12",
59 | "@typescript-eslint/eslint-plugin": "^6.0.0",
60 | "@typescript-eslint/parser": "^6.0.0",
61 | "axios": "^1.4.0",
62 | "eslint": "^8.44.0",
63 | "eslint-config-prettier": "^8.8.0",
64 | "eslint-plugin-prettier": "^5.0.0",
65 | "jest": "29.6.1",
66 | "prettier": "^3.0.0",
67 | "reflect-metadata": "^0.1.13",
68 | "release-it": "^16.1.0",
69 | "rxjs": "^7.8.1",
70 | "supertest": "6.3.3",
71 | "ts-jest": "29.1.1",
72 | "ts-node": "10.9.1",
73 | "tsc-watch": "6.0.4",
74 | "tsconfig-paths": "4.2.0",
75 | "typescript": "5.1.6",
76 | "uuid": "^9.0.0"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=Evanion_nestjs-correlation-id
2 | sonar.javascript.lcov.reportPaths=./coverage/lcov.info
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const CORRELATION_ID_HEADER = 'X-Correlation-Id';
2 | export const CORRELATION_CONFIG_TOKEN = 'CORRELATION_CONFIG';
3 |
--------------------------------------------------------------------------------
/src/correlation-id.middleware.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | /* eslint-disable @typescript-eslint/no-empty-function */
3 | import { CorrelationIdMiddleware } from './correlation-id.middleware';
4 | import { CorrelationService } from './correlation.service';
5 | import { CorrelationConfig } from './interfaces/correlation-config.interface';
6 |
7 | const mockCorrelationConfig: CorrelationConfig = {
8 | header: 'x-correlation-id',
9 | generator: () => '12345',
10 | };
11 |
12 | // @ts-expect-error: Test mock
13 | const mockCorrelationService: CorrelationService = {
14 | getCorrelationId: jest.fn().mockImplementation(() => 'test123'),
15 | setCorrelationId: jest.fn(),
16 | };
17 |
18 | describe('CorrelationIdMiddleware', () => {
19 | let middleware: CorrelationIdMiddleware;
20 | beforeEach(() => {
21 | middleware = new CorrelationIdMiddleware(
22 | mockCorrelationService,
23 | mockCorrelationConfig,
24 | );
25 | });
26 |
27 | it('should be defined', () => {
28 | expect(middleware).toBeDefined();
29 | });
30 |
31 | it('should set the correlation id in request object', () => {
32 | const req = {
33 | get: jest.fn(),
34 | headers: {},
35 | };
36 | const res = {
37 | get: jest.fn(),
38 | set: jest.fn(),
39 | headers: {},
40 | };
41 | jest.spyOn(res, 'set');
42 | middleware.use(req as any, res as any, jest.fn());
43 |
44 | expect(req.headers['x-correlation-id']).toBe('test123');
45 | });
46 |
47 | it('should set the correlation id in response object', () => {
48 | const req = {
49 | get: jest.fn().mockImplementation(() => 'test123'),
50 | headers: {},
51 | };
52 | const res = {
53 | get: jest.fn(),
54 | set: jest.fn(),
55 | headers: {},
56 | };
57 | jest.spyOn(res, 'set');
58 | middleware.use(req as any, res as any, () => {});
59 |
60 | expect(res.set).toHaveBeenCalledWith('x-correlation-id', 'test123');
61 | });
62 |
63 | it('should set the correlation id in correlationService', () => {
64 | const req = {
65 | get: jest.fn().mockImplementation(() => 'test123'),
66 | headers: {},
67 | };
68 | const res = {
69 | get: jest.fn(),
70 | set: jest.fn(),
71 | headers: {},
72 | };
73 | jest.spyOn(res, 'set');
74 | middleware.use(req as any, res as any, jest.fn());
75 |
76 | expect(mockCorrelationService.setCorrelationId).toHaveBeenCalledWith(
77 | 'test123',
78 | );
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/src/correlation-id.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
2 | import { Request, Response } from 'express';
3 | import { CORRELATION_CONFIG_TOKEN } from './constants';
4 | import { CorrelationService } from './correlation.service';
5 | import { CorrelationConfig } from './interfaces/correlation-config.interface';
6 |
7 | @Injectable()
8 | export class CorrelationIdMiddleware implements NestMiddleware {
9 | constructor(
10 | private correlationService: CorrelationService,
11 | @Inject(CORRELATION_CONFIG_TOKEN)
12 | private correlationConfig: CorrelationConfig,
13 | ) {}
14 | use(req: Request, res: Response, next: () => void) {
15 | const { header } = this.correlationConfig;
16 | const correlationId =
17 | req.get(header) || this.correlationService.getCorrelationId();
18 |
19 | if (!req.headers[header]) req.headers[header] = correlationId;
20 | if (!res.get(header)) res.set(header, correlationId);
21 |
22 | this.correlationService.setCorrelationId(correlationId);
23 | next();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/correlation.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Module, Provider } from '@nestjs/common';
2 | import { v4 as UUIDv4 } from 'uuid';
3 | import { CORRELATION_CONFIG_TOKEN, CORRELATION_ID_HEADER } from './constants';
4 | import { CorrelationService } from './correlation.service';
5 | import { CorrelationConfig } from './interfaces/correlation-config.interface';
6 |
7 | @Module({})
8 | export class CorrelationModule {
9 | static forRoot(config?: Partial): DynamicModule {
10 | const correlationConfigProvider: Provider = {
11 | provide: CORRELATION_CONFIG_TOKEN,
12 | useValue: {
13 | ...config,
14 | header: config?.header || CORRELATION_ID_HEADER,
15 | generator: config?.generator || UUIDv4,
16 | },
17 | };
18 | return {
19 | global: true,
20 | module: CorrelationModule,
21 | providers: [correlationConfigProvider, CorrelationService],
22 | exports: [correlationConfigProvider, CorrelationService],
23 | };
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/correlation.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CORRELATION_CONFIG_TOKEN, CORRELATION_ID_HEADER } from './constants';
3 | import { CorrelationService } from './correlation.service';
4 |
5 | describe('CorrelationService', () => {
6 | let service: CorrelationService;
7 |
8 | beforeEach(async () => {
9 | const module: TestingModule = await Test.createTestingModule({
10 | providers: [
11 | CorrelationService,
12 | {
13 | provide: CORRELATION_CONFIG_TOKEN,
14 | useValue: {
15 | header: CORRELATION_ID_HEADER,
16 | generator: () => 'test-id',
17 | },
18 | },
19 | ],
20 | }).compile();
21 |
22 | service = await module.resolve(CorrelationService);
23 | });
24 |
25 | it('should be defined', () => {
26 | expect(service).toBeDefined();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/correlation.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, Scope } from '@nestjs/common';
2 | import { v4 as UUIDv4 } from 'uuid';
3 | import { CORRELATION_CONFIG_TOKEN } from './constants';
4 | import { CorrelationConfig } from './interfaces/correlation-config.interface';
5 |
6 | @Injectable({ scope: Scope.REQUEST })
7 | export class CorrelationService {
8 | private correlationId: string;
9 |
10 | constructor(
11 | @Inject(CORRELATION_CONFIG_TOKEN) correlationConfig: CorrelationConfig,
12 | ) {
13 | this.correlationId = correlationConfig.generator
14 | ? correlationConfig.generator()
15 | : UUIDv4();
16 | }
17 |
18 | getCorrelationId(): string {
19 | return this.correlationId;
20 | }
21 | setCorrelationId(correlationId: string): void {
22 | this.correlationId = correlationId;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './correlation-id.middleware';
2 | export * from './correlation.service';
3 | export * from './correlation.module';
4 | export * from './withCorrelation.function';
5 | export * from './constants';
6 |
--------------------------------------------------------------------------------
/src/interfaces/correlation-config.interface.ts:
--------------------------------------------------------------------------------
1 | export interface CorrelationConfig {
2 | header: string;
3 | generator: () => string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/withCorrelation.function.ts:
--------------------------------------------------------------------------------
1 | import { HttpModuleOptions } from '@nestjs/axios';
2 | import { CORRELATION_ID_HEADER } from './constants';
3 | import { CorrelationModule } from './correlation.module';
4 | import { CorrelationService } from './correlation.service';
5 |
6 | export const withCorrelation = (config?: HttpModuleOptions) => ({
7 | imports: [CorrelationModule],
8 | useFactory: async (correlationService: CorrelationService) => ({
9 | ...config,
10 | headers: {
11 | ...(config?.headers && config.headers),
12 | [CORRELATION_ID_HEADER]: correlationService.getCorrelationId(),
13 | },
14 | }),
15 | inject: [CorrelationService],
16 | });
17 |
--------------------------------------------------------------------------------
/test/correlation.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import {
3 | HttpStatus,
4 | INestApplication,
5 | MiddlewareConsumer,
6 | Module,
7 | NestModule,
8 | } from '@nestjs/common';
9 | import * as request from 'supertest';
10 | import { CorrelationIdMiddleware, CorrelationModule } from '../src';
11 | import { TestModule } from './test-module/test.module';
12 | import { HttpService } from '@nestjs/axios';
13 | import { of } from 'rxjs';
14 | import { AxiosRequestHeaders } from 'axios';
15 |
16 | @Module({
17 | imports: [CorrelationModule.forRoot(), TestModule],
18 | })
19 | class AppModule implements NestModule {
20 | configure(consumer: MiddlewareConsumer) {
21 | consumer.apply(CorrelationIdMiddleware).forRoutes('*');
22 | }
23 | }
24 |
25 | describe('CorrelationMiddleware (e2e)', () => {
26 | let app: INestApplication;
27 | let httpService: HttpService;
28 |
29 | beforeEach(async () => {
30 | const moduleFixture: TestingModule = await Test.createTestingModule({
31 | imports: [AppModule],
32 | }).compile();
33 |
34 | httpService = moduleFixture.get(HttpService);
35 |
36 | app = moduleFixture.createNestApplication();
37 |
38 | await app.init();
39 |
40 | jest.spyOn(httpService, 'get').mockImplementation(() =>
41 | of({
42 | config: { url: 'http://example.com/test', method: 'GET', headers: {} as AxiosRequestHeaders },
43 | headers: {
44 | connection: 'keep-alive',
45 | 'content-type': 'application/json',
46 | },
47 | status: HttpStatus.OK,
48 | statusText: 'OK',
49 | data: { foo: 'bar' },
50 | }),
51 | );
52 | });
53 |
54 | it('/ (GET)', () => {
55 | return request(app.getHttpServer()).get('/').expect(200).expect('pong');
56 | });
57 |
58 | it('/ (GET) with correlation id', async () => {
59 | const result = await request(app.getHttpServer())
60 | .get('/')
61 | .set('X-Correlation-Id', 'test-id-1');
62 |
63 | expect(result.headers['x-correlation-id']).toBe('test-id-1');
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": "../",
4 | "modulePaths": [""],
5 | "testEnvironment": "node",
6 | "testRegex": ".e2e-spec.ts$",
7 | "transform": {
8 | "^.+\\.(t|j)s$": "ts-jest"
9 | },
10 | "globals": {
11 | "ts-jest": {
12 | "tsConfig": "tsconfig.e2e.json"
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/test/test-module/test.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import { TestService } from './test.service';
3 |
4 | @Controller()
5 | export class TestController {
6 | constructor(private readonly testService: TestService) {}
7 |
8 | @Get()
9 | ping() {
10 | return 'pong';
11 | }
12 |
13 | @Get('/axios')
14 | test() {
15 | return this.testService.getTest();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/test/test-module/test.module.ts:
--------------------------------------------------------------------------------
1 | import { HttpModule } from '@nestjs/axios';
2 | import { Module } from '@nestjs/common';
3 | import { withCorrelation } from 'src';
4 | import { TestController } from './test.controller';
5 | import { TestService } from './test.service';
6 |
7 | @Module({
8 | controllers: [TestController],
9 | providers: [TestService],
10 | imports: [HttpModule.registerAsync(withCorrelation())],
11 | })
12 | export class TestModule {}
13 |
--------------------------------------------------------------------------------
/test/test-module/test.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpService } from '@nestjs/axios';
2 | import { Injectable } from '@nestjs/common';
3 | import { lastValueFrom, map } from 'rxjs';
4 |
5 | @Injectable()
6 | export class TestService {
7 | constructor(private httpService: HttpService) {}
8 |
9 | getTest() {
10 | return lastValueFrom(
11 | this.httpService
12 | .get('http://example.com/test')
13 | .pipe(map((res) => res.data)),
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": { "rootDir": "./src" },
3 | "extends": "./tsconfig.json",
4 | "exclude": ["node_modules", "test/**/*.ts", "dist", "**/*spec.ts"]
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": { "rootDir": "./" },
3 | "extends": "./tsconfig.json",
4 | "include": ["src/**/*.ts", "test/**/*.ts"],
5 | "exclude": ["node_modules", "dist", "**/*spec.ts"]
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "target": "es6",
9 | "sourceMap": false,
10 | "outDir": "./dist",
11 | "rootDir": "./src",
12 | "baseUrl": "./",
13 | "noLib": false
14 | },
15 | "include": ["src/**/*.ts"],
16 | "exclude": ["node_modules"]
17 | }
18 |
--------------------------------------------------------------------------------