├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── commitlint.config.js ├── package-lock.json ├── package.json ├── src ├── di-token-constants.ts ├── got-config.provider.ts ├── got.config.defaults.ts ├── http-client-core.module.ts ├── http-client.config.defaults.ts ├── http-client.module.ts ├── http-client.service.spec.ts ├── http-client.service.ts ├── http-service-config-provider.interface.ts ├── http-service-config.provider.ts ├── index.ts ├── public-di-token.constants.ts ├── tests │ └── unit.spec.ts ├── types │ ├── config.types.ts │ ├── trace-data-service.interface.ts │ └── trace-data-service.type.ts ├── unique-providers-by-token.spec.ts └── utils.ts ├── test ├── fixtures │ ├── config-module │ │ ├── config-module.config.mock.ts │ │ └── config-module.mock.ts │ ├── got-mock.ts │ └── trace-data-module │ │ ├── trace-data-module.mock.ts │ │ └── trace-data-service.mock.ts ├── jest-e2e.json ├── merge-options.e2e.ts └── trace.e2e.ts ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'eslint-config-airbnb-base', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'prettier/@typescript-eslint', 7 | 'plugin:prettier/recommended', 8 | 'plugin:import/typescript', 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 2018, 12 | sourceType: 'module', 13 | }, 14 | rules: { 15 | '@typescript-eslint/no-parameter-properties': [ 16 | 'error', 17 | { 18 | allows: ['private readonly'], 19 | }, 20 | ], 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/interface-name-prefix': 'off', 23 | '@typescript-eslint/no-unused-vars': 'error', 24 | '@typescript-eslint/no-loss-of-precision': 'off', 25 | 'import/prefer-default-export': 'off', 26 | 'no-useless-constructor': 'off', 27 | 'no-empty-function': 'off', 28 | 'class-methods-use-this': 'off', 29 | 'new-cap': 'off', 30 | '@typescript-eslint/camelcase': 'off', 31 | 'no-underscore-dangle': 'off', 32 | 'import/extensions': [ 33 | 'error', 34 | 'ignorePackages', 35 | { 36 | js: 'never', 37 | jsx: 'never', 38 | ts: 'never', 39 | tsx: 'never', 40 | mjs: 'never', 41 | }, 42 | ], 43 | }, 44 | overrides: [ 45 | { 46 | files: ['*.test.{ts,js}', '*.spec.{ts,js}', 'test/**/*.{ts,js}'], 47 | env: { 48 | jest: true, 49 | }, 50 | rules: { 51 | 'no-console': 'off', 52 | 'import/no-extraneous-dependencies': 'off', 53 | '@typescript-eslint/no-var-requires': 'off', 54 | 'no-underscore-dangle': 'off', 55 | 'no-restricted-syntax': 'off', 56 | 'no-await-in-loop': 'off', 57 | 'max-classes-per-file': 'off', 58 | }, 59 | }, 60 | ], 61 | }; 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/linux,macos,windows,visualstudiocode,node,intellij,webstorm 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,windows,visualstudiocode,node,intellij,webstorm 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | .idea 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### Intellij Patch ### 79 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 80 | 81 | # *.iml 82 | # modules.xml 83 | # .idea/misc.xml 84 | # *.ipr 85 | 86 | # Sonarlint plugin 87 | .idea/**/sonarlint/ 88 | 89 | # SonarQube Plugin 90 | .idea/**/sonarIssues.xml 91 | 92 | # Markdown Navigator plugin 93 | .idea/**/markdown-navigator.xml 94 | .idea/**/markdown-navigator-enh.xml 95 | .idea/**/markdown-navigator/ 96 | 97 | # Cache file creation bug 98 | # See https://youtrack.jetbrains.com/issue/JBR-2257 99 | .idea/$CACHE_FILE$ 100 | 101 | ### Linux ### 102 | *~ 103 | 104 | # temporary files which can be created if a process still has a handle open of a deleted file 105 | .fuse_hidden* 106 | 107 | # KDE directory preferences 108 | .directory 109 | 110 | # Linux trash folder which might appear on any partition or disk 111 | .Trash-* 112 | 113 | # .nfs files are created when an open file is removed but is still being accessed 114 | .nfs* 115 | 116 | ### macOS ### 117 | # General 118 | .DS_Store 119 | .AppleDouble 120 | .LSOverride 121 | 122 | # Icon must end with two \r 123 | Icon 124 | 125 | # Thumbnails 126 | ._* 127 | 128 | # Files that might appear in the root of a volume 129 | .DocumentRevisions-V100 130 | .fseventsd 131 | .Spotlight-V100 132 | .TemporaryItems 133 | .Trashes 134 | .VolumeIcon.icns 135 | .com.apple.timemachine.donotpresent 136 | 137 | # Directories potentially created on remote AFP share 138 | .AppleDB 139 | .AppleDesktop 140 | Network Trash Folder 141 | Temporary Items 142 | .apdisk 143 | 144 | ### Node ### 145 | # Logs 146 | logs 147 | *.log 148 | npm-debug.log* 149 | yarn-debug.log* 150 | yarn-error.log* 151 | lerna-debug.log* 152 | 153 | # Diagnostic reports (https://nodejs.org/api/report.html) 154 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 155 | 156 | # Runtime data 157 | pids 158 | *.pid 159 | *.seed 160 | *.pid.lock 161 | 162 | # Directory for instrumented libs generated by jscoverage/JSCover 163 | lib-cov 164 | 165 | # Coverage directory used by tools like istanbul 166 | coverage 167 | *.lcov 168 | 169 | # nyc test coverage 170 | .nyc_output 171 | 172 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 173 | .grunt 174 | 175 | # Bower dependency directory (https://bower.io/) 176 | bower_components 177 | 178 | # node-waf configuration 179 | .lock-wscript 180 | 181 | # Compiled binary addons (https://nodejs.org/api/addons.html) 182 | build/Release 183 | 184 | # Dependency directories 185 | node_modules/ 186 | jspm_packages/ 187 | 188 | # TypeScript v1 declaration files 189 | typings/ 190 | 191 | # TypeScript cache 192 | *.tsbuildinfo 193 | 194 | # Optional npm cache directory 195 | .npm 196 | 197 | # Optional eslint cache 198 | .eslintcache 199 | 200 | # Microbundle cache 201 | .rpt2_cache/ 202 | .rts2_cache_cjs/ 203 | .rts2_cache_es/ 204 | .rts2_cache_umd/ 205 | 206 | # Optional REPL history 207 | .node_repl_history 208 | 209 | # Output of 'npm pack' 210 | *.tgz 211 | 212 | # Yarn Integrity file 213 | .yarn-integrity 214 | 215 | # dotenv environment variables file 216 | .env 217 | .env.test 218 | 219 | # parcel-bundler cache (https://parceljs.org/) 220 | .cache 221 | 222 | # Next.js build output 223 | .next 224 | 225 | # Nuxt.js build / generate output 226 | .nuxt 227 | dist 228 | 229 | # Gatsby files 230 | .cache/ 231 | # Comment in the public line in if your project uses Gatsby and not Next.js 232 | # https://nextjs.org/blog/next-9-1#public-directory-support 233 | # public 234 | 235 | # vuepress build output 236 | .vuepress/dist 237 | 238 | # Serverless directories 239 | .serverless/ 240 | 241 | # FuseBox cache 242 | .fusebox/ 243 | 244 | # DynamoDB Local files 245 | .dynamodb/ 246 | 247 | # TernJS port file 248 | .tern-port 249 | 250 | # Stores VSCode versions used for testing VSCode extensions 251 | .vscode-test 252 | 253 | ### VisualStudioCode ### 254 | .vscode/* 255 | !.vscode/settings.json 256 | !.vscode/tasks.json 257 | !.vscode/launch.json 258 | !.vscode/extensions.json 259 | *.code-workspace 260 | 261 | ### VisualStudioCode Patch ### 262 | # Ignore all local history of files 263 | .history 264 | 265 | ### WebStorm ### 266 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 267 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 268 | 269 | # User-specific stuff 270 | 271 | # Generated files 272 | 273 | # Sensitive or high-churn files 274 | 275 | # Gradle 276 | 277 | # Gradle and Maven with auto-import 278 | # When using Gradle or Maven with auto-import, you should exclude module files, 279 | # since they will be recreated, and may cause churn. Uncomment if using 280 | # auto-import. 281 | # .idea/artifacts 282 | # .idea/compiler.xml 283 | # .idea/jarRepositories.xml 284 | # .idea/modules.xml 285 | # .idea/*.iml 286 | # .idea/modules 287 | # *.iml 288 | # *.ipr 289 | 290 | # CMake 291 | 292 | # Mongo Explorer plugin 293 | 294 | # File-based project format 295 | 296 | # IntelliJ 297 | 298 | # mpeltonen/sbt-idea plugin 299 | 300 | # JIRA plugin 301 | 302 | # Cursive Clojure plugin 303 | 304 | # Crashlytics plugin (for Android Studio and IntelliJ) 305 | 306 | # Editor-based Rest Client 307 | 308 | # Android studio 3.1+ serialized cache file 309 | 310 | ### WebStorm Patch ### 311 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 312 | 313 | # *.iml 314 | # modules.xml 315 | # .idea/misc.xml 316 | # *.ipr 317 | 318 | # Sonarlint plugin 319 | 320 | # SonarQube Plugin 321 | 322 | # Markdown Navigator plugin 323 | 324 | # Cache file creation bug 325 | # See https://youtrack.jetbrains.com/issue/JBR-2257 326 | 327 | ### Windows ### 328 | # Windows thumbnail cache files 329 | Thumbs.db 330 | Thumbs.db:encryptable 331 | ehthumbs.db 332 | ehthumbs_vista.db 333 | 334 | # Dump file 335 | *.stackdump 336 | 337 | # Folder config file 338 | [Dd]esktop.ini 339 | 340 | # Recycle Bin used on file shares 341 | $RECYCLE.BIN/ 342 | 343 | # Windows Installer files 344 | *.cab 345 | *.msi 346 | *.msix 347 | *.msm 348 | *.msp 349 | 350 | # Windows shortcuts 351 | *.lnk 352 | 353 | # End of https://www.toptal.com/developers/gitignore/api/linux,macos,windows,visualstudiocode,node,intellij,webstorm 354 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | directories: 5 | - node_modules 6 | 7 | notifications: 8 | email: 9 | recipients: 10 | - laplandin.denis@gmail.com 11 | - goodluckhf@yandex.tu 12 | on_success: never 13 | on_failure: always 14 | 15 | stages: 16 | - lints 17 | - test 18 | - name: release 19 | if: branch = master 20 | jobs: 21 | include: 22 | - stage: lints 23 | name: 'linting and type checking' 24 | node_js: 25 | - '12' 26 | before_script: 27 | - npm prune 28 | script: 29 | - npm run lint 30 | - npm run build 31 | - stage: test 32 | node_js: 33 | - '12' 34 | before_script: 35 | - npm prune 36 | script: 37 | - npm run test:cov 38 | - npm run test:e2e 39 | after_success: 40 | - npm run report-coverage 41 | - stage: release 42 | node_js: '12' 43 | before_script: 44 | - npm prune 45 | script: 46 | - npm run build 47 | - npm run semantic-release 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 uKit Group 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 | # nestjs-http 2 | 3 | ![Travis](https://img.shields.io/travis/ukitgroup/nestjs-http/master.svg?style=flat-square) 4 | ![Coverage Status](https://coveralls.io/repos/github/ukitgroup/nestjs-http/badge.svg?branch=master) 5 | ![node](https://img.shields.io/node/v/@ukitgroup/nestjs-http.svg?style=flat-square) 6 | ![npm](https://img.shields.io/npm/v/@ukitgroup/nestjs-http.svg?style=flat-square) 7 | 8 | ![GitHub top language](https://img.shields.io/github/languages/top/ukitgroup/nestjs-http.svg?style=flat-square) 9 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/ukitgroup/nestjs-http.svg?style=flat-square) 10 | ![David](https://img.shields.io/david/ukitgroup/nestjs-http.svg?style=flat-square) 11 | ![David](https://img.shields.io/david/dev/ukitgroup/nestjs-http.svg?style=flat-square) 12 | 13 | ![license](https://img.shields.io/github/license/ukitgroup/nestjs-http.svg?style=flat-square) 14 | ![GitHub last commit](https://img.shields.io/github/last-commit/ukitgroup/nestjs-http.svg?style=flat-square) 15 | ![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square) 16 | 17 | ## Description 18 | 19 | ### Rich featured HttpClient for [nestjs](https://nestjs.com/) applications 20 | 21 | - [Got](https://www.npmjs.com/package/got) integration for `nestjs`; 22 | - Retries and all `Got` functions out of the box 23 | - Transparent `Got` usage (you will work with Got interface) 24 | - Accept external `Tracing Service` via DI for attaching specific http-headers across your microservice architecture; 25 | - Modularity - create instances for your modules with different configurations; 26 | - Default keep-alive http/https agent. 27 | 28 | ## Requirements 29 | 30 | 1. @nestjs/common ^9.1.4 31 | 2. @nestjs/core ^9.1.4 32 | 33 | ## Installation 34 | 35 | ```bash 36 | npm install --save @ukitgroup/nestjs-http 37 | ``` 38 | 39 | or 40 | 41 | ```bash 42 | yarn add @ukitgroup/nestjs-http 43 | ``` 44 | 45 | ## Short example 46 | 47 | **cat.service.ts** 48 | 49 | ```typescript 50 | @Injectable() 51 | export class CatService { 52 | constructor(@Inject(HttpClientService) private readonly httpClient: Got) {} 53 | 54 | meow(): string { 55 | // httpClient is an instance of Got 56 | // configuration from AppModule and CatModule 57 | // set built-in headers 58 | this.httpClient.post('/meow'); 59 | return `meows..`; 60 | } 61 | } 62 | ``` 63 | 64 | **cat.module.ts** 65 | 66 | ```typescript 67 | @Module({ 68 | imports: [ 69 | HttpClient.forInstance({ 70 | imports: [YourConfigModule], 71 | providers: [ 72 | { 73 | // override got configuration 74 | provide: HTTP_CLIENT_INSTANCE_GOT_OPTS, 75 | inject: [YourConfig], 76 | useFactory: config => { 77 | return { 78 | retry: config.retry, 79 | }; 80 | }, 81 | }, 82 | ], 83 | }), 84 | ], 85 | providers: [CatService], 86 | }) 87 | export class CatModule {} 88 | ``` 89 | 90 | **app.module.ts** 91 | 92 | ```typescript 93 | @Module({ 94 | imports: [HttpClient.forRoot({}), CatModule], 95 | }) 96 | export class AppModule {} 97 | ``` 98 | 99 | ## API 100 | 101 | Define root configuration for Got in AppModule with: 102 | 103 | ``` 104 | HttpClient.forRoot(options: HttpClientForRootType) 105 | ``` 106 | 107 | ``` 108 | HttpClientForRootType: { 109 | imports?: [], 110 | providers?: [], 111 | } 112 | ``` 113 | 114 | Provide configuration with tokens: 115 | 116 | - `HTTP_SERVICE_CONFIG` - ServiceConfigType for HttpClientService 117 | - `TRACE_DATA_SERVICE` - should implements TraceDataServiceInterface 118 | - `GOT_CONFIG` - got configuration 119 | 120 | Provided configuration will be merged: 121 | defaultConfig -> forRoot() -> forInstance() -> execution 122 | 123 | default config for httpService: 124 | 125 | ```typescript 126 | const defaultConfig = { 127 | enableTraceService: false, 128 | headersMap: { 129 | traceId: 'x-trace-id', 130 | ip: 'x-real-ip', 131 | userAgent: 'user-agent', 132 | referrer: 'referrer', 133 | }, 134 | excludeHeaders: [], 135 | }; 136 | ``` 137 | 138 | default config for got extends default config from [got documentation](https://github.com/sindresorhus/got) 139 | 140 | ```typescript 141 | const defaultConfig = { 142 | agent: { 143 | http: new http.Agent({ keepAlive: true }), 144 | https: new https.Agent({ keepAlive: true }), 145 | }, 146 | }; 147 | ``` 148 | 149 | Define instance configuration, trace service and http client service config in your module with: 150 | 151 | ``` 152 | HttpClient.forInstance(options: HttpClientForRootType) 153 | ``` 154 | 155 | # Trace service injection 156 | 157 | Usually you pass some headers across your microservices such as: `trace-id`, `ip`, `user-agent` etc. 158 | To make it convenient you can inject traceService via `dependency injection` and `HTTPClient` will pass this data with headers 159 | 160 | You just have to implement TraceServiceInterface: 161 | 162 | ```typescript 163 | export interface TraceDataServiceInterface { 164 | getRequestData(): TraceDataType; 165 | } 166 | 167 | // and TraceDataType 168 | export type TraceDataType = { 169 | [key: string]: string; 170 | }; 171 | ``` 172 | 173 | Headers mapping to values: 174 | 175 | ```typescript 176 | const headersMap = { 177 | traceId: 'x-trace-id', 178 | ip: 'x-real-ip', 179 | userAgent: 'user-agent', 180 | referrer: 'referrer', 181 | }; 182 | ``` 183 | 184 | Sou you can just define your service which should return these fields. For example, you can use `cls-hooked` for retrieving request data 185 | 186 | ```typescript 187 | export class TraceService implements TraceDataServiceInterface { 188 | getRequestData() { 189 | return { 190 | traceId: 'unique-id', 191 | ip: '127.0.0.1', 192 | }; 193 | } 194 | } 195 | ``` 196 | 197 | And then just define configuration: 198 | 199 | ```typescript 200 | @Module({ 201 | imports: [ 202 | HttpClient.forInstance({ 203 | providers: [ 204 | { 205 | provide: HTTP_SERVICE_CONFIG, 206 | useValue: { 207 | enableTraceService: true, 208 | }, 209 | }, 210 | { provide: TRACE_DATA_SERVICE, useClass: TraceService }, 211 | ], 212 | }), 213 | ], 214 | }) 215 | class AwesomeModule {} 216 | ``` 217 | 218 | ### TODO 219 | 220 | - Injection of `MetricService` for exports requests metrics such as: statuses, errors, retries, timing 221 | - Client balancing for servers with several `ip`s or endpoints in different AZ which don't support server's LB. 222 | - ... Feature/Pull requests are welcome!😅 223 | 224 | ## License 225 | 226 | @ukitgroup/nestjs-http is [MIT licensed](LICENSE). 227 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ukitgroup/nestjs-http", 3 | "private": false, 4 | "version": "2.0.4", 5 | "description": "Full featured Http adapter for nestjs based on GOT", 6 | "main": "index.js", 7 | "types": "index.d.ts", 8 | "scripts": { 9 | "build": "tsc --skipLibCheck -p tsconfig.build.json", 10 | "prebuild": "rimraf dist", 11 | "lint": "eslint --ignore-path .gitignore \"**/*.{js,ts}\"", 12 | "lint:fix": "eslint --fix --ignore-path .gitignore \"**/*.{js,ts}\"", 13 | "test": "jest --passWithNoTests", 14 | "test:e2e": "jest --config ./test/jest-e2e.json --forceExit --detectOpenHandles --passWithNoTests", 15 | "test:watch": "jest --watch --passWithNoTests", 16 | "test:cov": "jest --coverage --passWithNoTests", 17 | "report-coverage": "cat coverage/lcov.info | coveralls", 18 | "semantic-release": "cp ./package.json ./dist/package.json && cp ./README.md ./dist/README.md && cd ./dist && semantic-release" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/ukitgroup/nestjs-http.git" 23 | }, 24 | "keywords": [ 25 | "nestjs", 26 | "http", 27 | "got", 28 | "tracing", 29 | "typescript", 30 | "microservices" 31 | ], 32 | "author": "laplandin ", 33 | "contributors": [ 34 | "Goodluckhf " 35 | ], 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/ukitgroup/nestjs-http/issues" 39 | }, 40 | "homepage": "https://github.com/ukitgroup/nestjs-http#readme", 41 | "peerDependencies": { 42 | "@nestjs/common": "^9.1.4", 43 | "@nestjs/core": "^9.1.4", 44 | "got": "^11.8.5" 45 | }, 46 | "dependencies": { 47 | "lodash": "^4.17.21", 48 | "reflect-metadata": "^0.1.14" 49 | }, 50 | "devDependencies": { 51 | "@commitlint/cli": "^11.0.0", 52 | "@commitlint/config-conventional": "^17.0.3", 53 | "@nestjs/common": "^9.1.4", 54 | "@nestjs/core": "^9.1.4", 55 | "@nestjs/platform-express": "^9.2.1", 56 | "@nestjs/testing": "^9.4.3", 57 | "@semantic-release/changelog": "5.0.1", 58 | "@semantic-release/commit-analyzer": "8.0.1", 59 | "@semantic-release/git": "9.0.0", 60 | "@semantic-release/github": "^7.1.1", 61 | "@semantic-release/npm": "7.0.5", 62 | "@semantic-release/release-notes-generator": "9.0.1", 63 | "@types/got": "^9.6.11", 64 | "@types/jest": "26.0.0", 65 | "@types/mongoose": "^5.7.37", 66 | "@types/node": "^13.13.30", 67 | "@types/supertest": "^2.0.10", 68 | "@typescript-eslint/eslint-plugin": "^5.39.0", 69 | "@typescript-eslint/parser": "^5.39.0", 70 | "coveralls": "^3.1.0", 71 | "eslint": "^6.8.0", 72 | "eslint-config-airbnb-base": "^14.1.0", 73 | "eslint-config-prettier": "^6.15.0", 74 | "eslint-plugin-import": "^2.22.1", 75 | "eslint-plugin-prettier": "^3.1.3", 76 | "express": "^4.18.1", 77 | "got": "^11.8.5", 78 | "husky": "^4.3.0", 79 | "jest": "^28.1.2", 80 | "lint-staged": "^10.5.1", 81 | "nock": "^12.0.3", 82 | "prettier": "^1.19.1", 83 | "semantic-release": "^17.2.2", 84 | "supertest": "^4.0.2", 85 | "ts-jest": "^28.0.5", 86 | "ts-loader": "^6.2.1", 87 | "ts-node": "^8.6.2", 88 | "tsconfig-paths": "^3.9.0", 89 | "typescript": "^4.7.4" 90 | }, 91 | "jest": { 92 | "moduleFileExtensions": [ 93 | "js", 94 | "json", 95 | "ts" 96 | ], 97 | "rootDir": "src", 98 | "testRegex": ".spec.ts$", 99 | "transform": { 100 | "^.+\\.(t|j)s$": "ts-jest" 101 | }, 102 | "coverageDirectory": "../coverage", 103 | "collectCoverageFrom": [ 104 | "**/*.ts" 105 | ], 106 | "coveragePathIgnorePatterns": [ 107 | "/node_modules/", 108 | "index.ts", 109 | ".module.ts" 110 | ], 111 | "testEnvironment": "node" 112 | }, 113 | "husky": { 114 | "hooks": { 115 | "pre-commit": "lint-staged", 116 | "pre-push": "npm test", 117 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 118 | } 119 | }, 120 | "lint-staged": { 121 | "*.{js,ts}": [ 122 | "eslint --fix --ignore-path .gitignore", 123 | "prettier --write", 124 | "git add" 125 | ] 126 | }, 127 | "release": { 128 | "analyzeCommits": { 129 | "preset": "angular", 130 | "releaseRules": [ 131 | { 132 | "type": "refactor", 133 | "release": "patch" 134 | }, 135 | { 136 | "type": "chore", 137 | "release": "patch" 138 | } 139 | ] 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/di-token-constants.ts: -------------------------------------------------------------------------------- 1 | export const GOT_INSTANCE = 'GOT_INSTANCE'; 2 | export const TRACE_DATA_SERVICE = 'TRACE_DATA_SERVICE'; 3 | export const DEFAULT__SERVICE_CONFIG = 'DEFAULT__SERVICE_CONFIG'; 4 | export const FOR_INSTANCE__SERVICE_CONFIG = 'FOR_INSTANCE__SERVICE_CONFIG'; 5 | export const FOR_ROOT__SERVICE_CONFIG = 'FOR_ROOT__SERVICE_CONFIG'; 6 | export const FOR_INSTANCE__GOT_OPTS = 'HTTP_CLIENT_FOR_INSTANCE_GOT_OPTS'; 7 | export const FOR_ROOT__GOT_OPTS = 'HTTP_CLIENT_FOR_ROOT_GOT_OPTS'; 8 | export const DEFAULT__GOT_OPTS = 'HTTP_CLIENT_DEFAULT_GOT_OPTS'; 9 | -------------------------------------------------------------------------------- /src/got-config.provider.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Optional } from '@nestjs/common'; 2 | import { merge } from 'lodash'; 3 | 4 | import { Options } from 'got'; 5 | import { 6 | DEFAULT__GOT_OPTS, 7 | FOR_INSTANCE__GOT_OPTS, 8 | FOR_ROOT__GOT_OPTS, 9 | } from './di-token-constants'; 10 | 11 | @Injectable() 12 | export class GotConfigProvider { 13 | constructor( 14 | @Inject(DEFAULT__GOT_OPTS) 15 | private readonly defaultGotConfig: Options, 16 | @Optional() 17 | @Inject(FOR_ROOT__GOT_OPTS) 18 | private readonly forRootGotConfig: Options = {}, 19 | @Optional() 20 | @Inject(FOR_INSTANCE__GOT_OPTS) 21 | private readonly forInstanceGotConfig: Options = {}, 22 | ) {} 23 | 24 | getConfig(): Options { 25 | return merge( 26 | {}, 27 | this.defaultGotConfig, 28 | this.forRootGotConfig, 29 | this.forInstanceGotConfig, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/got.config.defaults.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import https from 'https'; 3 | import { Options } from 'got'; 4 | 5 | export const gotConfigDefaults: Options = { 6 | agent: { 7 | http: new http.Agent({ keepAlive: true }), 8 | https: new https.Agent({ keepAlive: true }), 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/http-client-core.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Module, Provider } from '@nestjs/common'; 2 | import { HttpClientForRootType } from './types/config.types'; 3 | import { 4 | DEFAULT__GOT_OPTS, 5 | DEFAULT__SERVICE_CONFIG, 6 | FOR_ROOT__GOT_OPTS, 7 | FOR_ROOT__SERVICE_CONFIG, 8 | } from './di-token-constants'; 9 | import { gotConfigDefaults } from './got.config.defaults'; 10 | import { httpServiceConfigDefaults } from './http-client.config.defaults'; 11 | import { GOT_CONFIG, HTTP_SERVICE_CONFIG } from './public-di-token.constants'; 12 | import { uniqueProvidersByToken } from './utils'; 13 | 14 | @Global() 15 | @Module({}) 16 | export class HttpClientCoreModule { 17 | static forRoot({ 18 | imports = [], 19 | providers = [], 20 | }: HttpClientForRootType): DynamicModule { 21 | const defaultProviders: Provider[] = [ 22 | { 23 | provide: DEFAULT__GOT_OPTS, 24 | useValue: gotConfigDefaults, 25 | }, 26 | { 27 | provide: DEFAULT__SERVICE_CONFIG, 28 | useValue: httpServiceConfigDefaults, 29 | }, 30 | ]; 31 | const forRootGotConfigProvider = providers.find( 32 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 33 | // @ts-ignore 34 | p => p.provide && p.provide === GOT_CONFIG, 35 | ); 36 | 37 | const forRootHTTPServiceProvider = providers.find( 38 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 39 | // @ts-ignore 40 | p => p.provide && p.provide === HTTP_SERVICE_CONFIG, 41 | ); 42 | 43 | if (forRootGotConfigProvider) { 44 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 45 | // @ts-ignore 46 | defaultProviders.push({ 47 | ...forRootGotConfigProvider, 48 | provide: FOR_ROOT__GOT_OPTS, 49 | }); 50 | } 51 | 52 | if (forRootHTTPServiceProvider) { 53 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 54 | // @ts-ignore 55 | defaultProviders.push({ 56 | ...forRootHTTPServiceProvider, 57 | provide: FOR_ROOT__SERVICE_CONFIG, 58 | }); 59 | } 60 | 61 | const uniqueProviders = uniqueProvidersByToken([ 62 | ...defaultProviders, 63 | ...providers, 64 | ]); 65 | 66 | return { 67 | module: HttpClientCoreModule, 68 | imports, 69 | providers: uniqueProviders, 70 | exports: uniqueProviders, 71 | }; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/http-client.config.defaults.ts: -------------------------------------------------------------------------------- 1 | import { ServiceConfigType } from './types/config.types'; 2 | 3 | export const httpServiceConfigDefaults: ServiceConfigType = { 4 | enableTraceService: false, 5 | headersMap: { 6 | traceId: 'x-trace-id', 7 | ip: 'x-real-ip', 8 | userAgent: 'user-agent', 9 | referrer: 'referrer', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/http-client.module.ts: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import { DynamicModule, Module, Provider } from '@nestjs/common'; 3 | import { HttpClientForRootType } from './types/config.types'; 4 | import { HttpClientCoreModule } from './http-client-core.module'; 5 | import { 6 | FOR_INSTANCE__GOT_OPTS, 7 | FOR_INSTANCE__SERVICE_CONFIG, 8 | GOT_INSTANCE, 9 | } from './di-token-constants'; 10 | import { HttpClientService } from './http-client.service'; 11 | import { GotConfigProvider } from './got-config.provider'; 12 | import { HttpServiceConfigProvider } from './http-service-config.provider'; 13 | import { GOT_CONFIG, HTTP_SERVICE_CONFIG } from './public-di-token.constants'; 14 | import { uniqueProvidersByToken } from './utils'; 15 | 16 | @Module({}) 17 | export class HttpClient { 18 | static forRoot({ 19 | imports = [], 20 | providers = [], 21 | }: HttpClientForRootType = {}): DynamicModule { 22 | return { 23 | module: HttpClient, 24 | imports: [HttpClientCoreModule.forRoot({ imports, providers })], 25 | }; 26 | } 27 | 28 | static forInstance({ 29 | imports = [], 30 | providers = [], 31 | }: HttpClientForRootType = {}): DynamicModule { 32 | const gotProviderFactory = { 33 | provide: GOT_INSTANCE, 34 | useFactory: (gotConfigProvider: GotConfigProvider) => { 35 | const config = gotConfigProvider.getConfig(); 36 | return got.extend(config); 37 | }, 38 | inject: [GotConfigProvider], 39 | }; 40 | 41 | const allProviders: Provider[] = []; 42 | 43 | const forInstanceGotConfigProvider = providers.find( 44 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 45 | // @ts-ignore 46 | p => p.provide && p.provide === GOT_CONFIG, 47 | ); 48 | 49 | const forInstanceHTTPServiceProvider = providers.find( 50 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 51 | // @ts-ignore 52 | p => p.provide && p.provide === HTTP_SERVICE_CONFIG, 53 | ); 54 | 55 | if (forInstanceGotConfigProvider) { 56 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 57 | // @ts-ignore 58 | allProviders.push({ 59 | ...forInstanceGotConfigProvider, 60 | provide: FOR_INSTANCE__GOT_OPTS, 61 | }); 62 | } 63 | 64 | if (forInstanceHTTPServiceProvider) { 65 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 66 | // @ts-ignore 67 | allProviders.push({ 68 | ...forInstanceHTTPServiceProvider, 69 | provide: FOR_INSTANCE__SERVICE_CONFIG, 70 | }); 71 | } 72 | 73 | const uniqueProviders = uniqueProvidersByToken([ 74 | ...allProviders, 75 | ...providers, 76 | gotProviderFactory, 77 | HttpServiceConfigProvider, 78 | GotConfigProvider, 79 | HttpClientService, 80 | ]); 81 | 82 | return { 83 | module: HttpClient, 84 | imports, 85 | providers: uniqueProviders, 86 | exports: [HttpClientService], 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/http-client.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Got } from 'got'; 2 | import { HttpClientService } from './http-client.service'; 3 | import { TraceDataServiceInterface } from './types/trace-data-service.interface'; 4 | import { ServiceConfigType } from './types/config.types'; 5 | import { HttpServiceConfigProviderInterface } from './http-service-config-provider.interface'; 6 | 7 | describe('HTTP client service', () => { 8 | let ctx: { 9 | gotInstance: Got; 10 | traceDataService: TraceDataServiceInterface; 11 | httpServiceConfigProvider: HttpServiceConfigProviderInterface; 12 | }; 13 | 14 | beforeEach(() => { 15 | ctx = { 16 | gotInstance: {} as Got, 17 | traceDataService: { 18 | getRequestData() { 19 | return {}; 20 | }, 21 | }, 22 | httpServiceConfigProvider: { 23 | getConfig(): ServiceConfigType { 24 | return {}; 25 | }, 26 | }, 27 | }; 28 | }); 29 | 30 | describe('feature-flag: shouldTraceServiceInvoke', () => { 31 | it('should throw error if option is provided but service is not', () => { 32 | ctx.httpServiceConfigProvider = { 33 | getConfig(): ServiceConfigType { 34 | return { 35 | enableTraceService: true, 36 | }; 37 | }, 38 | }; 39 | 40 | ctx.traceDataService = undefined; 41 | 42 | const service = new HttpClientService( 43 | ctx.gotInstance, 44 | ctx.traceDataService, 45 | ctx.httpServiceConfigProvider, 46 | ); 47 | 48 | expect(() => service.shouldTraceServiceInvoke).toThrow( 49 | /had enabled usage of TraceDataService/, 50 | ); 51 | }); 52 | 53 | it('should return false if option is disabled', () => { 54 | ctx.httpServiceConfigProvider = { 55 | getConfig(): ServiceConfigType { 56 | return { 57 | enableTraceService: false, 58 | }; 59 | }, 60 | }; 61 | 62 | const service = new HttpClientService( 63 | ctx.gotInstance, 64 | ctx.traceDataService, 65 | ctx.httpServiceConfigProvider, 66 | ); 67 | 68 | expect(service.shouldTraceServiceInvoke).toBeFalsy(); 69 | }); 70 | 71 | it('should return true if option is enabled', () => { 72 | ctx.httpServiceConfigProvider = { 73 | getConfig(): ServiceConfigType { 74 | return { 75 | enableTraceService: true, 76 | }; 77 | }, 78 | }; 79 | 80 | const service = new HttpClientService( 81 | ctx.gotInstance, 82 | ctx.traceDataService, 83 | ctx.httpServiceConfigProvider, 84 | ); 85 | 86 | expect(service.shouldTraceServiceInvoke).toBeTruthy(); 87 | }); 88 | }); 89 | 90 | describe.each(['get', 'post', 'delete', 'head', 'put', 'patch'])( 91 | 'Method invocation: %s', 92 | (method: string) => { 93 | it('Should provide client options', () => { 94 | ctx.gotInstance[method] = jest.fn(); 95 | 96 | const service = new HttpClientService( 97 | ctx.gotInstance, 98 | ctx.traceDataService, 99 | ctx.httpServiceConfigProvider, 100 | ); 101 | 102 | service[method]('', { headers: { test: 10 }, retries: 10 }); 103 | 104 | expect(ctx.gotInstance[method]).toBeCalledWith('', { 105 | headers: { test: 10 }, 106 | retries: 10, 107 | }); 108 | }); 109 | 110 | it('Should not add headers if shouldTraceServiceInvoke is disabled', () => { 111 | ctx.gotInstance[method] = jest.fn(); 112 | 113 | const service = new HttpClientService( 114 | ctx.gotInstance, 115 | ctx.traceDataService, 116 | ctx.httpServiceConfigProvider, 117 | ); 118 | 119 | service[method](''); 120 | 121 | expect(ctx.gotInstance[method]).toBeCalledWith('', { headers: {} }); 122 | }); 123 | 124 | it('Should add trace headers if shouldTraceServiceInvoke is enabled', () => { 125 | ctx.gotInstance[method] = jest.fn(); 126 | ctx.httpServiceConfigProvider = { 127 | getConfig(): ServiceConfigType { 128 | return { 129 | enableTraceService: true, 130 | headersMap: { 131 | traceId: 'x-trace-id', 132 | }, 133 | excludeHeaders: [], 134 | }; 135 | }, 136 | }; 137 | 138 | ctx.traceDataService.getRequestData = () => ({ traceId: 'testId' }); 139 | 140 | const service = new HttpClientService( 141 | ctx.gotInstance, 142 | ctx.traceDataService, 143 | ctx.httpServiceConfigProvider, 144 | ); 145 | 146 | service[method]('', { headers: { test: 1 } }); 147 | 148 | expect(ctx.gotInstance[method]).toBeCalledWith('', { 149 | headers: { 'x-trace-id': 'testId', test: 1 }, 150 | }); 151 | }); 152 | }, 153 | ); 154 | }); 155 | -------------------------------------------------------------------------------- /src/http-client.service.ts: -------------------------------------------------------------------------------- 1 | import { Got, Options, Headers, Response, CancelableRequest } from 'got'; 2 | import { Inject, Optional } from '@nestjs/common'; 3 | import { GOT_INSTANCE, TRACE_DATA_SERVICE } from './di-token-constants'; 4 | import { ServiceConfigType } from './types/config.types'; 5 | import { TraceDataServiceInterface } from './types/trace-data-service.interface'; 6 | import { HttpServiceConfigProvider } from './http-service-config.provider'; 7 | import { HttpServiceConfigProviderInterface } from './http-service-config-provider.interface'; 8 | 9 | export class HttpClientService { 10 | private readonly clientConfig: ServiceConfigType; 11 | 12 | constructor( 13 | @Inject(GOT_INSTANCE) private readonly gotInstance: Got, 14 | @Optional() 15 | @Inject(TRACE_DATA_SERVICE) 16 | private readonly traceDataService: TraceDataServiceInterface, 17 | @Inject(HttpServiceConfigProvider) 18 | private readonly httpServiceConfigProvider: HttpServiceConfigProviderInterface, 19 | ) { 20 | this.clientConfig = this.httpServiceConfigProvider.getConfig(); 21 | } 22 | 23 | get( 24 | url: string, 25 | clientOpts: Options = {}, 26 | ): CancelableRequest> { 27 | const { headers } = clientOpts; 28 | const traceHeaders: Headers = this.getHeaders(); 29 | return this.gotInstance.get(url, { 30 | ...clientOpts, 31 | headers: { ...headers, ...traceHeaders }, 32 | }) as CancelableRequest>; 33 | } 34 | 35 | post( 36 | url: string, 37 | clientOpts: Options = {}, 38 | ): CancelableRequest> { 39 | const { headers } = clientOpts; 40 | const traceHeaders = this.getHeaders(); 41 | return this.gotInstance.post(url, { 42 | ...clientOpts, 43 | headers: { ...headers, ...traceHeaders }, 44 | }) as CancelableRequest>; 45 | } 46 | 47 | delete( 48 | url: string, 49 | clientOpts: Options = {}, 50 | ): CancelableRequest> { 51 | const { headers } = clientOpts; 52 | const traceHeaders = this.getHeaders(); 53 | return this.gotInstance.delete(url, { 54 | ...clientOpts, 55 | headers: { ...headers, ...traceHeaders }, 56 | }) as CancelableRequest>; 57 | } 58 | 59 | head( 60 | url: string, 61 | clientOpts: Options = {}, 62 | ): CancelableRequest> { 63 | const { headers } = clientOpts; 64 | const traceHeaders = this.getHeaders(); 65 | return this.gotInstance.head(url, { 66 | ...clientOpts, 67 | headers: { ...headers, ...traceHeaders }, 68 | }) as CancelableRequest>; 69 | } 70 | 71 | put( 72 | url: string, 73 | clientOpts: Options = {}, 74 | ): CancelableRequest> { 75 | const { headers } = clientOpts; 76 | const traceHeaders = this.getHeaders(); 77 | return this.gotInstance.put(url, { 78 | ...clientOpts, 79 | headers: { ...headers, ...traceHeaders }, 80 | }) as CancelableRequest>; 81 | } 82 | 83 | patch( 84 | url: string, 85 | clientOpts: Options = {}, 86 | ): CancelableRequest> { 87 | const { headers } = clientOpts; 88 | const traceHeaders = this.getHeaders(); 89 | return this.gotInstance.patch(url, { 90 | ...clientOpts, 91 | headers: { ...headers, ...traceHeaders }, 92 | }) as CancelableRequest>; 93 | } 94 | 95 | private getHeaders() { 96 | if (!this.shouldTraceServiceInvoke) { 97 | return {}; 98 | } 99 | const traceData = this.traceDataService.getRequestData(); 100 | 101 | const { headersMap } = this.clientConfig; 102 | 103 | return Object.entries(headersMap).reduce((acc, [propName, headerName]) => { 104 | acc[headerName] = traceData[propName]; 105 | return acc; 106 | }, {}); 107 | } 108 | 109 | get shouldTraceServiceInvoke() { 110 | if (this.clientConfig.enableTraceService && !this.traceDataService) { 111 | throw new Error( 112 | "You had enabled usage of TraceDataService, but didn't passed it", 113 | ); 114 | } 115 | return !!(this.clientConfig.enableTraceService && this.traceDataService); 116 | } 117 | 118 | get clientOpts() { 119 | return this.gotInstance.defaults.options; 120 | } 121 | 122 | get serviceConfig() { 123 | return this.clientConfig; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/http-service-config-provider.interface.ts: -------------------------------------------------------------------------------- 1 | import { ServiceConfigType } from './types/config.types'; 2 | 3 | export interface HttpServiceConfigProviderInterface { 4 | getConfig(): ServiceConfigType; 5 | } 6 | -------------------------------------------------------------------------------- /src/http-service-config.provider.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Optional } from '@nestjs/common'; 2 | import { merge } from 'lodash'; 3 | 4 | import { 5 | DEFAULT__SERVICE_CONFIG, 6 | FOR_INSTANCE__SERVICE_CONFIG, 7 | FOR_ROOT__SERVICE_CONFIG, 8 | } from './di-token-constants'; 9 | import { ServiceConfigType } from './types/config.types'; 10 | import { HttpServiceConfigProviderInterface } from './http-service-config-provider.interface'; 11 | 12 | @Injectable() 13 | export class HttpServiceConfigProvider 14 | implements HttpServiceConfigProviderInterface { 15 | constructor( 16 | @Inject(DEFAULT__SERVICE_CONFIG) 17 | private readonly defaultServiceConfig: ServiceConfigType, 18 | @Optional() 19 | @Inject(FOR_ROOT__SERVICE_CONFIG) 20 | private readonly forRootServiceConfig: ServiceConfigType = {}, 21 | @Optional() 22 | @Inject(FOR_INSTANCE__SERVICE_CONFIG) 23 | private readonly forInstanceServiceConfig: ServiceConfigType = {}, 24 | ) {} 25 | 26 | getConfig(): ServiceConfigType { 27 | return merge( 28 | {}, 29 | this.defaultServiceConfig, 30 | this.forRootServiceConfig, 31 | this.forInstanceServiceConfig, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-client.module'; 2 | export * from './http-client.service'; 3 | 4 | export * from './types/trace-data-service.interface'; 5 | export * from './types/trace-data-service.type'; 6 | export * from './types/config.types'; 7 | export * from './public-di-token.constants'; 8 | -------------------------------------------------------------------------------- /src/public-di-token.constants.ts: -------------------------------------------------------------------------------- 1 | export { TRACE_DATA_SERVICE } from './di-token-constants'; 2 | export const GOT_CONFIG = 'GOT_CONFIG'; 3 | export const HTTP_SERVICE_CONFIG = 'HTTP_SERVICE_CONFIG'; 4 | -------------------------------------------------------------------------------- /src/tests/unit.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { httpServiceConfigMock } from '../../test/fixtures/config-module/config-module.config.mock'; 3 | import { HttpClient } from '../http-client.module'; 4 | import { HttpClientService } from '../http-client.service'; 5 | 6 | import { TraceDataServiceMock } from '../../test/fixtures/trace-data-module/trace-data-service.mock'; 7 | import { TraceDataServiceModuleMock } from '../../test/fixtures/trace-data-module/trace-data-module.mock'; 8 | import { 9 | GOT_CONFIG, 10 | HTTP_SERVICE_CONFIG, 11 | TRACE_DATA_SERVICE, 12 | } from '../public-di-token.constants'; 13 | 14 | describe('HttpClient', () => { 15 | it('Test HttpService has all http methods', async () => { 16 | const methods = ['get', 'post', 'delete', 'head', 'put', 'patch']; 17 | const appModuleRef = await Test.createTestingModule({ 18 | imports: [ 19 | HttpClient.forRoot({ 20 | imports: [TraceDataServiceModuleMock], 21 | providers: [ 22 | { 23 | provide: TRACE_DATA_SERVICE, 24 | useClass: TraceDataServiceMock, 25 | }, 26 | ], 27 | }), 28 | HttpClient.forInstance(), 29 | ], 30 | }).compile(); 31 | 32 | const httpService = appModuleRef.get(HttpClientService); 33 | methods.forEach(method => { 34 | expect(httpService[method]).toBeInstanceOf(Function); 35 | }); 36 | }); 37 | 38 | it('Test HttpService without module opts', async () => { 39 | const appModuleRef = await Test.createTestingModule({ 40 | imports: [ 41 | HttpClient.forRoot({ 42 | imports: [TraceDataServiceModuleMock], 43 | providers: [ 44 | { 45 | provide: TRACE_DATA_SERVICE, 46 | useClass: TraceDataServiceMock, 47 | }, 48 | ], 49 | }), 50 | HttpClient.forInstance(), 51 | ], 52 | }).compile(); 53 | 54 | const { shouldTraceServiceInvoke } = appModuleRef.get(HttpClientService); 55 | expect(shouldTraceServiceInvoke).toBeFalsy(); 56 | }); 57 | 58 | it('Test enabling TraceService by config', async () => { 59 | const serviceConfigWithDisabledTraceService = { 60 | ...httpServiceConfigMock, 61 | enableTraceService: true, 62 | }; 63 | const appModuleRef = await Test.createTestingModule({ 64 | imports: [ 65 | HttpClient.forRoot({ 66 | imports: [TraceDataServiceModuleMock], 67 | providers: [ 68 | { 69 | provide: TRACE_DATA_SERVICE, 70 | useClass: TraceDataServiceMock, 71 | }, 72 | { 73 | provide: HTTP_SERVICE_CONFIG, 74 | useValue: serviceConfigWithDisabledTraceService, 75 | }, 76 | ], 77 | }), 78 | HttpClient.forInstance(), 79 | ], 80 | }).compile(); 81 | 82 | const { shouldTraceServiceInvoke } = appModuleRef.get(HttpClientService); 83 | expect(shouldTraceServiceInvoke).toBeTruthy(); 84 | }); 85 | 86 | it('Test for exception with enabled TraceService without passed one', async () => { 87 | const serviceConfigWithEnabledTraceService = { 88 | ...httpServiceConfigMock, 89 | enableTraceService: true, 90 | }; 91 | const appModuleRef = await Test.createTestingModule({ 92 | imports: [ 93 | HttpClient.forRoot({ 94 | imports: [TraceDataServiceModuleMock], 95 | providers: [ 96 | { 97 | provide: HTTP_SERVICE_CONFIG, 98 | useValue: serviceConfigWithEnabledTraceService, 99 | }, 100 | ], 101 | }), 102 | HttpClient.forInstance(), 103 | ], 104 | }).compile(); 105 | 106 | const httpClientService = appModuleRef.get(HttpClientService); 107 | expect(function readGetter() { 108 | return httpClientService.shouldTraceServiceInvoke; 109 | }).toThrow(); 110 | }); 111 | 112 | it('Test applying default options for enabled TraceService, if there are not specified', async () => { 113 | const serviceConfigWithDisabledTraceService = { enableTraceService: true }; 114 | const expectedConfig = { 115 | ...httpServiceConfigMock, 116 | enableTraceService: true, 117 | }; 118 | const appModuleRef = await Test.createTestingModule({ 119 | imports: [ 120 | HttpClient.forRoot({ 121 | imports: [TraceDataServiceModuleMock], 122 | providers: [ 123 | { 124 | provide: HTTP_SERVICE_CONFIG, 125 | useValue: serviceConfigWithDisabledTraceService, 126 | }, 127 | ], 128 | }), 129 | HttpClient.forInstance(), 130 | ], 131 | }).compile(); 132 | 133 | const { serviceConfig } = appModuleRef.get(HttpClientService); 134 | expect(serviceConfig).toEqual(expectedConfig); 135 | }); 136 | 137 | it('Test applying default options for enabled TraceService, if there are defined partially', async () => { 138 | const serviceConfigWithDisabledTraceService = { 139 | headersMap: { 140 | traceId: 'x-trace-id', 141 | }, 142 | enableTraceService: true, 143 | }; 144 | const expectedConfig = { 145 | ...httpServiceConfigMock, 146 | enableTraceService: true, 147 | }; 148 | const appModuleRef = await Test.createTestingModule({ 149 | imports: [ 150 | HttpClient.forRoot({ 151 | imports: [TraceDataServiceModuleMock], 152 | providers: [ 153 | { 154 | provide: HTTP_SERVICE_CONFIG, 155 | useValue: serviceConfigWithDisabledTraceService, 156 | }, 157 | ], 158 | }), 159 | HttpClient.forInstance(), 160 | ], 161 | }).compile(); 162 | 163 | const { serviceConfig } = appModuleRef.get(HttpClientService); 164 | expect(serviceConfig).toEqual(expectedConfig); 165 | }); 166 | 167 | it('dynamic modules has different scopes', async () => { 168 | await Test.createTestingModule({ 169 | imports: [ 170 | HttpClient.forRoot({ 171 | providers: [ 172 | { 173 | provide: GOT_CONFIG, 174 | useValue: { timeout: 999, retry: 5 }, 175 | }, 176 | ], 177 | }), 178 | HttpClient.forInstance(), 179 | ], 180 | }).compile(); 181 | 182 | const innerModuleRef = await Test.createTestingModule({ 183 | imports: [ 184 | HttpClient.forInstance({ 185 | providers: [ 186 | { 187 | provide: GOT_CONFIG, 188 | useValue: { retry: 10 }, 189 | }, 190 | ], 191 | }), 192 | HttpClient.forRoot(), 193 | ], 194 | }).compile(); 195 | 196 | const { clientOpts } = innerModuleRef.get(HttpClientService); 197 | expect(clientOpts.retry.limit).toEqual(10); 198 | }); 199 | 200 | it('For instance has priority', async () => { 201 | const module = await Test.createTestingModule({ 202 | imports: [ 203 | HttpClient.forRoot({ 204 | providers: [ 205 | { 206 | provide: GOT_CONFIG, 207 | useValue: { timeout: 999, retry: 5 }, 208 | }, 209 | ], 210 | }), 211 | HttpClient.forInstance({ 212 | providers: [ 213 | { 214 | provide: GOT_CONFIG, 215 | useValue: { timeout: 999, retry: 1 }, 216 | }, 217 | ], 218 | }), 219 | ], 220 | }).compile(); 221 | 222 | const { clientOpts } = module.get(HttpClientService); 223 | expect(clientOpts.retry.limit).toEqual(1); 224 | expect(clientOpts.timeout.request).toEqual(999); 225 | }); 226 | 227 | it('Should use default values if nothing overridden', async () => { 228 | const module = await Test.createTestingModule({ 229 | imports: [HttpClient.forRoot(), HttpClient.forInstance()], 230 | }).compile(); 231 | 232 | const service = module.get(HttpClientService); 233 | expect(service.clientOpts.retry.limit).toEqual(2); 234 | expect(service.serviceConfig.enableTraceService).toEqual(false); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /src/types/config.types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicModule, 3 | ForwardReference, 4 | Provider, 5 | Type, 6 | } from '@nestjs/common'; 7 | 8 | export type HttpClientForRootType = { 9 | imports?: Array< 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | Type | DynamicModule | Promise | ForwardReference 12 | >; 13 | providers?: Array; 14 | }; 15 | 16 | export type ServiceConfigType = { 17 | enableTraceService?: boolean; 18 | headersMap?: { 19 | [key: string]: string; 20 | }; 21 | excludeHeaders?: Array; 22 | }; 23 | -------------------------------------------------------------------------------- /src/types/trace-data-service.interface.ts: -------------------------------------------------------------------------------- 1 | import { TraceDataType } from './trace-data-service.type'; 2 | 3 | export interface TraceDataServiceInterface { 4 | getRequestData(): TraceDataType; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/trace-data-service.type.ts: -------------------------------------------------------------------------------- 1 | export type TraceDataType = { 2 | [key: string]: string; 3 | }; 4 | -------------------------------------------------------------------------------- /src/unique-providers-by-token.spec.ts: -------------------------------------------------------------------------------- 1 | import { uniqueProvidersByToken } from './utils'; 2 | 3 | describe('uniqueProvidersByToken', () => { 4 | it('Should return array with 1 provider', () => { 5 | const providers = uniqueProvidersByToken([ 6 | { provide: 'tokenA', useValue: 10 }, 7 | { provide: 'tokenA', useValue: 11 }, 8 | ]); 9 | 10 | expect(providers.length).toEqual(1); 11 | expect(providers).toEqual( 12 | expect.arrayContaining([ 13 | expect.objectContaining({ provide: 'tokenA', useValue: 11 }), 14 | ]), 15 | ); 16 | }); 17 | 18 | it('Should leave 2 different providers', () => { 19 | const providers = uniqueProvidersByToken([ 20 | { provide: 'tokenA', useValue: 10 }, 21 | { provide: 'tokenB', useValue: 11 }, 22 | ]); 23 | 24 | expect(providers.length).toEqual(2); 25 | expect(providers).toEqual( 26 | expect.arrayContaining([ 27 | expect.objectContaining({ provide: 'tokenB', useValue: 11 }), 28 | expect.objectContaining({ provide: 'tokenA', useValue: 10 }), 29 | ]), 30 | ); 31 | }); 32 | 33 | it('Should remove 1 provider and leave 2', () => { 34 | const providers = uniqueProvidersByToken([ 35 | { provide: 'tokenA', useValue: 10 }, 36 | { provide: 'tokenB', useValue: 10 }, 37 | { provide: 'tokenB', useValue: 11 }, 38 | ]); 39 | 40 | expect(providers.length).toEqual(2); 41 | expect(providers).toEqual( 42 | expect.arrayContaining([ 43 | expect.objectContaining({ provide: 'tokenB', useValue: 11 }), 44 | expect.objectContaining({ provide: 'tokenA', useValue: 10 }), 45 | ]), 46 | ); 47 | }); 48 | 49 | it('Should leave all class providers', () => { 50 | class A {} 51 | class B {} 52 | class C {} 53 | 54 | const providers = uniqueProvidersByToken([A, B, C]); 55 | 56 | expect(providers.length).toEqual(3); 57 | expect(providers).toContain(A); 58 | expect(providers).toContain(B); 59 | expect(providers).toContain(C); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common'; 2 | 3 | export function uniqueProvidersByToken(providers: Provider[]): Provider[] { 4 | return [ 5 | ...new Map( 6 | providers.map(provider => { 7 | const token = 8 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 9 | // @ts-ignore 10 | typeof provider.provide !== 'undefined' ? provider.provide : provider; 11 | return [token, provider]; 12 | }), 13 | ).values(), 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/config-module/config-module.config.mock.ts: -------------------------------------------------------------------------------- 1 | export const httpServiceConfigMock = { 2 | enableTraceService: true, 3 | headersMap: { 4 | traceId: 'x-trace-id', 5 | ip: 'x-real-ip', 6 | userAgent: 'user-agent', 7 | referrer: 'referrer', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/config-module/config-module.mock.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { httpServiceConfigMock } from './config-module.config.mock'; 3 | 4 | export const MODULE_CONFIG_TOKEN = 'MODULE_CONFIG_TOKEN'; 5 | 6 | @Module({ 7 | providers: [ 8 | { 9 | provide: MODULE_CONFIG_TOKEN, 10 | useValue: httpServiceConfigMock, 11 | }, 12 | ], 13 | exports: [MODULE_CONFIG_TOKEN], 14 | }) 15 | export class ConfigModuleMock {} 16 | -------------------------------------------------------------------------------- /test/fixtures/got-mock.ts: -------------------------------------------------------------------------------- 1 | import { RetryObject } from 'got'; 2 | 3 | export const calculateDelayMock = (obj: RetryObject) => obj.computedValue / 100; 4 | -------------------------------------------------------------------------------- /test/fixtures/trace-data-module/trace-data-module.mock.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TraceDataServiceMock } from './trace-data-service.mock'; 3 | 4 | @Module({ 5 | providers: [TraceDataServiceMock], 6 | exports: [TraceDataServiceMock], 7 | }) 8 | export class TraceDataServiceModuleMock {} 9 | -------------------------------------------------------------------------------- /test/fixtures/trace-data-module/trace-data-service.mock.ts: -------------------------------------------------------------------------------- 1 | import { TraceDataServiceInterface } from '../../../src/types/trace-data-service.interface'; 2 | 3 | export type TraceMockDataType = { 4 | traceId: string; 5 | ip?: string; 6 | referrer?: string; 7 | }; 8 | 9 | export class TraceDataServiceMock implements TraceDataServiceInterface { 10 | getRequestData(): TraceMockDataType { 11 | return { 12 | traceId: 'test-id', 13 | ip: '185.154.75.21', 14 | referrer: 'my-referrer.com/some/page.html', 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "testEnvironment": "node", 4 | "testTimeout": 10000, 5 | "rootDir": ".", 6 | "testRegex": ".e2e.ts$", 7 | "transform": { 8 | "^.+\\.(t|j)s$": "ts-jest" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/merge-options.e2e.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | HttpCode, 5 | INestApplication, 6 | Inject, 7 | Module, 8 | Post, 9 | } from '@nestjs/common'; 10 | import { Test, TestingModule } from '@nestjs/testing'; 11 | import { Got } from 'got'; 12 | import supertest from 'supertest'; 13 | import nock from 'nock'; 14 | import { HttpClient, HttpClientService } from '../src'; 15 | import { calculateDelayMock } from './fixtures/got-mock'; 16 | import { GOT_CONFIG } from '../src/public-di-token.constants'; 17 | 18 | describe('Use different options for GOT in different modules', () => { 19 | const mockUrl = 'http://example.domain'; 20 | let tryCounter = 0; 21 | const ctx: { 22 | appModule?: TestingModule; 23 | app?: INestApplication; 24 | http?: supertest.SuperTest; 25 | } = {}; 26 | 27 | @Controller('cat') 28 | class CatController { 29 | constructor(@Inject(HttpClientService) private readonly httpClient: Got) {} 30 | 31 | @Get('error') 32 | @HttpCode(500) 33 | private async getErrorHandler() { 34 | try { 35 | return await this.httpClient.get(`${mockUrl}/error`); 36 | } catch (err) { 37 | return err; 38 | } 39 | } 40 | } 41 | 42 | @Controller('root') 43 | class RootController { 44 | constructor( 45 | @Inject(HttpClientService) private readonly httpClient: HttpClientService, 46 | ) {} 47 | 48 | @Get('error') 49 | @HttpCode(500) 50 | private async getErrorHandler() { 51 | try { 52 | return await this.httpClient.get(`${mockUrl}/error`); 53 | } catch (err) { 54 | return err; 55 | } 56 | } 57 | } 58 | 59 | @Module({ 60 | imports: [ 61 | HttpClient.forInstance({ 62 | providers: [ 63 | { 64 | provide: GOT_CONFIG, 65 | useValue: { retry: { limit: 4 } }, 66 | }, 67 | ], 68 | }), 69 | ], 70 | controllers: [CatController], 71 | }) 72 | class CatModule {} 73 | 74 | @Module({ 75 | imports: [HttpClient.forInstance()], 76 | controllers: [RootController], 77 | }) 78 | class RootModule {} 79 | 80 | @Controller('dog') 81 | class DogController { 82 | constructor(@Inject(HttpClientService) private readonly httpClient: Got) {} 83 | 84 | @Get('error') 85 | @HttpCode(500) 86 | private async getErrorHandler() { 87 | try { 88 | return await this.httpClient.get(`${mockUrl}/error`); 89 | } catch (err) { 90 | return err; 91 | } 92 | } 93 | 94 | @Post('error') 95 | @HttpCode(500) 96 | private async postErrorHandler() { 97 | try { 98 | return await this.httpClient.post(`${mockUrl}/error`); 99 | } catch (err) { 100 | return err; 101 | } 102 | } 103 | } 104 | 105 | @Module({ 106 | imports: [ 107 | HttpClient.forInstance({ 108 | providers: [ 109 | { 110 | provide: GOT_CONFIG, 111 | useValue: { retry: { limit: 3 } }, 112 | }, 113 | ], 114 | }), 115 | ], 116 | controllers: [DogController], 117 | }) 118 | class DogModule {} 119 | 120 | beforeEach(async () => { 121 | nock(mockUrl) 122 | .get('/error') 123 | .times(20) 124 | .reply(500, () => { 125 | tryCounter += 1; 126 | return { error: 'error message' }; 127 | }); 128 | 129 | nock(mockUrl) 130 | .post('/error') 131 | .times(20) 132 | .reply(500, () => { 133 | tryCounter += 1; 134 | return { error: 'error message' }; 135 | }); 136 | 137 | ctx.appModule = await Test.createTestingModule({ 138 | imports: [ 139 | HttpClient.forRoot({ 140 | providers: [ 141 | { 142 | provide: GOT_CONFIG, 143 | useValue: { 144 | retry: { 145 | limit: 1, 146 | calculateDelay: calculateDelayMock, 147 | }, 148 | }, 149 | }, 150 | ], 151 | }), 152 | CatModule, 153 | DogModule, 154 | RootModule, 155 | ], 156 | }).compile(); 157 | 158 | ctx.app = ctx.appModule.createNestApplication(); 159 | await ctx.app.init(); 160 | 161 | ctx.http = supertest(ctx.app.getHttpServer()); 162 | 163 | tryCounter = 0; 164 | }); 165 | 166 | afterEach(async () => { 167 | await ctx.app.close(); 168 | nock.cleanAll(); 169 | }); 170 | 171 | it('should get correct retry option from instance', async () => { 172 | await ctx.http.get('/cat/error'); 173 | expect(tryCounter).toBe(5); 174 | }); 175 | 176 | it('should retry twice', async () => { 177 | await ctx.http.get('/dog/error'); 178 | expect(tryCounter).toBe(4); 179 | }); 180 | 181 | it('should get correct retry option from root', async () => { 182 | await ctx.http.get('/root/error'); 183 | expect(tryCounter).toBe(2); 184 | }); 185 | 186 | it('Should not retry with POST', async () => { 187 | await ctx.http.post('/dog/error'); 188 | expect(tryCounter).toBe(1); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /test/trace.e2e.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | HttpCode, 5 | INestApplication, 6 | Inject, 7 | Module, 8 | } from '@nestjs/common'; 9 | import { Test, TestingModule } from '@nestjs/testing'; 10 | import { Got } from 'got'; 11 | import supertest from 'supertest'; 12 | import nock from 'nock'; 13 | import { HttpClient, HttpClientService } from '../src'; 14 | import { 15 | GOT_CONFIG, 16 | HTTP_SERVICE_CONFIG, 17 | TRACE_DATA_SERVICE, 18 | } from '../src/public-di-token.constants'; 19 | import { TraceDataServiceModuleMock } from './fixtures/trace-data-module/trace-data-module.mock'; 20 | import { TraceDataServiceMock } from './fixtures/trace-data-module/trace-data-service.mock'; 21 | import { ConfigModuleMock } from './fixtures/config-module/config-module.mock'; 22 | import { httpServiceConfigMock } from './fixtures/config-module/config-module.config.mock'; 23 | 24 | describe('Trace data service', () => { 25 | const mockUrl = 'http://example.domain'; 26 | 27 | const ctx: { 28 | appModule?: TestingModule; 29 | app?: INestApplication; 30 | http?: supertest.SuperTest; 31 | } = {}; 32 | 33 | @Controller('cat') 34 | class CatController { 35 | constructor(@Inject(HttpClientService) private readonly httpClient: Got) {} 36 | 37 | @Get('transit') 38 | @HttpCode(200) 39 | private async getSuccessHandler() { 40 | return this.httpClient.get(`${mockUrl}/transit`).json(); 41 | } 42 | } 43 | 44 | @Module({ 45 | imports: [ 46 | HttpClient.forInstance({ 47 | imports: [ConfigModuleMock, TraceDataServiceModuleMock], 48 | providers: [ 49 | { 50 | provide: GOT_CONFIG, 51 | useValue: { retry: { limit: 1 } }, 52 | }, 53 | { 54 | provide: HTTP_SERVICE_CONFIG, 55 | useValue: { 56 | ...httpServiceConfigMock, 57 | excludeHeaders: ['referrer'], 58 | }, 59 | }, 60 | { provide: TRACE_DATA_SERVICE, useClass: TraceDataServiceMock }, 61 | ], 62 | }), 63 | ], 64 | controllers: [CatController], 65 | }) 66 | class CatModule {} 67 | 68 | beforeEach(async () => { 69 | nock(mockUrl) 70 | .get('/transit') 71 | .times(10) 72 | .reply(200, function handler() { 73 | return { headers: this.req.headers }; 74 | }); 75 | 76 | ctx.appModule = await Test.createTestingModule({ 77 | imports: [HttpClient.forRoot({}), CatModule], 78 | }).compile(); 79 | 80 | ctx.app = ctx.appModule.createNestApplication(); 81 | await ctx.app.init(); 82 | 83 | ctx.http = supertest(ctx.app.getHttpServer()); 84 | }); 85 | 86 | afterEach(async () => { 87 | await ctx.app.close(); 88 | nock.cleanAll(); 89 | }); 90 | 91 | it('should retry once', async () => { 92 | const expectedHeaders = { 93 | 'x-trace-id': 'test-id', 94 | 'x-real-ip': '185.154.75.21', 95 | // referrer: 'my-referrer.com/some/page.html', 96 | }; 97 | const { 98 | body: { headers }, 99 | } = await ctx.http 100 | .get('/cat/transit') 101 | .set('x-trace-id', expectedHeaders['x-trace-id']) 102 | .set('x-real-ip', expectedHeaders['x-real-ip']) 103 | .set('referrer', 'my-referrer.com/some/page.html'); 104 | 105 | Object.entries(expectedHeaders).forEach(([headerName, value]) => { 106 | expect(headers[headerName]).toEqual(value); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "esModuleInterop": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | --------------------------------------------------------------------------------