├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── apidoc.md ├── custom_service.md ├── decorators.md ├── http_client.md ├── logging.md ├── migration_guides │ └── 10_migrate_from_v6_to_v7.md ├── routes.md └── testing.md ├── examples ├── advanced │ ├── .npmrc │ ├── greetBusinessLogic.js │ ├── greetByGroup.md │ ├── index.js │ ├── package.json │ └── tests │ │ └── greetByGroup.test.js ├── basic │ ├── .npmrc │ ├── index.js │ └── package.json └── default.env ├── index.d.ts ├── index.js ├── lib ├── ajvSetup.js ├── decoratorsCommonFunctions.js ├── httpClient.js ├── postDecorator.js ├── preDecorator.js ├── rawCustomPlugin.js └── util.js ├── package.json ├── scripts └── update-version.sh └── tests ├── environment.test.js ├── fixtures └── keys │ ├── ca.crt │ ├── client.crt │ ├── client.key │ ├── server.crt │ └── server.key ├── getHeadersToProxy.test.js ├── getHttpClient.test.js ├── httpClient.test.js ├── index.test.js ├── oas.test.js ├── postDecorator.test.js ├── preDecorator.test.js ├── services ├── advanced-config.js ├── advanced-custom-service.js ├── advanced-first-level-properties-service.js ├── all-of-env-validation-custom-service.js ├── any-of-env-validation-custom-service.js ├── get-headers-to-proxy.js ├── http-client.js ├── if-then-else-env-validation-custom-service.js ├── one-of-env-validation-custom-service.js ├── overlapping-env-validation-custom-service.js ├── plain-custom-service.js ├── post-decorator.js ├── pre-decorator.js └── service-with-formats.js └── types ├── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Desktop (please complete the following information):** 24 | - Version [e.g. 22] 25 | - Node Version 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: tap 11 | versions: 12 | - 15.0.0 13 | - 15.0.1 14 | - 15.0.2 15 | - 15.0.3 16 | - 15.0.4 17 | - dependency-name: ajv 18 | versions: 19 | - 7.0.3 20 | - 7.0.4 21 | - 7.1.0 22 | - 7.1.1 23 | - 7.2.1 24 | - 7.2.3 25 | - 8.0.1 26 | - 8.0.3 27 | - 8.0.5 28 | - 8.1.0 29 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | tags: 10 | - '*' 11 | pull_request: 12 | branches: [ master ] 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [18.x, 20.x, 22.x] 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: npm i 30 | - run: npm run build --if-present 31 | - run: npm test 32 | - run: npm run coverage 33 | 34 | - name: Coveralls 35 | uses: coverallsapp/github-action@master 36 | with: 37 | github-token: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | publish: 40 | runs-on: ubuntu-latest 41 | needs: [build] 42 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 43 | steps: 44 | - uses: actions/checkout@v3 45 | - uses: JS-DevTools/npm-publish@v2 46 | with: 47 | token: ${{ secrets.NPM_TOKEN }} 48 | access: 'public' 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # We are a library, ignore lock files 76 | package-lock.json 77 | yarn.lock 78 | 79 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 80 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 81 | .idea 82 | # User-specific stuff 83 | .idea/**/workspace.xml 84 | .idea/**/tasks.xml 85 | .idea/**/usage.statistics.xml 86 | .idea/**/dictionaries 87 | .idea/**/shelf 88 | 89 | # Generated files 90 | .idea/**/contentModel.xml 91 | 92 | # Sensitive or high-churn files 93 | .idea/**/dataSources/ 94 | .idea/**/dataSources.ids 95 | .idea/**/dataSources.local.xml 96 | .idea/**/sqlDataSources.xml 97 | .idea/**/dynamic.xml 98 | .idea/**/uiDesigner.xml 99 | .idea/**/dbnavigator.xml 100 | 101 | # Gradle 102 | .idea/**/gradle.xml 103 | .idea/**/libraries 104 | 105 | # Gradle and Maven with auto-import 106 | # When using Gradle or Maven with auto-import, you should exclude module files, 107 | # since they will be recreated, and may cause churn. Uncomment if using 108 | # auto-import. 109 | # .idea/modules.xml 110 | # .idea/*.iml 111 | # .idea/modules 112 | 113 | # CMake 114 | cmake-build-*/ 115 | 116 | # Mongo Explorer plugin 117 | .idea/**/mongoSettings.xml 118 | 119 | # File-based project format 120 | *.iws 121 | 122 | # IntelliJ 123 | out/ 124 | 125 | # mpeltonen/sbt-idea plugin 126 | .idea_modules/ 127 | 128 | # JIRA plugin 129 | atlassian-ide-plugin.xml 130 | 131 | # Cursive Clojure plugin 132 | .idea/replstate.xml 133 | 134 | # Crashlytics plugin (for Android Studio and IntelliJ) 135 | com_crashlytics_export_strings.xml 136 | crashlytics.properties 137 | crashlytics-build.properties 138 | fabric.properties 139 | 140 | # Editor-based Rest Client 141 | .idea/httpRequests 142 | 143 | # Android studio 3.1+ serialized cache file 144 | .idea/caches/build_file_checksums.ser 145 | 146 | .vscode 147 | .vscode/* 148 | !.vscode/settings.json 149 | !.vscode/tasks.json 150 | !.vscode/launch.json 151 | !.vscode/extensions.json 152 | 153 | # Linux OS 154 | *~ 155 | 156 | # temporary files which can be created if a process still has a handle open of a deleted file 157 | .fuse_hidden* 158 | 159 | # KDE directory preferences 160 | .directory 161 | 162 | # Linux trash folder which might appear on any partition or disk 163 | .Trash-* 164 | 165 | # .nfs files are created when an open file is removed but is still being accessed 166 | .nfs* 167 | 168 | # macOS 169 | .DS_Store 170 | .AppleDouble 171 | .LSOverride 172 | 173 | # Icon must end with two \r 174 | Icon 175 | 176 | 177 | # Thumbnails 178 | ._* 179 | 180 | # Files that might appear in the root of a volume 181 | .DocumentRevisions-V100 182 | .fseventsd 183 | .Spotlight-V100 184 | .TemporaryItems 185 | .Trashes 186 | .VolumeIcon.icns 187 | .com.apple.timemachine.donotpresent 188 | 189 | # Directories potentially created on remote AFP share 190 | .AppleDB 191 | .AppleDesktop 192 | Network Trash Folder 193 | Temporary Items 194 | .apdisk 195 | 196 | examples/**/package-lock.json 197 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .nyc_output 3 | coverage 4 | examples 5 | scripts 6 | tests 7 | .editorconfig 8 | .nvmrc 9 | .travis.yml 10 | *.log 11 | *.tgz 12 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/iron 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | ## v7.0.1 - 2024-12-16 10 | 11 | ### Updated 12 | 13 | - axios: v1.7.9 14 | 15 | ## v7.0.0 - 2024-07-04 16 | 17 | ### BREAKING 18 | 19 | - dropped node 14 and node 16 support 20 | - dropped deprecated method `getServiceProxy` and `getDirectServiceProxy`, see the [migration guide](./docs/migration_guides/10_migrate_from_v6_to_v7.md) for further details. 21 | 22 | ### Updated 23 | 24 | - lc39: v8 25 | 26 | ## v6.0.3 27 | 28 | ### Updated 29 | 30 | - update axios to v1.6.7 31 | 32 | ## v6.0.2 - 2023-10-30 33 | 34 | ### Updated 35 | 36 | - update axios to v1.6.0 37 | 38 | ## v6.0.1 - 2023-10-20 39 | 40 | ### Changes 41 | - update @mia-platform/lc39 lib to v7.0.2 to fix the "fastify.close is not a function" error 42 | 43 | ## v6.0.0 - 2023-05-11 44 | 45 | ### BREAKING CHANGES 46 | 47 | - when returns stream, content-type `application/octet-stream` is not added by default anymore. So, you should add it on your own. See [here](https://github.com/fastify/fastify/pull/3086) for reference 48 | - upgrade lc39 to v7 and fastify to v4 49 | 50 | ## v5.1.7 - 2023-03-13 51 | 52 | ### Fixed 53 | 54 | - fix ts types 55 | 56 | ### v5.1.6 - 2023-02-08 57 | 58 | ### Fixed 59 | 60 | - merging of baseProperties with customProperties causing inconsistent microservices' env Variables schema 61 | 62 | ### Added 63 | 64 | - add optional metrics on request duration. The metrics collection is enabled by mean of the variable `ENABLE_HTTP_CLIENT_METRICS` (default: false) 65 | 66 | ## v5.1.5 - 2022-11-24 67 | 68 | ### Fixed 69 | 70 | - fix the handle of env var required with a configured default 71 | 72 | ## v5.1.4 - 2022-11-23 73 | 74 | THIS VERSION CONTAINS A BREAKING CHANGES 75 | 76 | This version contains a bug when it's configured a required env var with a default. The default is not handled before the default, so the service will not start. 77 | 78 | ### Added 79 | 80 | - add property `duration` to the httpClient response 81 | 82 | ### Fixed 83 | 84 | - [issue-123](https://github.com/mia-platform/custom-plugin-lib/issues/231): use if-then-else syntax at root level in json-schema of envs 85 | 86 | ## v5.1.3 - 2022-10-07 87 | 88 | ### Fixed 89 | 90 | - http client base options 91 | 92 | ## v5.1.2 - 2022-09-23 93 | 94 | ### Fixed 95 | 96 | - http client base options 97 | 98 | ## v5.1.1 - 2022-07-21 99 | 100 | ### Fixed 101 | 102 | - make `pino` a dependency 103 | 104 | ## v5.1.0 - 2022-07-08 105 | 106 | ### Fixed 107 | 108 | - fixed http client typings 109 | - base url with path prefix 110 | - httpsAgent in options 111 | 112 | ## v5.0.0 - 2022-05-13 113 | 114 | ### BREAKING CHANGES 115 | 116 | - upgrade lc39 to v6 117 | - drop Node support <14 118 | 119 | ### Added 120 | 121 | - add an http client method to replace deprecated `getServiceProxy` and `getDirectServiceProxy` (see all the features in the http client docs). 122 | Main breaking changes from the already existent `getServiceProxy` and `getDirectServiceProxy`: 123 | - streams respond with an object with headers, payload and statusCode. The payload has the stream interface 124 | - `allowedStatusCodes` array of status codes is replaced by the function `validateStatus` (which accept by default 2xx) 125 | - `agent` to configure the proxy is renamed to `proxy` and it is now an object 126 | - `port` and `protocol` are now accepted only in url and baseUrl 127 | - expose `getHeadersToProxy` function to calculate headers to proxy 128 | 129 | ### Deprecation 130 | 131 | - deprecate `getServiceProxy` and `getDirectServiceProxy` function. Please, substitute it with the `getHttpClient` function. 132 | 133 | ### Fixes 134 | 135 | - fix Mia header propagation (resolved [#161](https://github.com/mia-platform/custom-plugin-lib/issues/161)) 136 | 137 | ### Changes 138 | 139 | - upgrade dependencies 140 | 141 | ## v4.3.2 - 2022-02-15 142 | 143 | ### Fixes 144 | 145 | - Fixed typos and changed docs links inside `docs` directory 146 | - CVE-2022-0355 147 | 148 | ## v4.3.1 - 2022-02-02 149 | 150 | ### Fixes 151 | 152 | - Fixed error JSON.parse while passing both `returnAs:'BUFFER'` and `allowedStatusCode:[204]` 153 | 154 | ## v4.3.0 - 2022-01-26 155 | 156 | ### Added 157 | 158 | - Typings improvement 159 | 160 | ### Fixes 161 | 162 | - Fixed PostDecoratorDecoratedRequest typing 163 | 164 | ## v4.2.0 - 2021-10-15 165 | 166 | ### Added 167 | 168 | - optional configuration object to customize ajv formats and vocabulary when defining the custom plugin function 169 | 170 | ## v4.1.1 - 2021-09-28 171 | 172 | ### Added 173 | 174 | - Change `addValidatorSchema` to add also schema using `fastify.addSchema` methods to correctly resolve swagger and serialization. 175 | 176 | ## v4.1.0 - 2021-09-23 177 | 178 | ### Added 179 | 180 | - It is now possible to use ajv instance to extend schema using `addValidatorSchema`. Use fastify addSchema feature is not supported because the validator compiler is overwritten. 181 | - Add new function `getValidatorSchema` to get schema added to the validator. 182 | 183 | ## v4.0.0 - 2021-08-31 184 | 185 | ### BREAKING CHANGES 186 | 187 | - [@mia-platform/lc39](https://github.com/mia-platform/lc39) updated to v5 188 | - OpenAPI 3 is now the default version. Read the [official documentation](https://github.com/mia-platform/lc39/blob/master/docs/main-entrypoint.md#exposed-swagger-documentation) for more 189 | - Improved types definitions 190 | 191 | ## v3.1.1 - 2021-07-27 192 | 193 | ### Fixed 194 | 195 | - fix validation error message from other resource than body 196 | 197 | ## v3.1.0 - 2021-07-27 198 | 199 | ### Added 200 | 201 | - improve validation error message 202 | 203 | ## v3.0.0 - 2021-28-06 204 | 205 | ### BREAKING CHANGES 206 | 207 | - ajv formats are no longer supported. Validation can now be achieved using a pattern field and specifying a regular expression. 208 | 209 | This change is due to an incompatibility between fastify-env dependency env-schema and ajv formats. From ajv version 7.0.0 formats have been separated from the main package, requiring to be explicitly added. Visit ajv [official documentation](https://ajv.js.org/guide/formats.html#string-formats) and env-schema [official release page](https://github.com/fastify/env-schema/releases/tag/v3.0.0) for further details regarding this topic. 210 | 211 | - Environment variables using default values cannot be required anymore (and viceversa). 212 | 213 | Fastify-env dependency env-schema no longer support both required and default properties to be defined simultaneously. Please make sure your environment variables now respect this constraint. You can have further details regarding this issue [here](https://github.com/fastify/env-schema/blob/master/index.js#L51) 214 | 215 | ### Fixed 216 | 217 | - replaced expired test certificates with new ones that will expire on 3 September 3061 218 | 219 | ### Changed 220 | 221 | - the `getDirectServiceProxy` function now can receive a complete url as `serviceName` parameter 222 | 223 | - update dependencies to fastify v3 and lc39 to v4.0.0 224 | 225 | ## v2.3.0 - 2021-03-03 226 | 227 | - update lc39 to 3.3.0 228 | 229 | ## v2.2.0 - 2021-03-03 230 | 231 | ### Added 232 | 233 | - added tls options in service proxies 234 | 235 | ## v2.1.1 - 2020-10-26 236 | 237 | ### Fixed 238 | 239 | - change charset from `utf8` to `utf-8` 240 | 241 | ## v2.1.0 - 2020-10-21 242 | 243 | ### Added 244 | 245 | - add `timeout` option to service builder to make requests 246 | - add `agent` option to service builder to make requests 247 | 248 | ## v2.0.4 - 2020-10-08 249 | 250 | - update lc39 to 3.1.4 251 | 252 | ## v2.0.3 - 2020-10-02 253 | 254 | - update lc39 to 3.1.3 255 | 256 | ## v2.0.2 - 2020-09-29 257 | 258 | ### Added 259 | 260 | - updated lc39 dependency to 3.1.2 261 | 262 | ## v2.0.1 - 2020-09-22 263 | 264 | - updated lc39 dependency to 3.1.1 265 | 266 | ## v2.0.0 - 2020-09-17 267 | 268 | ### BREAKING CHANGES 269 | 270 | - Dropped support to Node 8 271 | - Update @mia-platform/lc39 2.2.2 -> 3.1.0. 272 | 273 | This update bring this breaking change: 274 | * Request and response logged information are now compliant with Mia-Platform logging guidelines. To see the guidelines, please check [Mia Platform Docs](https://docs.mia-platform.eu/docs/development_suite/api-console/api-design/guidelines-for-logs). You can find the implementation details [here](https://github.com/mia-platform/lc39/blob/master/lib/custom-logger.js) 275 | 276 | ### Added 277 | 278 | - Added `getUserProperties` to decorated `fastify.Request`, which returns the user properties 279 | 280 | ### Changed 281 | 282 | - Update ajv 6.10.2 -> 6.12.0 283 | - Update fastify-plugin 1.6.0 -> 1.6.1 284 | - Update simple-get 3.0.3 -> 3.1.0 285 | 286 | ## v1.1.1 - 2019-12-09 287 | ## Add 288 | - Add README 289 | 290 | ## v1.1.0 - 2019-12-09 291 | ## Add 292 | - advanced config in route 293 | 294 | ## v1.0.5 - 2019-09-09 295 | 296 | ### Changed 297 | 298 | - Update @types/node 12.7.1 -> 12.7.4 299 | - Moved URL construction from deprecated url.format to the new WHATWG API 300 | 301 | ## v1.0.4 - 2019-08-08 302 | 303 | ### Changed 304 | 305 | - Update @mia-platform/lc39 2.2.0 -> 2.2.2 306 | - Update @types/node 12.6.0 -> 12.7.1 307 | - Update ajv 6.10.1 -> 6.10.2 308 | 309 | ## v1.0.3 - 2019-07-08 310 | 311 | ### Changed 312 | 313 | - Update @mia-platform/lc39 2.1.2 -> 2.2.0 314 | - Update @types/node 12.0.2 -> 12.6.0 315 | - Update ajv 6.10.0 -> 6.10.1 316 | - Update http-errors 1.7.2 -> 1.7.3 317 | - Update fastify-env 0.6.2 -> 1.0.1 318 | 319 | ## v1.0.1 - 2019-05-28 320 | 321 | ### Changed 322 | 323 | - Improved condition for hinding error messages to users to avoid false positives 324 | 325 | ## v1.0.0 - 2019-05-23 326 | 327 | ### Added 328 | 329 | - Add new flag for filtering mia header injection 330 | - Add extra headers proxy during internal http calls 331 | 332 | ### Changed 333 | 334 | - Update repository to use lc39 and fastify 2 335 | - Modify error message on status code validation failure 336 | 337 | ## v1.0.0-rc.2 - 2019-05-08 338 | 339 | ### Changed 340 | 341 | - Fix wrong dependencies in `package.json` file 342 | 343 | ## v1.0.0-rc.1 - 2019-05-08 344 | 345 | ### Changed 346 | 347 | - Fix wrong encapsulation from fastify for custom functions 348 | 349 | ## v1.0.0-rc.0 - 2019-05-06 350 | 351 | ### Changed 352 | 353 | - Update repository to use lc39 and fastify 2 354 | - Modify error message on status code validation failure 355 | 356 | ## v0.7.0 - 2019-03-12 357 | 358 | ### Added 359 | 360 | - Add checks for `env` variables 361 | - Add response status code validation 362 | - Add new way to create a service proxy 363 | 364 | ## v0.6.0 - 2019-02-13 365 | 366 | ### Added 367 | 368 | - Add support for proxy custom headers to the services pipeline 369 | 370 | ## v0.5.2 - 2019-02-06 371 | 372 | ### Added 373 | 374 | - Add support for proxy custom headers to other services 375 | 376 | ### Changed 377 | 378 | - Update fastify-cli 0.22.0 -> 0.24.0 379 | 380 | ## v0.5.1 - 2018-10-19 381 | 382 | ### Added 383 | 384 | - Add support for trusted proxies 385 | 386 | ### Changed 387 | 388 | - Update fastify 1.9.0 -> 1.12.1 389 | - Update fastify-cli 0.19.0 -> 0.22.0 390 | 391 | ## v0.5.0 - 2018-08-23 392 | 393 | ### Added 394 | 395 | - Add more customization options for calling other services 396 | 397 | ## v0.4.0 - 2018-08-01 398 | 399 | Initial public release 400 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Mia service Node.js Library 4 | 5 | [![Build Status][travis-svg]][travis-org] 6 | [![javascript style guide][standard-mia-svg]][standard-mia] 7 | [![Coverage Status][coverall-svg]][coverall-io] 8 | [![NPM version][npmjs-svg]][npmjs-com] 9 | 10 |
11 | 12 | This library is intended to ease the [creation of new services](https://docs.mia-platform.eu/development_suite/api-console/api-design/services/) to deploy 13 | on [Mia-Platform][mia-platform]. 14 | Built on [`Fastify`][fastify], it takes advantage of Mia-Platform Node.js service launcher [`lc39`][lc39]. 15 | 16 | # Getting Started 17 | You can use this module in your projects or, from the [DevOps Console](https://docs.mia-platform.eu/development_suite/overview-dev-suite/), get started quickly and easily with a [ready-to-use microservice template](https://docs.mia-platform.eu/development_suite/api-console/api-design/custom_microservice_get_started/). Even in the [Mia-Platform Marketplace](https://github.com/mia-platform-marketplace) repository, you can find some examples and templates that using this library. 18 | 19 | ## Setup the local development environment 20 | 21 | To develop the service locally you need: 22 | - Node.js v12 or later. 23 | 24 | To setup node.js, we suggest using [nvm][nvm], so you can manage multiple versions easily. 25 | Once you have installed nvm, you can go inside the directory of the project and simply run 26 | `nvm install`, the `.nvmrc` file will install and select the correct version 27 | if you don’t already have it. 28 | 29 | Once you have all the dependency in place, you can launch: 30 | ```shell 31 | npm i 32 | npm run coverage 33 | ``` 34 | 35 | These two commands, will install the dependencies and run the tests with the coverage report that you can view as an HTML 36 | page in `coverage/lcov-report/index.html`. 37 | 38 | ## Install module 39 | 40 | To install the package with npm: 41 | 42 | ```sh 43 | npm i @mia-platform/custom-plugin-lib --save 44 | ``` 45 | To install with Yarn: 46 | 47 | ```sh 48 | yarn add @mia-platform/custom-plugin-lib 49 | ``` 50 | ## Define a Custom Service 51 | 52 | You can define a new Custom Service that integrates with the platform simply writing this: 53 | ```js 54 | const customService = require('@mia-platform/custom-plugin-lib')() 55 | 56 | module.exports = customService(async function helloWorldService(service) { 57 | service.addRawCustomPlugin('GET', '/hello', function handler(request, reply) { 58 | request.log.trace('requested myuser') 59 | // if the user is not logged, this method returns a falsy value 60 | const user = request.getUserId() || 'World' 61 | reply.send({ 62 | hello: `${user}!`, 63 | }) 64 | }) 65 | }) 66 | ``` 67 | - The library exports a function which creates the infrastructure ready to accept the definition of routes and decorators. Optionally can take a schema of the required environment variables, you can find the reference [here][fastify-env]. The function returned, `customService`, expects an async function to initialize and configure the `service`. 68 | - `service` is a [Fastify instance](https://www.fastify.io/docs/latest/Server/), that is decorated by the library to help you interact with Mia-Platform resources. You can use *service* to register any Fastify routes, custom decorations and plugin, see [here][fastify-ecosystem] for a list of currently available plugins. 69 | 70 | - `addRawCustomPlugin` is a function that requires the HTTP method, the path of the route and a handler. The handler can also be an [async function](https://www.fastify.io/docs/latest/Routes/#async-await). 71 | Optionally you can indicate the JSONSchemas to validate the querystring, the parameters, the payload and the response. 72 | 73 | To get more info about Custom Services can you look at the [related section](./docs/custom_service.md). 74 | 75 | ## Environment Variables configuration 76 | To works correctly, this library needs some specific environment variables: 77 | 78 | * `USERID_HEADER_KEY` 79 | * `USER_PROPERTIES_HEADER_KEY` 80 | * `GROUPS_HEADER_KEY` 81 | * `CLIENTTYPE_HEADER_KEY` 82 | * `BACKOFFICE_HEADER_KEY` 83 | * `MICROSERVICE_GATEWAY_SERVICE_NAME` 84 | 85 | When creating a new service from Mia-Platform DevOps Console, they come already defined but you can always change or add them anytime as described [in the DevOps console documentation](https://docs.mia-platform.eu/docs/development_suite/api-console/api-design/services#environment-variable-configuration). 86 | In local, the environment variables are defined 87 | in this [file](examples/default.env). 88 | 89 | Other variables can be specified by setting your envSchema when calling the plugin. 90 | 91 | ## Examples 92 | You can see an [advanced example](examples/advanced/) to see different use cases of the library. 93 | 94 | To see other examples of library use you can visit [GitHub dependant repository graph](https://github.com/mia-platform/custom-plugin-lib/network/dependents?package_id=UGFja2FnZS00NTc2OTY4OTE%3D) and check all packages that depends on it. 95 | 96 | To run the [examples](examples) directly you can move to specific example folder and run: 97 | 98 | ```sh 99 | npm run start:local 100 | ``` 101 | 102 | This command will launch the service on `localhost:3000` with the environment variables defined 103 | in this [file](examples/default.env). 104 | Now you can consult the swagger documentation of the service at 105 | [http://localhost:3000/documentation/](http://localhost:3000/documentation/). 106 | 107 | # How to 108 | 109 | * Create a Custom Service 110 | * Declare routes 111 | * Add decorators 112 | * Call the other services on the Platform project 113 | * API documentation 114 | * Testing 115 | * Logging 116 | 117 | [17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array 118 | 119 | [18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String 120 | 121 | [19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object 122 | 123 | [20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean 124 | 125 | [21]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise 126 | 127 | [22]: https://developer.mozilla.org/docs/Web/API/Comment/Comment 128 | 129 | [23]: https://daringfireball.net/projects/markdown/ 130 | 131 | [travis-svg]: https://travis-ci.org/mia-platform/custom-plugin-lib.svg?branch=master 132 | [travis-org]: https://travis-ci.org/mia-platform/custom-plugin-lib 133 | [standard-mia-svg]: https://img.shields.io/badge/code_style-standard--mia-orange.svg 134 | [standard-mia]: https://github.com/mia-platform/eslint-config-mia 135 | [coverall-svg]: https://coveralls.io/repos/github/mia-platform/custom-plugin-lib/badge.svg 136 | [coverall-io]: https://coveralls.io/github/mia-platform/custom-plugin-lib 137 | [npmjs-svg]: https://img.shields.io/npm/v/@mia-platform/custom-plugin-lib.svg?logo=npm 138 | [npmjs-com]: https://www.npmjs.com/package/@mia-platform/custom-plugin-lib 139 | 140 | [mia-platform]: https://www.mia-platform.eu/ 141 | [lc39]: https://github.com/mia-platform/lc39 142 | [nvm]: https://github.com/creationix/nvm 143 | 144 | [fastify]: https://www.fastify.io/ 145 | [fastify-env]: https://github.com/fastify/fastify-env 146 | [fastify-async]: https://www.fastify.io/docs/latest/Routes/#async-await 147 | [fastify-ecosystem]: https://www.fastify.io/ecosystem/ 148 | [fastify-parsers]: https://www.fastify.io/docs/latest/ContentTypeParser/ 149 | -------------------------------------------------------------------------------- /docs/apidoc.md: -------------------------------------------------------------------------------- 1 | # API documentation 2 | 3 | Services developed with this library automatically also exposes the documentation of the routes and decorators that 4 | are implemented. The documentation is specified using the [OpenAPI 2.0 standard](https://swagger.io/specification/v2/) 5 | and exhibited through [Swagger](https://swagger.io). 6 | 7 | Once the service is started, its documentation can be accessed at 8 | route [`http://localhost:3000/documentation`](http://localhost:3000/documentation). 9 | 10 | The specification of the request scheme 11 | and responses to a route must conform to the format accepted by 12 | [Fastify](https://www.fastify.io/docs/latest/Validation-and-Serialization). 13 | 14 | ## Example 15 | 16 | ```js 17 | const customService = require('@mia-platform/custom-plugin-lib')() 18 | 19 | const schema = { 20 | body: { 21 | type: 'object', 22 | properties: { 23 | someKey: { type: 'string' }, 24 | someOtherKey: { type: 'number' } 25 | } 26 | }, 27 | 28 | querystring: { 29 | name: { type: 'string' }, 30 | excitement: { type: 'integer' } 31 | }, 32 | 33 | params: { 34 | type: 'object', 35 | properties: { 36 | par1: { type: 'string' }, 37 | par2: { type: 'number' } 38 | } 39 | }, 40 | 41 | headers: { 42 | type: 'object', 43 | properties: { 44 | 'x-foo': { type: 'string' } 45 | }, 46 | required: ['x-foo'] 47 | } 48 | } 49 | 50 | module.exports = customService(async function exampleService(service) { 51 | service.addRawCustomPlugin('GET', '/endpoint', function handler(request,reply) { ... }, schema) 52 | }) 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/custom_service.md: -------------------------------------------------------------------------------- 1 | # Custom Service 2 | 3 | A Custom Microservice is a service that receives HTTP requests, whose cycle of use and deploy is managed by the platform. A Custom Microservice encapsulates ad-hoc business logics that can be developed by any user of the platform. To know how manage your services in the DevOps Console see the [documentation](https://docs.mia-platform.eu/docs/development_suite/api-console/api-design/services) 4 | 5 | The library exports a function which creates the infrastructure ready to accept the definition of routes and decorators. 6 | The function optionally can take a schema of the required environment variables (you can find the reference [fastify-env](https://github.com/fastify/fastify-env). 7 | 8 | ## Example 9 | 10 | ```js 11 | const customService = require('@mia-platform/custom-plugin-lib')({ 12 | type: 'object', 13 | required: ['ENV_VAR'], 14 | properties: { 15 | ENV_VAR: { type: 'string' }, 16 | }, 17 | }) 18 | ``` 19 | 20 | > **_More examples?_** Go [here](https://github.com/mia-platform/custom-plugin-lib/blob/master/examples/advanced/index.js) to see another use cases. 21 | 22 | You can configure the environment variables from DevOps console, in your service configuration. For further detail see [Mia-Platform documentation](https://docs.mia-platform.eu/docs/development_suite/api-console/api-design/services#environment-variable-configuration). 23 | 24 | The customService function expects two arguments: 25 | 26 | - an async function to initialize and configure the `service`, a [Fastify instance](https://www.fastify.io/docs/latest/Server/); 27 | - an optional `serviceOptions` object useful for configuring the plugins used by the library. 28 | 29 | You can access the environment variables values from `service.config`: 30 | 31 | ```js 32 | module.exports = customService(async function handler(service) { 33 | const { ENV_VAR } = service.config 34 | ... 35 | }, serviceOptions) 36 | ``` 37 | 38 | Upon `service`, you can you can add [routes](./routes.md) and [decorators](./decorators.md). 39 | 40 | The `serviceOptions` argument supports the following properties: 41 | 42 | - `ajv`: an object useful to customize certain configurations of the `ajv` instance used by the service: 43 | - `vocabulary`: a list of strings used to setup a custom keyword vocabulary, 44 | keep in mind that if you whish to extend the OAS specification generated by your services and create a valid API specification, each 45 | vocabulary entry **must** start with the `x-` prefix, check out the [OAS specification](https://swagger.io/docs/specification/openapi-extensions/) for further details; 46 | - `plugins`: allows setting up the different plugins used by the service: 47 | - `ajv-formats` (`^2.1.1`): with this option you can configure certain [`ajv-formats`](https://github.com/ajv-validator/ajv-formats) configurations 48 | - `formats`: a list of strings used to setup the formats that should be allowed when validating schemas 49 | 50 | An example of service options is the following: 51 | 52 | ```js 53 | const serviceOptions = { 54 | ajv: { 55 | plugins: { 56 | 'ajv-formats': { formats: ['date-time'] }, 57 | }, 58 | vocabulary: ['x-name'], 59 | }, 60 | } 61 | module.exports = customService(async function myService(service) { 62 | ... 63 | }, serviceOptions) 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/decorators.md: -------------------------------------------------------------------------------- 1 | # Add decorators 2 | 3 | Decorators are particular endpoint that a microservice can expose. Using the DevOps console you can manage your decorators and link them to your endpoint routes. Check out [decorators documentation](https://docs.mia-platform.eu/docs/development_suite/api-console/api-design/decorators) for further detail on their usage and management. 4 | 5 | Decorators allow you to perform custom actions upon specific API handler invocations. There are three types of decorators: 6 | 7 | * **PRE**: invoked *before* the configured route handler. 8 | * **POST**: invoked *after* the successful execution of configured route handler (a 2xx status code is returned). 9 | * **CATCH**: invoked after the failure of the configured route handler (any other error status code, 4xx or 5xx). 10 | 11 | You can add a decorator with these methods: 12 | 13 | * ```addPreDecorator(path, handler)``` 14 | * ```addPostDecorator(path, handler)``` 15 | 16 | whose arguments are, in order: 17 | 18 | * `path` - the route path (e.g.,`/status /alive`). 19 | * `handler`- function that contains the actual behavior. It must respect the same interface defined in the 20 | documentation of the handlers of [fastify](https://www.fastify.io/docs/latest/Routes/#routes-config). 21 | 22 | ## PRE decorators 23 | 24 | ```js 25 | const customService = require('@mia-platform/custom-plugin-lib')() 26 | 27 | module.exports = customService(async function handler(service) { 28 | service.addPreDecorator('/checkwho', function checkWhoHandler(request) { 29 | const defaultWho = 'John Doe' 30 | const body = request.getOriginalRequestBody() 31 | const { who } = body 32 | const newBody = { 33 | ...body, 34 | who: who || request.getUserId() || defaultWho, 35 | } 36 | // Set original request with retrieved data, the target microservice will receive your newly defined body. 37 | return request.changeOriginalRequest().setBody(newBody) 38 | }) 39 | }) 40 | ``` 41 | 42 | The first parameter of the handler function is [Request](https://www.fastify.io/docs/latest/Request/). The request is decorated as the `addRawCustomPlugin` method, in addition, with the following methods: 43 | 44 | * `getOriginalRequest()` - returns the original request. 45 | * `getOriginalRequestMethod()` - returns the original request HTTP method. 46 | * `getOriginalRequestPath()` - returns the path of the original request. 47 | * `getOriginalRequestHeaders()` - returns the headers of the original request. 48 | * `getOriginalRequestQuery()` - returns the querystring of the original request. 49 | * `getOriginalRequestBody()` - returns the body of the original request. 50 | * `changeOriginalRequest()`- returns a builder object with following methods: 51 | * `setBody(newBody)` - modify the body of the original request. 52 | * `setHeaders(newHeaders)` - modify the headers of the original request. 53 | * `setQuery(newPath)` - modify the querystring of the original request. 54 | * `leaveOriginalRequestUnmodified()` - leave the original request unmodified . 55 | 56 | ## POST and CATCH decorators 57 | 58 | ```js 59 | ... 60 | /* 61 | POST decorator 62 | */ 63 | service.addPostDecorator('/notify', function notifyHandler(request) { 64 | // Get "notifications" setting from the request querystring 65 | const { notifications } = request.getOriginalRequestQuery() 66 | if(!notifications) { 67 | // It's not necessary to send notification 68 | // leave the original response unmodified 69 | return req.leaveOriginalResponseUnmodified() 70 | } 71 | 72 | const notifier = new Notifier() 73 | // Try to send a notification 74 | const response = await notifier.send({ text: `${who} says: ${mymsg}`}) 75 | // Adds to original response body the time of notification send 76 | return request.changeOriginalResponse().setBody( 77 | { ...req.getOriginalRequestBody(),notifySendedAt:new Date() } 78 | ) 79 | 80 | /* 81 | CATCH decorator 82 | */ 83 | service.addPostDecorator('/catch', function exceptionHandler(request) { 84 | return request.changeOriginalResponse() 85 | .setBody({msg:"Error"}) 86 | .setStatusCode(500) 87 | }) 88 | ``` 89 | 90 | Even in this case the first parameter of the handler function is [Request](https://www.fastify.io/docs/latest/Request/). The request is decorated as the `addRawCustomPlugin` method, in addition, with the following methods: 91 | 92 | * `getOriginalRequest()` - returns the original request. 93 | * `getOriginalRequestMethod()` - returns the original request method. 94 | * `getOriginalRequestPath()` - returns the path of the original request. 95 | * `getOriginalRequestHeaders()` - returns the headers of the original request. 96 | * `getOriginalRequestQuery()` - returns the querystring of the original request. 97 | * `getOriginalRequestBody()` - returns the body of the original request. 98 | 99 | Related to the original response: 100 | 101 | * `getOriginalResponseBody()` - returns the body of the original response. 102 | * `getOriginalResponseHeaders()` - returns the headers of the original response. 103 | * `getOriginalResponseStatusCode()` - returns the status code of the original response. 104 | * `changeOriginalResponse()`- returns a builder object with following methods: 105 | * `setBody(newBody)` - modify the body of the original response. 106 | * `setHeaders(newHeaders)` - modifies the headers of the original response. 107 | * `setStatusCode(newCode)` - changes the status code of the original response. 108 | * `leaveOriginalResponseUnmodified()` - leaves the original response unmodified. 109 | 110 | ## Abort chain 111 | 112 | To abort the decorator chain, you can call on the `request` the method: 113 | 114 | `abortChain(finalStatusCode, finalBody, finalHeaders)` 115 | 116 | whose arguments are, in order 117 | 118 | * `finalStatusCode` - the final returned status code. 119 | * `finalBody` - the final returned body. 120 | * `finalHeaders` - the final returned headers. 121 | 122 | > **_More examples?_** Go [here](https://github.com/mia-platform/custom-plugin-lib/blob/master/examples/advanced/index.js) to see another decorators implementations. 123 | -------------------------------------------------------------------------------- /docs/http_client.md: -------------------------------------------------------------------------------- 1 | # Call the other services on the Platform project 2 | 3 | You can call any service or any endpoint defined on the Platform project, obtaining and using a proxy object. 4 | 5 | For example, if you need to connect to a CRUD, you have to use a Proxy towards the `crud-service`. 6 | 7 | You can get a proxy calling these methods both on `Request` (the first argument of handler) and `Service` (the Fastify instance): 8 | 9 | * `getHttpClient(baseURL, options)` - returns an http client configured with the given base URL and options. 10 | * `baseURL` - the base URL of the service, with protocol, host and it is possible to add a base prefix. The prefix must ends with a slash. Keep in mind that in method functions you must start the path without slash if you want to reuse the path prefix. 11 | * `options` - an object with the following optional fields: 12 | * `headers` - an object that represents the set of headers to send to the service 13 | * `timeout` - set a request timeout 14 | * `cert` - set a custom certificate to use for the requests 15 | * `key` - set a custom key to use for the requests 16 | * `ca` - set a custom ca to use for the requests 17 | * `logger` - the Pino logger instance, it is used to log request (headers, payload and url) and response (headers, payload and status code) in trace level and error message if there is an error during the API call. If not passed, no log are printed. Keep in mind that headers, payload and url could contains sensitive information. If it is the case, do not pass the logger instance or use the redact options to hide the sensitive information ([read here](https://docs.mia-platform.eu/docs/runtime_suite_libraries/lc39/service-options) for more information). 18 | * `isMiaHeaderInjected` - a boolean value that identifies whether Mia's headers should be forwarded in the request. Default `true`. 19 | * `httpsAgent` - an instance of `require('https').Agent` that will be used for the requests, only if `cert`, `key` and `ca` are not configured. 20 | 21 | Potentially, the `getHttpClient` method allows you to also query services outside the Platform. In this case, however, it is necessary to bear in mind that the platform headers will be automatically forwarded. You can do it by setting the `isMiaHeaderInjected` option value to false. 22 | 23 | The http client created from the Request by default forwards the four Mia headers to the service called. In addition, other headers of the original request can also be forwarded to the named service. To do this it is necessary to define an additional environment variable, `ADDITIONAL_HEADERS_TO_PROXY`, whose value must be a string containing the keys of the headers to be forwarded separated by a comma. 24 | 25 | The client expose the methods to perform a specific HTTP request to service. 26 | 27 | * `get(path, options)` 28 | * `post(path, body, options)` 29 | * `put(path, body, options)` 30 | * `patch(path, body, options)` 31 | * `delete(path, body, options)` 32 | 33 | The params to be passed to these functions are: 34 | 35 | * `path` *required* - a string which identifies the route to which you want to send the request (it handles also the query string). Keep in mind that if base url contains a prefix, you must start the path without slash if you want to reuse the path prefix. 36 | * `body` - the body of the request which can be: 37 | * a JSON object 38 | * a [Buffer](https://nodejs.org/api/buffer.html#) 39 | * one [Stream](https://nodejs.org/api/stream.html) 40 | * `options` - optional, an object that admits all the `options` listed above for the `getHttpHeader` methods (which will eventually be overwritten), plus the following fields: 41 | * `returnAs` - a string that identifies the format in which you want to receive the response. It can be `JSON`,`BUFFER` or `STREAM`. Default `JSON`. 42 | * `validateStatus` - a function which returns a boolean indicating whether the status code is valid or not. Default `(status) => status >= 200 && status < 300`. 43 | * `errorMessageKey` - key of the response object (if response in JSON) that will be used to identify the error message. Default `message`. 44 | * `proxy`: object that contains the following fields: 45 | * `protocol`: 'http' or 'https' 46 | * `host`: host of the proxy service 47 | * `port`: port of the proxy service in number format 48 | * `auth`: object that contains the following fields: 49 | * `username`: username to use for the proxy authentication 50 | * `password`: password to use for the proxy authentication 51 | * `query`: object that will be stringified and add to the query 52 | 53 | All methods return a *Promise object*. You can access to: 54 | 55 | * **Status code** of the response trough the `statusCode` property 56 | * **Body** of the response trough the `payload` property 57 | * **Headers** of the response trough the `headers` property 58 | * **Duration** of the http call trough the `duration` property 59 | 60 | If service responds with status code not valid (it is possible to modify this using the `validateStatus` ), the error object contains: 61 | 62 | * **Message** of the response trough the `message` property (or it is possible to customize the key using the `errorMessageKey` option) if response is in JSON. Otherwise, it returns a default error message `Something went wrong` 63 | * **Status code** of the response trough the `statusCode` property 64 | * **Body** of the response trough the `payload` property 65 | * **Headers** of the response trough the `headers` property 66 | * **Duration** of the http call trough the `duration` property 67 | 68 | If error is not an http error, it is throw the error message and the error code. 69 | 70 | ## Examples 71 | 72 | ```js 73 | async function tokenGeneration(request, response) { 74 | const crudProxy = request.getHttpClient('http://my-service/base-path/') 75 | const result = await crudProxy 76 | .post('/tokens-collection/', { 77 | id: request.body.quotationId, 78 | valid: true 79 | }) 80 | 81 | const tokens=result.payload; 82 | // ... 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | You can log a message to see in DevOps console. The library use the [Fastify logging system](https://www.fastify.io/docs/v2.0.x/Logging/), that is based on [pino](https://github.com/pinojs/pino). 4 | 5 | To log messages call these methods on the logger instance of request. Logging is enabled by default. Therefore you can call on `request.log` or `service.log`: 6 | 7 | * `debug()` 8 | * `info()` 9 | * `warn()` 10 | * `error()` 11 | * `fatal()` 12 | 13 | Each method creates a log with the homonym level. 14 | 15 | By default the library will generate two logs for each request, one representing the incoming request and one for the request completion, logs are created with *trace* and *info* levels respectively and already provide useful information for later analysis or debugging. If you need more, you can add your logs. 16 | 17 | ## Example 18 | 19 | ```js 20 | service.addPostDecorator('/notify', function notifyHandler(request) { 21 | // Get "notifications" setting from the request querystring 22 | const { notifications } = request.getOriginalRequestQuery() 23 | if(!notifications) { 24 | return req.leaveOriginalResponseUnmodified() 25 | } 26 | 27 | try { 28 | const notifier = new Notifier() 29 | const response = await notifier.send({ text: `${who} says: ${mymsg}`}) 30 | const sendedAt = new Date(); 31 | 32 | // Log at "INFO" level 33 | req.log.info({ statusCode: response.statusCode }, 'Notify sent') 34 | 35 | return request.changeOriginalResponse().setBody( 36 | { ...req.getOriginalRequestBody(), notifySendedAt:sendedAt} 37 | ) 38 | } catch (error) { 39 | // Log at "ERROR" level 40 | req.log.error({ error }, 'Error sending notification') 41 | } 42 | ) 43 | ``` 44 | 45 | For further detail about logs can you see the [guidelines for logs](https://docs.mia-platform.eu/docs/development_suite/monitoring/resources/pods#pod-logs). 46 | -------------------------------------------------------------------------------- /docs/migration_guides/10_migrate_from_v6_to_v7.md: -------------------------------------------------------------------------------- 1 | # Migrate from v6 to v7 2 | 3 | With v7 the `getServiceProxy` and `getDirectServiceProxy` methods have been removed. 4 | 5 | In order to upgrade to v7 you need to change the implementation using such methods to use another HTTP Client. 6 | 7 | :::tip 8 | 9 | Custom Plugin Lib already provides the [`getHttpClient`](../http_client.md) method to build an axios-based HTTP client since [v5.0.0](../../CHANGELOG.md#v500---2022-05-13). 10 | 11 | Main breaking changes from the already existent `getServiceProxy` and `getDirectServiceProxy`: 12 | 13 | - streams respond with an object with headers, payload and statusCode. The payload has the stream interface 14 | - `allowedStatusCodes` array of status codes is replaced by the function `validateStatus` (which accept by default 2xx) 15 | - `agent` to configure the proxy is renamed to `proxy` and it is now an object 16 | - `port` and `protocol` are now accepted only in url and baseUrl 17 | 18 | ::: 19 | 20 | ## Migrate getDirectServiceProxy 21 | 22 | ```js 23 | const proxy = fastify.getServiceProxy('my-service', {}) 24 | // becomes 25 | const proxy = fastify.getHttpClient('http://microservice-gateway/', {}) 26 | ``` 27 | 28 | ## Migrate getServiceProxy 29 | 30 | ```js 31 | const proxy = fastify.getServiceProxy('my-service', {}) 32 | // becomes 33 | const proxy = fastify.getHttpClient('http://my-service/', {}) 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/routes.md: -------------------------------------------------------------------------------- 1 | # Declare routes 2 | 3 | You can define the behavior of the Custom Microservice in response to an HTTP request by declaring the routes. For this purpose, you can use the `addRawCustomPlugin` method: 4 | 5 | ```js 6 | service.addRawCustomPlugin(httpVerb, path, handler, schema) 7 | ``` 8 | 9 | whose arguments are, in order 10 | 11 | * `httpVerb` - the HTTP verb of the request (e.g.,`GET`). 12 | * `path` - the route path (e.g.,`/status /alive`). 13 | * `handler` - function that contains the actual behavior. It must respect the same interface defined in the 14 | documentation of the handlers of [fastify](https://www.fastify.io/docs/latest/Routes/#async-await). 15 | * `schema` - definition of the request and response data schema. 16 | The format is the one accepted by [fastify](https://www.fastify.io/docs/latest/Validation-and-Serialization). To further detail see [`related section`](./apidoc.md). 17 | 18 | To get more info about how to declare a route can you look at the related [Fastify documentation](https://github.com/fastify/fastify/blob/master/docs/Routes.md). 19 | 20 | ## Example 21 | 22 | ```js 23 | const customService = require('@mia-platform/custom-plugin-lib')() 24 | 25 | module.exports = customService(async function handler(service) { 26 | service.addRawCustomPlugin('GET', '/hello', function helloHandler(request, reply) { 27 | const user = request.getUserId() || 'World' 28 | 29 | reply.send({ 30 | hello: `${user}!`, 31 | }) 32 | }) 33 | }) 34 | ``` 35 | 36 | > **_More examples?_** Go [here](https://github.com/mia-platform/custom-plugin-lib/blob/master/examples/advanced/index.js) to see another `addRawCustomPlugin` uses case. 37 | 38 | * The first parameter of the handler function is [Request](https://www.fastify.io/docs/latest/Request/). The request is automatically decorated, indeed we can call `request.getUserId()`. 39 | 40 | The instance of `Request` is decorated with functions: 41 | 42 | * `getUserId()` - exposes the user's id, if logged in or `null`. 43 | * `getGroups()` - exposes an array containing strings that identify the groups to which the logged-in user belongs. 44 | * `getClientType()` - exposes the type of client that performed the HTTP request. 45 | * `isFromBackOffice()` - exposes a boolean to discriminate whether the HTTP request from the CMS. 46 | * `getMiaHeaders()` - exposes an object with all previous information. 47 | The set of this data is called `Mia headers` and getting the values from the following environment variables: 48 | * *USERID_HEADER_KEY* 49 | * *GROUPS_HEADER_KEY* 50 | * *CLIENTTYPE_HEADER_KEY* 51 | * *BACKOFFICE_HEADER_KEY* 52 | * `getHeadersToProxy({isMiaHeaderInjected})` - returns the object of the request headers to proxy in API calls, using the extra headers, the mia headers and the headers set with variable `CUSTOM_HEADERS_TO_PROXY`. 53 | 54 | * The second parameter is a [Reply instance](https://www.fastify.io/docs/latest/Reply/). Use this object to reply to the request. Its main methods are the following: 55 | * `headers(object)` - sets the headers of the response. 56 | * `code(statusCode)` - sets the HTTP status code of the response. 57 | * `send(data)` - sends the payload `data` to the end user. 58 | 59 | * Inside the handler scope it's possible to access Fastify instance using `this`. 60 | 61 | ## Adding a shared schema 62 | 63 | It is possible to add shared schema between routes. For this purpose, you can access to the `ajv` instance used to perform route validation. It is possible to add a schema to the `ajv` instance using the `addValidatorSchema` method. `addValidatorSchema` method also adds schema to fastify using *fastify.addSchema* function. It is also possible to get the added schema using the `getValidatorSchema` method. 64 | 65 | ```js 66 | const customService = require('@mia-platform/custom-plugin-lib')() 67 | 68 | module.exports = customService(async function handler(service) { 69 | service.addValidatorSchema({ 70 | $id: 'example', 71 | type: 'object', 72 | properties: { 73 | foobar: { 74 | type: 'string', 75 | } 76 | } 77 | }) 78 | 79 | const exampleSchema = service.getValidatorSchema('example') 80 | console.log('Log the example schema', exampleSchema) 81 | 82 | service.addRawCustomPlugin('GET', '/hello', function helloHandler(request, reply) { 83 | const user = request.getUserId() || 'World' 84 | 85 | reply.send({ 86 | hello: `${user}!`, 87 | }) 88 | }, { 89 | body: { 90 | $ref: 'example#' 91 | } 92 | }) 93 | }) 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | `Mia service Node.js Library` is built on Fastify and therefore integrates with [testing tools](https://www.fastify.io/docs/latest/Testing/) 4 | made available by the framework. A complete example of this type of test is available [here](https://github.com/mia-platform/custom-plugin-lib/tree/master/examples/advanced/tests). 5 | 6 | ## Integration and Unit test 7 | 8 | The testing of service can be performed at multiple levels of abstraction. One possibility is to use a technique called _fake http injection_ for which it is possible to simulate 9 | receiving an HTTP request. In this way, all the service logic is exercised from the HTTP layer to the handlers. This pattern is an example of Integration Testing. 10 | 11 | ### Example Integration Test 12 | 13 | In the example below the test framework [Mocha](https://mochajs.org/). 14 | 15 | ```js 16 | 'use strict' 17 | 18 | const assert = require('assert') 19 | const fastify = require('fastify') 20 | 21 | const customPlugin = require('@mia-platform/custom-plugin-lib')() 22 | const index = customPlugin(async service => { 23 | service.addRawCustomPlugin( 24 | 'GET', 25 | '/status/alive', 26 | async (request, reply) => ({ 27 | status: 'ok' 28 | }) 29 | ) 30 | }) 31 | 32 | const createTestServer = () => { 33 | // Silent => trace for enabling logs 34 | const createdServer = fastify({ logger: { level: 'silent' } }) 35 | 36 | createdServer.register(index) 37 | return createdServer 38 | } 39 | 40 | describe('/status/alive', () => { 41 | it('should be available', async () => { 42 | const server = createTestServer() 43 | 44 | const response = await server.inject({ 45 | url: '/status/alive', 46 | }) 47 | 48 | assert.equal(response.statusCode, 200) 49 | }) 50 | }) 51 | ``` 52 | -------------------------------------------------------------------------------- /examples/advanced/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /examples/advanced/greetBusinessLogic.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | class BusinessLogic { 20 | constructor(groupToGreet) { 21 | this.groupToGreet = groupToGreet 22 | } 23 | greet(user, groups) { 24 | const userString = user || 'World' 25 | if (groups.includes(this.groupToGreet)) { 26 | return { 27 | message: `Hello ${userString} of group: ${this.groupToGreet}!\n`, 28 | user, 29 | groups, 30 | } 31 | } 32 | 33 | return { 34 | message: `Hello ${userString}!\n`, 35 | user, 36 | groups, 37 | } 38 | } 39 | } 40 | 41 | module.exports = BusinessLogic 42 | -------------------------------------------------------------------------------- /examples/advanced/greetByGroup.md: -------------------------------------------------------------------------------- 1 | # Greet by group 2 | 3 | This endpoint greets you only if your groups include a special one 4 | This is a markdown `yeah!` 5 | -------------------------------------------------------------------------------- /examples/advanced/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const { readFileSync } = require('fs') 20 | const { join } = require('path') 21 | const customService = require('@mia-platform/custom-plugin-lib')({ 22 | type: 'object', 23 | required: ['GROUP_TO_GREET'], 24 | properties: { 25 | GROUP_TO_GREET: { type: 'string' }, 26 | }, 27 | }) 28 | 29 | const BusinessLogic = require('./greetBusinessLogic') 30 | 31 | const greetByGroupSchema = { 32 | description: readFileSync(join(__dirname, 'greetByGroup.md'), { encoding: 'utf-8' }), 33 | tags: ['Greet by Group'], 34 | response: { 35 | 200: { 36 | type: 'object', 37 | properties: { 38 | message: { type: 'string', description: 'the greeting message' }, 39 | user: { type: 'string', description: 'the user' }, 40 | groups: { type: 'array', items: { type: 'string' } }, 41 | }, 42 | }, 43 | }, 44 | } 45 | 46 | const sumSchema = { 47 | description: 'Sum an array of numbers', 48 | tags: ['Sum'], 49 | body: { 50 | type: 'array', 51 | items: { type: 'number' }, 52 | }, 53 | response: { 54 | 200: { type: 'number' }, 55 | }, 56 | } 57 | 58 | function greetByGroupHandler(request, reply) { 59 | request.log.trace('requested myuser') 60 | const user = request.getUserId() 61 | const groups = request.getGroups() 62 | // call the decorated fastify instance 63 | reply.send(this.myBusinessLogic.greet(user, groups)) 64 | } 65 | 66 | function handleSum(request, reply) { 67 | const args = request.body 68 | request.log.trace({ args }, 'requested sum') 69 | let acc = 0 70 | for (let idx = 0; idx < args.length; idx++) { 71 | acc += args[idx] 72 | } 73 | reply.send({ acc }) 74 | } 75 | 76 | module.exports = customService(async function exampleService(service) { 77 | // retrieve environment variables from config 78 | const { GROUP_TO_GREET } = service.config 79 | // instantiate your business logic 80 | const myBusinessLogic = new BusinessLogic(GROUP_TO_GREET) 81 | // instantiate your business logic and decorate the fastify instance 82 | // to call it with this.myBusinessLogic in the handler 83 | service.decorate('myBusinessLogic', myBusinessLogic) 84 | 85 | service 86 | .addRawCustomPlugin('GET', '/greetbygroup', greetByGroupHandler, greetByGroupSchema) 87 | .addRawCustomPlugin('POST', '/sum', handleSum, sumSchema) 88 | }) 89 | -------------------------------------------------------------------------------- /examples/advanced/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mia-platform/advanced-custom-plugin-test", 3 | "version": "1.0.0", 4 | "description": "An advanced example for a custom plugin", 5 | "homepage": "https://www.mia-platform.eu/", 6 | "bugs": { 7 | "url": "https://github.com/mia-platform/custom-plugin-lib/issues", 8 | "email": "core@mia-platform.eu" 9 | }, 10 | "license": "Apache-2.0", 11 | "author": "Mia Platform Core Team ", 12 | "contributors": [ 13 | "Tommaso Allevi ", 14 | "Luca Scannapieco ", 15 | "Paolo Sarti ", 16 | "Jacopo Andrea Giola ", 17 | "Davide Bianchi " 18 | ], 19 | "main": "index.js", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/mia-platform/custom-plugin-lib.git" 23 | }, 24 | "scripts": { 25 | "start": "lc39 ./index.js", 26 | "start:local": "npm start -- --env-path ./../default.env", 27 | "test": "tap -b tests/*.test.js" 28 | }, 29 | "dependencies": { 30 | "@mia-platform/custom-plugin-lib": "mia-platform/custom-plugin-lib" 31 | }, 32 | "engines": { 33 | "node": ">=8" 34 | }, 35 | "devDependencies": { 36 | "tap": "^13.1.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/advanced/tests/greetByGroup.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const tap = require('tap') 20 | const lc39 = require('@mia-platform/lc39') 21 | 22 | const USERID_HEADER_KEY = 'userid-header-key' 23 | const GROUPS_HEADER_KEY = 'groups-header-key' 24 | const CLIENTTYPE_HEADER_KEY = 'clienttype-header-key' 25 | const BACKOFFICE_HEADER_KEY = 'backoffice-header-key' 26 | const GROUP_TO_GREET = 'group-to-greet' 27 | const MICROSERVICE_GATEWAY_SERVICE_NAME = 'microservice-gateway' 28 | const env = { 29 | USERID_HEADER_KEY, 30 | GROUPS_HEADER_KEY, 31 | CLIENTTYPE_HEADER_KEY, 32 | BACKOFFICE_HEADER_KEY, 33 | GROUP_TO_GREET, 34 | MICROSERVICE_GATEWAY_SERVICE_NAME, 35 | } 36 | 37 | async function setupFastify(envVariables) { 38 | const fastify = await lc39('./index.js', { 39 | logLevel: 'silent', 40 | envVariables, 41 | }) 42 | return fastify 43 | } 44 | 45 | tap.test('greetByGroup', test => { 46 | test.test('greets the special group', async assert => { 47 | const fastify = await setupFastify(env) 48 | const user = 'Mark' 49 | 50 | const response = await fastify.inject({ 51 | method: 'GET', 52 | url: '/greetbygroup', 53 | headers: { 54 | [USERID_HEADER_KEY]: user, 55 | [GROUPS_HEADER_KEY]: 'group-to-greet', 56 | }, 57 | }) 58 | 59 | assert.strictSame(response.statusCode, 200) 60 | assert.ok(/application\/json/.test(response.headers['content-type'])) 61 | assert.ok(/charset=utf-8/.test(response.headers['content-type'])) 62 | assert.strictSame(JSON.parse(response.payload), { 63 | message: `Hello ${user} of group: group-to-greet!\n`, 64 | user, 65 | groups: ['group-to-greet'], 66 | }) 67 | assert.end() 68 | }) 69 | 70 | test.end() 71 | }) 72 | -------------------------------------------------------------------------------- /examples/basic/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /examples/basic/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const customService = require('@mia-platform/custom-plugin-lib')() 20 | 21 | module.exports = customService(async function helloWorldService(service) { 22 | service.addRawCustomPlugin('GET', '/hello', function handler(request, reply) { 23 | request.log.trace('requested myuser') 24 | // if the user is not logged, this method returns a falsy value 25 | const user = request.getUserId() || 'World' 26 | reply.send({ 27 | hello: `${user}!`, 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mia-platform/basic-custom-plugin-test", 3 | "version": "1.0.0", 4 | "description": "A simple example for a custom plugin", 5 | "homepage": "https://www.mia-platform.eu/", 6 | "bugs": { 7 | "url": "https://github.com/mia-platform/custom-plugin-lib/issues", 8 | "email": "core@mia-platform.eu" 9 | }, 10 | "license": "Apache-2.0", 11 | "author": "Mia Platform Core Team ", 12 | "contributors": [ 13 | "Tommaso Allevi ", 14 | "Luca Scannapieco ", 15 | "Paolo Sarti ", 16 | "Jacopo Andrea Giola ", 17 | "Davide Bianchi " 18 | ], 19 | "main": "index.js", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/mia-platform/custom-plugin-lib.git" 23 | }, 24 | "scripts": { 25 | "start": "lc39 ./index.js", 26 | "start:local": "npm start -- --env-path ./../default.env" 27 | }, 28 | "dependencies": { 29 | "@mia-platform/custom-plugin-lib": "mia-platform/custom-plugin-lib" 30 | }, 31 | "engines": { 32 | "node": ">=8" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/default.env: -------------------------------------------------------------------------------- 1 | USERID_HEADER_KEY=userid-header-key 2 | USER_PROPERTIES_HEADER_KEY=miauserproperties 3 | GROUPS_HEADER_KEY=groups-header-key 4 | CLIENTTYPE_HEADER_KEY=clienttype-header-key 5 | BACKOFFICE_HEADER_KEY=backoffice-header-key 6 | MICROSERVICE_GATEWAY_SERVICE_NAME=microservice-gateway-service-name 7 | GROUP_TO_GREET=group-name 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as fastify from 'fastify' 18 | import * as http from 'http' 19 | import * as https from 'https' 20 | 21 | import {FormatName} from 'ajv-formats' 22 | 23 | export = customPlugin 24 | 25 | declare function customPlugin(envSchema?: customPlugin.environmentSchema): customPlugin.CustomService 26 | 27 | declare namespace customPlugin { 28 | type ServiceConfig> = T 29 | 30 | type CustomService = (asyncInitFunction: AsyncInitFunction, serviceOptions?: CustomServiceOptions) => any 31 | 32 | function getHttpClient(url: string, options?: HttpClientBaseOptions): HttpClient 33 | 34 | interface environmentSchema { 35 | type: 'object', 36 | required?: readonly string[], 37 | properties: object 38 | } 39 | 40 | type AsyncInitFunction = (service: DecoratedFastify) => Promise 41 | 42 | interface CustomServiceOptions { 43 | ajv?: { 44 | plugins?: { 45 | 'ajv-formats'?: {formats: FormatName[]} 46 | } 47 | } 48 | vocabulary?: string[] 49 | } 50 | 51 | type RawCustomPluginAdvancedConfig = Pick 60 | 61 | type RawCustomPluginMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' 62 | 63 | type RequestGeneric = T 64 | 65 | interface DecoratedFastify extends fastify.FastifyInstance { 66 | config: Config, 67 | addRawCustomPlugin(method: RawCustomPluginMethod, path: string, handler: AsyncHandler | Handler, schema?: InputOutputSchemas, advancedConfigs?: RawCustomPluginAdvancedConfig): DecoratedFastify, 68 | addPreDecorator(path: string, handler: preDecoratorHandler): DecoratedFastify 69 | addPostDecorator(path: string, handler: postDecoratorHandler): DecoratedFastify 70 | getHttpClient(url: string, options?: HttpClientBaseOptions): HttpClient, 71 | addValidatorSchema(schema: object): void, 72 | getValidatorSchema(schemaId: string): undefined | ((data: any) => boolean | Promise), 73 | } 74 | 75 | interface DecoratedRequest extends fastify.FastifyRequest { 76 | getUserId: () => string | null, 77 | getUserProperties: () => object | null, 78 | getGroups: () => string[], 79 | getClientType: () => string | null, 80 | isFromBackOffice: () => boolean, 81 | getMiaHeaders: () => NodeJS.Dict 82 | getHeadersToProxy({isMiaHeaderInjected}: {isMiaHeaderInjected?: boolean}): NodeJS.Dict 83 | getHttpClient(url: string, options?: HttpClientBaseOptions): HttpClient, 84 | USERID_HEADER_KEY: string, 85 | USER_PROPERTIES_HEADER_KEY: string, 86 | GROUPS_HEADER_KEY: string, 87 | CLIENTTYPE_HEADER_KEY: string, 88 | BACKOFFICE_HEADER_KEY: string, 89 | MICROSERVICE_GATEWAY_SERVICE_NAME: string, 90 | ADDITIONAL_HEADERS_TO_PROXY: string[] 91 | } 92 | 93 | // 94 | // CUSTOM PLUGIN 95 | // 96 | type BasicHandler> = (this: DecoratedFastify, request: DecoratedRequest, reply: fastify.FastifyReply) => ResponseType 97 | type Handler = BasicHandler 98 | type AsyncHandler = BasicHandler> 99 | 100 | // 101 | // HTTP CLIENT 102 | // 103 | type HttpClientMetrics = { 104 | disabled: boolean 105 | urlLabel: string 106 | } 107 | interface HttpClientBaseOptions { 108 | headers?: http.IncomingHttpHeaders, 109 | timeout?: number, 110 | cert?: string, 111 | key?: string, 112 | ca?: string, 113 | httpsAgent?: https.Agent, 114 | logger?: fastify.FastifyLoggerInstance, 115 | isMiaHeaderInjected?: boolean, 116 | disableMetrics?: boolean 117 | } 118 | interface BaseHttpClientResponse { 119 | headers: http.IncomingHttpHeaders 120 | statusCode: number 121 | duration: number 122 | } 123 | interface StreamResponse extends BaseHttpClientResponse { 124 | payload: NodeJS.ReadableStream 125 | } 126 | interface JSONResponse extends BaseHttpClientResponse { 127 | payload: Payload 128 | } 129 | interface BufferResponse extends BaseHttpClientResponse { 130 | payload: Buffer 131 | } 132 | type Response = StreamResponse | JSONResponse | BufferResponse 133 | interface HttpClientProxy { 134 | protocol: 'http' | 'https' 135 | host: string 136 | port: number 137 | auth: { 138 | username: string 139 | password: string 140 | } 141 | } 142 | interface HttpClientOptions extends HttpClientBaseOptions { 143 | returnAs?: 'STREAM' | 'JSON' | 'BUFFER'; 144 | validateStatus?: (statusCode: number) => boolean; 145 | errorMessageKey?: string; 146 | proxy?: HttpClientProxy; 147 | query?: Record; 148 | metrics?: HttpClientMetrics; 149 | } 150 | 151 | type RequestBody = any | Buffer | ReadableStream 152 | type RequestMethodWithBody = (path: string, body: RequestBody, options?: HttpClientOptions) => Promise 153 | type RequestMethodWithoutBody = (path: string, options?: HttpClientOptions) => Promise 154 | 155 | interface HttpClient { 156 | get: RequestMethodWithoutBody 157 | post: RequestMethodWithBody 158 | put: RequestMethodWithBody 159 | patch: RequestMethodWithBody 160 | delete: RequestMethodWithBody 161 | } 162 | 163 | // 164 | // SERVICE 165 | // 166 | interface InitServiceOptions { 167 | port?: number, 168 | protocol?: 'http' | 'https', 169 | headers?: http.IncomingHttpHeaders, 170 | prefix?: string, 171 | } 172 | type Certificate = string | Buffer 173 | type ResponseFormats = 'JSON' | 'BUFFER' | 'STREAM' 174 | interface ServiceOptions extends InitServiceOptions{ 175 | returnAs?: ResponseFormats 176 | allowedStatusCodes?: number[] 177 | isMiaHeaderInjected?: boolean 178 | cert?: Certificate 179 | key?: Certificate 180 | ca?: Certificate 181 | } 182 | interface BaseServiceResponse extends http.ServerResponse { 183 | headers: http.IncomingHttpHeaders 184 | } 185 | interface StreamedServiceResponse extends BaseServiceResponse { 186 | } 187 | interface JSONServiceResponse extends BaseServiceResponse { 188 | payload: Payload 189 | } 190 | interface BufferServiceResponse extends BaseServiceResponse { 191 | payload: Buffer 192 | } 193 | type ServiceResponse = StreamedServiceResponse | JSONServiceResponse | BufferServiceResponse 194 | type QueryString = string | NodeJS.Dict> | Iterable<[string, string]> | ReadonlyArray<[string, string]> 195 | 196 | interface Service { 197 | get: (path: string, queryString?: QueryString, options?: ServiceOptions) => Promise, 198 | post: (path: string, body: any | Buffer | ReadableStream, queryString?: QueryString, options?: ServiceOptions) => Promise, 199 | put: (path: string, body: any | Buffer | ReadableStream, queryString?: QueryString, options?: ServiceOptions) => Promise, 200 | patch: (path: string, body: any | Buffer | ReadableStream, queryString?: QueryString, options?: ServiceOptions) => Promise, 201 | delete: (path: string, body: any | Buffer | ReadableStream, queryString?: QueryString, options?: ServiceOptions) => Promise, 202 | } 203 | 204 | // 205 | // PRE DECORATOR 206 | // 207 | interface LeaveRequestUnchangedAction { } 208 | interface ChangeRequestAction { 209 | setBody: (newBody: any) => ChangeRequestAction, 210 | setQuery: (newQuery: QueryString) => ChangeRequestAction, 211 | setHeaders: (newHeaders: http.IncomingHttpHeaders) => ChangeRequestAction 212 | } 213 | interface AbortRequestAction { } 214 | type PreDecoratorAction = LeaveRequestUnchangedAction | ChangeRequestAction | AbortRequestAction; 215 | type preDecoratorHandler = (this: DecoratedFastify, request: PreDecoratorDecoratedRequest, reply: fastify.FastifyReply) => Promise; 216 | 217 | interface OriginalRequest { 218 | method: string, 219 | path: string, 220 | query: QueryString, 221 | headers: http.IncomingHttpHeaders, 222 | body?: any 223 | } 224 | 225 | interface OriginalResponse { 226 | statusCode: number, 227 | headers: http.OutgoingHttpHeaders, 228 | body?: any 229 | } 230 | 231 | interface PreDecoratorDecoratedRequest extends DecoratedRequest { 232 | getOriginalRequest: () => OriginalRequest, 233 | getOriginalRequestMethod: () => string, 234 | getOriginalRequestPath: () => string, 235 | getOriginalRequestHeaders: () => http.IncomingHttpHeaders, 236 | getOriginalRequestQuery: () => QueryString 237 | getOriginalRequestBody: () => any, 238 | 239 | getMiaHeaders: () => NodeJS.Dict, 240 | 241 | changeOriginalRequest: () => ChangeRequestAction, 242 | leaveOriginalRequestUnmodified: () => LeaveRequestUnchangedAction, 243 | abortChain: (statusCode: number, finalBody: any, headers?: http.IncomingHttpHeaders) => AbortRequestAction 244 | } 245 | 246 | // 247 | // POST DECORATOR 248 | // 249 | interface LeaveResponseUnchangedAction { } 250 | interface ChangeResponseAction { 251 | setBody: (newBody: any) => ChangeResponseAction, 252 | setStatusCode: (newStatusCode: number) => ChangeResponseAction, 253 | setHeaders: (newHeaders: http.IncomingHttpHeaders) => ChangeResponseAction 254 | } 255 | interface AbortResponseAction { } 256 | type PostDecoratorAction = LeaveResponseUnchangedAction | ChangeResponseAction | AbortResponseAction; 257 | type postDecoratorHandler = (this: DecoratedFastify, request: PostDecoratorDecoratedRequest, reply: fastify.FastifyReply) => Promise; 258 | 259 | interface PostDecoratorDecoratedRequest extends DecoratedRequest { 260 | getOriginalRequest: () => OriginalRequest, 261 | getOriginalRequestMethod: () => string, 262 | getOriginalRequestPath: () => string, 263 | getOriginalRequestHeaders: () => http.IncomingHttpHeaders, 264 | getOriginalRequestQuery: () => QueryString 265 | getOriginalRequestBody: () => any, 266 | 267 | getOriginalResponse: () => OriginalResponse, 268 | getOriginalResponseHeaders: () => http.OutgoingHttpHeaders, 269 | getOriginalResponseBody: () => any, 270 | getOriginalResponseStatusCode: () => number, 271 | 272 | getMiaHeaders: () => NodeJS.Dict, 273 | 274 | changeOriginalResponse: () => ChangeResponseAction, 275 | leaveOriginalResponseUnmodified: () => LeaveResponseUnchangedAction, 276 | abortChain: (statusCode: number, finalBody: any, headers?: http.IncomingHttpHeaders) => AbortResponseAction 277 | } 278 | 279 | // Utilities 280 | interface InputOutputSchemas extends fastify.FastifySchema { 281 | tags?: string[] 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const fastifyEnv = require('@fastify/env') 20 | const fp = require('fastify-plugin') 21 | const fastifyFormbody = require('@fastify/formbody') 22 | const Ajv = require('ajv') 23 | const path = require('path') 24 | const { name, description, version } = require(path.join(process.cwd(), 'package.json')) 25 | 26 | const addRawCustomPlugin = require('./lib/rawCustomPlugin') 27 | const addPreDecorator = require('./lib/preDecorator') 28 | const addPostDecorator = require('./lib/postDecorator') 29 | const ajvSetup = require('./lib/ajvSetup') 30 | const HttpClient = require('./lib/httpClient') 31 | const { extraHeadersKeys } = require('./lib/util') 32 | 33 | const USERID_HEADER_KEY = 'USERID_HEADER_KEY' 34 | const USER_PROPERTIES_HEADER_KEY = 'USER_PROPERTIES_HEADER_KEY' 35 | const GROUPS_HEADER_KEY = 'GROUPS_HEADER_KEY' 36 | const CLIENTTYPE_HEADER_KEY = 'CLIENTTYPE_HEADER_KEY' 37 | const BACKOFFICE_HEADER_KEY = 'BACKOFFICE_HEADER_KEY' 38 | const MICROSERVICE_GATEWAY_SERVICE_NAME = 'MICROSERVICE_GATEWAY_SERVICE_NAME' 39 | const ADDITIONAL_HEADERS_TO_PROXY = 'ADDITIONAL_HEADERS_TO_PROXY' 40 | const ENABLE_HTTP_CLIENT_METRICS = 'ENABLE_HTTP_CLIENT_METRICS' 41 | 42 | const baseSchema = { 43 | type: 'object', 44 | required: [ 45 | USERID_HEADER_KEY, 46 | GROUPS_HEADER_KEY, 47 | CLIENTTYPE_HEADER_KEY, 48 | BACKOFFICE_HEADER_KEY, 49 | MICROSERVICE_GATEWAY_SERVICE_NAME, 50 | ], 51 | properties: { 52 | [USERID_HEADER_KEY]: { 53 | type: 'string', 54 | description: 'the header key to get the user id', 55 | minLength: 1, 56 | }, 57 | [USER_PROPERTIES_HEADER_KEY]: { 58 | type: 'string', 59 | description: 'the header key to get the user permissions', 60 | minLength: 1, 61 | default: 'miauserproperties', 62 | }, 63 | [GROUPS_HEADER_KEY]: { 64 | type: 'string', 65 | description: 'the header key to get the groups comma separated list', 66 | minLength: 1, 67 | }, 68 | [CLIENTTYPE_HEADER_KEY]: { 69 | type: 'string', 70 | description: 'the header key to get the client type', 71 | minLength: 1, 72 | }, 73 | [BACKOFFICE_HEADER_KEY]: { 74 | type: 'string', 75 | description: 'the header key to get if the request is from backoffice (any truly string is true!!!)', 76 | minLength: 1, 77 | }, 78 | [MICROSERVICE_GATEWAY_SERVICE_NAME]: { 79 | type: 'string', 80 | description: 'the service name of the microservice gateway', 81 | pattern: '^(?=.{1,253}.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*.?$', 82 | }, 83 | [ADDITIONAL_HEADERS_TO_PROXY]: { 84 | type: 'string', 85 | default: '', 86 | description: 'comma separated list of additional headers to proxy', 87 | }, 88 | [ENABLE_HTTP_CLIENT_METRICS]: { 89 | type: 'boolean', 90 | default: false, 91 | description: 'flag to enable the httpClient metrics', 92 | }, 93 | }, 94 | } 95 | 96 | function mergeObjectOrArrayProperty(toBeMergedValues, alreadyMergedValues, isArray) { 97 | return isArray ? [ 98 | ...toBeMergedValues ?? [], 99 | ...alreadyMergedValues, 100 | ] : { 101 | ...toBeMergedValues ?? {}, 102 | ...alreadyMergedValues, 103 | } 104 | } 105 | 106 | // WARNING: including any first level properties other than the ones already declared 107 | // may have undesired effects on the result of the merge 108 | function mergeWithDefaultJsonSchema(schema) { 109 | const defaultSchema = { 110 | ...baseSchema, 111 | } 112 | 113 | Object.keys(schema).forEach(key => { 114 | defaultSchema[key] = typeof schema[key] === 'object' 115 | ? mergeObjectOrArrayProperty(defaultSchema[key], schema[key], Array.isArray(schema[key])) 116 | : schema[key] 117 | }) 118 | 119 | return defaultSchema 120 | } 121 | 122 | function getOverlappingKeys(properties, otherProperties) { 123 | if (!otherProperties) { 124 | return [] 125 | } 126 | const propertiesNames = Object.keys(properties) 127 | const otherPropertiesNames = Object.keys(otherProperties) 128 | const overlappingProperties = propertiesNames.filter(propertyName => 129 | otherPropertiesNames.includes(propertyName) 130 | ) 131 | return overlappingProperties 132 | } 133 | 134 | function getCustomHeaders(headersKeyToProxy, headers) { 135 | return headersKeyToProxy.reduce((acc, headerKey) => { 136 | if (!{}.hasOwnProperty.call(headers, headerKey)) { 137 | return acc 138 | } 139 | const headerValue = headers[headerKey] 140 | return { 141 | ...acc, 142 | [headerKey]: headerValue, 143 | } 144 | }, {}) 145 | } 146 | 147 | function getBaseOptionsDecorated(headersKeyToProxy, baseOptions, headers) { 148 | return { 149 | ...baseOptions, 150 | headers: { 151 | ...getCustomHeaders(headersKeyToProxy, headers), 152 | ...baseOptions.headers, 153 | }, 154 | } 155 | } 156 | 157 | function getMiaHeaders() { 158 | const userId = this.getUserId() 159 | const userProperties = this.getUserProperties() 160 | const groups = this.getGroups().join(',') 161 | const clientType = this.getClientType() 162 | const fromBackoffice = this.isFromBackOffice() ? '1' : '' 163 | return { 164 | ...userId !== null ? { [this.USERID_HEADER_KEY]: userId } : {}, 165 | ...userProperties !== null ? { [this.USER_PROPERTIES_HEADER_KEY]: JSON.stringify(userProperties) } : {}, 166 | ...groups ? { [this.GROUPS_HEADER_KEY]: groups } : {}, 167 | ...clientType !== null ? { [this.CLIENTTYPE_HEADER_KEY]: clientType } : {}, 168 | ...fromBackoffice ? { [this.BACKOFFICE_HEADER_KEY]: fromBackoffice } : {}, 169 | } 170 | } 171 | 172 | function getOriginalRequestHeaders() { 173 | return this.headers 174 | } 175 | 176 | function getHttpClientFromRequest(url, baseOptions = {}) { 177 | const requestHeaders = this.getOriginalRequestHeaders() 178 | const extraHeaders = getCustomHeaders(extraHeadersKeys, requestHeaders) 179 | const options = getBaseOptionsDecorated(this[ADDITIONAL_HEADERS_TO_PROXY], baseOptions, requestHeaders) 180 | const serviceHeaders = { ...this.getMiaHeaders(), ...extraHeaders } 181 | return new HttpClient(url, serviceHeaders, options, this.httpClientMetrics) 182 | } 183 | 184 | function getHttpClient(url, baseOptions = {}, httpClientMetrics = {}) { 185 | return new HttpClient(url, {}, baseOptions, httpClientMetrics) 186 | } 187 | 188 | function getHttpClientFastifyDecoration(url, baseOptions = {}) { 189 | return getHttpClient(url, baseOptions, this.httpClientMetrics) 190 | } 191 | 192 | function getHeadersToProxy({ isMiaHeaderInjected = true } = {}) { 193 | const requestHeaders = this.getOriginalRequestHeaders() 194 | 195 | const miaHeaders = this.getMiaHeaders() 196 | const extraMiaHeaders = getCustomHeaders(extraHeadersKeys, requestHeaders) 197 | const customHeaders = getCustomHeaders(this[ADDITIONAL_HEADERS_TO_PROXY], requestHeaders) 198 | return { 199 | ...isMiaHeaderInjected ? miaHeaders : {}, 200 | ...isMiaHeaderInjected ? extraMiaHeaders : {}, 201 | ...customHeaders, 202 | } 203 | } 204 | 205 | function decorateFastify(fastify) { 206 | const { config } = fastify 207 | const httpClientMetrics = config[ENABLE_HTTP_CLIENT_METRICS] ? getHttpClientMetrics(fastify) : {} 208 | 209 | fastify.decorateRequest(USERID_HEADER_KEY, config[USERID_HEADER_KEY]) 210 | fastify.decorateRequest(USER_PROPERTIES_HEADER_KEY, config[USER_PROPERTIES_HEADER_KEY]) 211 | fastify.decorateRequest(GROUPS_HEADER_KEY, config[GROUPS_HEADER_KEY]) 212 | fastify.decorateRequest(CLIENTTYPE_HEADER_KEY, config[CLIENTTYPE_HEADER_KEY]) 213 | fastify.decorateRequest(BACKOFFICE_HEADER_KEY, config[BACKOFFICE_HEADER_KEY]) 214 | fastify.decorateRequest(MICROSERVICE_GATEWAY_SERVICE_NAME, config[MICROSERVICE_GATEWAY_SERVICE_NAME]) 215 | fastify.decorateRequest(ADDITIONAL_HEADERS_TO_PROXY, { 216 | getter() { 217 | return config[ADDITIONAL_HEADERS_TO_PROXY].split(',').filter(header => header) 218 | }, 219 | }) 220 | 221 | fastify.decorateRequest('getMiaHeaders', getMiaHeaders) 222 | fastify.decorateRequest('getOriginalRequestHeaders', getOriginalRequestHeaders) 223 | fastify.decorateRequest('getHeadersToProxy', getHeadersToProxy) 224 | 225 | fastify.decorateRequest('getHttpClient', getHttpClientFromRequest) 226 | fastify.decorateRequest('httpClientMetrics', { getter: () => httpClientMetrics }) 227 | 228 | fastify.decorate(MICROSERVICE_GATEWAY_SERVICE_NAME, config[MICROSERVICE_GATEWAY_SERVICE_NAME]) 229 | fastify.decorate('addRawCustomPlugin', addRawCustomPlugin) 230 | fastify.decorate('addPreDecorator', addPreDecorator) 231 | fastify.decorate('addPostDecorator', addPostDecorator) 232 | 233 | fastify.decorate('getHttpClient', getHttpClientFastifyDecoration) 234 | fastify.decorate('httpClientMetrics', httpClientMetrics) 235 | } 236 | 237 | async function decorateRequestAndFastifyInstance(fastify, { asyncInitFunction, serviceOptions = {} }) { 238 | const { ajv: ajvServiceOptions } = serviceOptions 239 | 240 | const ajv = new Ajv({ coerceTypes: true, useDefaults: true }) 241 | ajvSetup(ajv, ajvServiceOptions) 242 | 243 | fastify.setValidatorCompiler(({ schema }) => ajv.compile(schema)) 244 | 245 | fastify.decorate('addValidatorSchema', (schema) => { 246 | ajv.addSchema(schema) 247 | fastify.addSchema(schema) 248 | }) 249 | fastify.decorate('getValidatorSchema', ajv.getSchema.bind(ajv)) 250 | 251 | decorateFastify(fastify) 252 | 253 | fastify.register(fp(asyncInitFunction)) 254 | fastify.setErrorHandler(function errorHandler(error, request, reply) { 255 | if (reply.raw.statusCode === 500 && !error.statusCode) { 256 | request.log.error(error) 257 | reply.send(new Error('Something went wrong')) 258 | return 259 | } 260 | reply.send(error) 261 | }) 262 | fastify.setSchemaErrorFormatter((errors, dataVar) => { 263 | const [{ instancePath, message }] = errors 264 | const objectPath = `${dataVar}${instancePath.replace(/\//g, '.')}` 265 | const customErr = new Error(`${objectPath} ${message}`) 266 | customErr.statusCode = 400 267 | return customErr 268 | }) 269 | } 270 | 271 | 272 | function initCustomServiceEnvironment(envSchema) { 273 | return function customService(asyncInitFunction, serviceOptions) { 274 | async function index(fastify, opts) { 275 | const overlappingPropertiesNames = getOverlappingKeys(baseSchema.properties, envSchema?.properties) 276 | if (overlappingPropertiesNames.length > 0) { 277 | throw new Error(`The provided Environment JSON Schema includes properties declared in the Base JSON Schema of the custom-plugin-lib, please remove them from your schema. The properties to remove are: ${overlappingPropertiesNames.join(', ')}`) 278 | } 279 | 280 | const schema = envSchema ? mergeWithDefaultJsonSchema(envSchema) : baseSchema 281 | fastify.register(fastifyEnv, { schema, data: opts }) 282 | fastify.register(fastifyFormbody) 283 | fastify.register(fp(decorateRequestAndFastifyInstance), { asyncInitFunction, serviceOptions }) 284 | } 285 | index.options = { 286 | errorHandler: false, 287 | trustProxy: process.env.TRUSTED_PROXIES, 288 | } 289 | 290 | index.swaggerDefinition = { 291 | info: { 292 | title: name, 293 | description, 294 | version, 295 | }, 296 | consumes: ['application/json', 'application/x-www-form-urlencoded'], 297 | produces: ['application/json'], 298 | } 299 | return index 300 | } 301 | } 302 | 303 | function getHttpClientMetrics(fastify) { 304 | if (fastify.metrics?.client) { 305 | const requestDuration = new fastify.metrics.client.Histogram({ 306 | name: 'http_request_duration_milliseconds', 307 | help: 'request duration histogram', 308 | labelNames: ['baseUrl', 'url', 'method', 'statusCode'], 309 | buckets: [5, 10, 50, 100, 500, 1000, 5000, 10000], 310 | }) 311 | return { requestDuration } 312 | } 313 | } 314 | 315 | module.exports = initCustomServiceEnvironment 316 | module.exports.getHttpClient = getHttpClient 317 | -------------------------------------------------------------------------------- /lib/ajvSetup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const addFormats = require('ajv-formats') 20 | 21 | function configureAjvPlugins(ajv, ajvPluginsOptions) { 22 | if (ajvPluginsOptions['ajv-formats'] && ajvPluginsOptions['ajv-formats'].formats) { 23 | addFormats(ajv, ajvPluginsOptions['ajv-formats'].formats) 24 | } 25 | } 26 | function configureAjvVocabulary(ajv, ajvVocabulary) { 27 | ajv.addVocabulary(ajvVocabulary) 28 | } 29 | 30 | module.exports = (ajv, ajvServiceOptions = {}) => { 31 | if (ajvServiceOptions.vocabulary) { 32 | configureAjvVocabulary(ajv, ajvServiceOptions.vocabulary) 33 | } 34 | if (ajvServiceOptions.plugins) { 35 | configureAjvPlugins(ajv, ajvServiceOptions.plugins) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/decoratorsCommonFunctions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ABORT_CHAIN_STATUS_CODE = 418 4 | 5 | const { getUserId, getGroups, getClientType, isFromBackOffice, getUserProperties } = require('./util') 6 | 7 | function getUserIdFromBody() { 8 | return getUserId(this.getOriginalRequestHeaders()[this.USERID_HEADER_KEY]) 9 | } 10 | 11 | function getUserPropertiesFromBody() { 12 | return getUserProperties(this.getOriginalRequestHeaders()[this.USER_PROPERTIES_HEADER_KEY]) 13 | } 14 | 15 | function getGroupsFromBody() { 16 | return getGroups(this.getOriginalRequestHeaders()[this.GROUPS_HEADER_KEY] || '') 17 | } 18 | 19 | function getClientTypeFromBody() { 20 | return getClientType(this.getOriginalRequestHeaders()[this.CLIENTTYPE_HEADER_KEY]) 21 | } 22 | 23 | function isFromBackOfficeFromBody() { 24 | return isFromBackOffice(this.getOriginalRequestHeaders()[this.BACKOFFICE_HEADER_KEY]) 25 | } 26 | 27 | module.exports = { 28 | ABORT_CHAIN_STATUS_CODE, 29 | getUserIdFromBody, 30 | getUserPropertiesFromBody, 31 | getGroupsFromBody, 32 | getClientTypeFromBody, 33 | isFromBackOfficeFromBody, 34 | } 35 | -------------------------------------------------------------------------------- /lib/httpClient.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const axios = require('axios') 20 | const httpsClient = require('https') 21 | const Pino = require('pino') 22 | 23 | class HttpClient { 24 | constructor(baseUrl, requestHeadersToProxy, baseOptions = {}, metrics = {}) { 25 | this.baseURL = baseUrl 26 | this.requestHeadersToProxy = requestHeadersToProxy 27 | this.baseOptions = baseOptions 28 | this.metrics = baseOptions.disableMetrics ? {} : metrics 29 | 30 | const httpsAgent = getHttpsAgent(baseOptions) 31 | this.axios = axios.create({ 32 | ...httpsAgent ? { httpsAgent } : {}, 33 | baseURL: baseUrl, 34 | timeout: baseOptions.timeout, 35 | }) 36 | decorateResponseWithDuration(this.axios) 37 | } 38 | 39 | async get(path, options = {}) { 40 | return this.makeCall(path, undefined, { 41 | ...options, 42 | method: 'GET', 43 | }) 44 | } 45 | 46 | async post(path, payload, options = {}) { 47 | return this.makeCall(path, payload, { 48 | ...options, 49 | method: 'POST', 50 | }) 51 | } 52 | 53 | async put(path, payload, options = {}) { 54 | return this.makeCall(path, payload, { 55 | ...options, 56 | method: 'PUT', 57 | }) 58 | } 59 | 60 | async patch(path, payload, options = {}) { 61 | return this.makeCall(path, payload, { 62 | ...options, 63 | method: 'PATCH', 64 | }) 65 | } 66 | 67 | async delete(path, payload, options = {}) { 68 | return this.makeCall(path, payload, { 69 | ...options, 70 | method: 'DELETE', 71 | }) 72 | } 73 | 74 | getLogger(options) { 75 | return options.logger || this.baseOptions.logger || Pino({ level: 'silent' }) 76 | } 77 | 78 | getMaxValues(options) { 79 | return { 80 | maxContentLength: options.maxContentLength || this.baseOptions.maxContentLength, 81 | maxBodyLength: options.maxBodyLength || this.baseOptions.maxBodyLength, 82 | maxRedirects: options.maxRedirects || this.baseOptions.maxRedirects, 83 | } 84 | } 85 | 86 | // eslint-disable-next-line max-statements 87 | async makeCall(url, payload, options) { 88 | const logger = this.getLogger(options) 89 | const headers = getHeaders(options, this.requestHeadersToProxy, this.baseOptions, payload) 90 | const httpsAgent = getHttpsAgent(options) 91 | const errorMessageKey = getErrorMessageKey(options, this.baseOptions) 92 | const metricsOptions = getMetricsOptions(options.metrics, url, this.metrics) 93 | try { 94 | const validateStatus = getValidateStatus(options) 95 | logger.trace({ baseURL: this.baseURL, url, headers, payload }, 'make call') 96 | const response = await this.axios({ 97 | url, 98 | method: options.method, 99 | headers, 100 | data: getData(payload), 101 | responseType: getResponseType(options), 102 | params: options.query, 103 | ...validateStatus ? { validateStatus } : {}, 104 | timeout: options.timeout, 105 | proxy: options.proxy, 106 | ...httpsAgent ? { httpsAgent } : {}, 107 | ...this.getMaxValues(options), 108 | }) 109 | const responseBody = { 110 | statusCode: response.status, 111 | headers: { ...response.headers.toJSON() }, 112 | payload: response.data, 113 | duration: response.duration, 114 | } 115 | logger.trace({ url, ...responseBody }, 'response info') 116 | if (metricsOptions.enabled) { 117 | this.metrics.requestDuration.observe({ 118 | method: options.method, 119 | url: metricsOptions.urlLabel, 120 | baseUrl: this.baseURL, 121 | statusCode: response.status, 122 | }, 123 | response.duration, 124 | ) 125 | } 126 | return responseBody 127 | } catch (error) { 128 | if (error.response) { 129 | const errorMessage = getErrorMessage(error.response, options.returnAs, errorMessageKey) 130 | const responseError = new Error(errorMessage) 131 | responseError.headers = { ...error.response.headers.toJSON() } 132 | responseError.statusCode = error.response.status 133 | responseError.payload = error.response.data 134 | responseError.duration = error.duration 135 | logger.error({ statusCode: error.response.status, message: errorMessage }, 'response error') 136 | if (metricsOptions.enabled) { 137 | this.metrics.requestDuration.observe({ 138 | method: options.method, 139 | url: metricsOptions.urlLabel, 140 | baseUrl: this.baseURL, 141 | statusCode: error.response.status, 142 | }, 143 | error.duration, 144 | ) 145 | } 146 | throw responseError 147 | } 148 | const errToReturn = new Error(error.message) 149 | errToReturn.code = error.code 150 | // eslint-disable-next-line id-blacklist 151 | logger.error({ err: errToReturn }, 'generic request error') 152 | throw errToReturn 153 | } 154 | } 155 | } 156 | 157 | function getErrorMessageKey(options, baseOptions) { 158 | return options.errorMessageKey || baseOptions.errorMessageKey || 'message' 159 | } 160 | 161 | function getHttpsAgent({ cert, ca, key, httpsAgent }) { 162 | if (cert || ca || key) { 163 | return new httpsClient.Agent({ cert, key, ca }) 164 | } 165 | if (httpsAgent) { 166 | return httpsAgent 167 | } 168 | } 169 | 170 | function getValidateStatus({ validateStatus }) { 171 | if (validateStatus && typeof validateStatus !== 'function') { 172 | throw new Error('validateStatus must be a function') 173 | } 174 | return validateStatus 175 | } 176 | 177 | function getData(payload) { 178 | if (payload === null) { 179 | return 'null' 180 | } 181 | if (payload === undefined) { 182 | return '' 183 | } 184 | return payload 185 | } 186 | 187 | const DEFAULT_ERROR_MESSAGE = 'Something went wrong' 188 | function getErrorMessage(response, returnAs, errorMessageKey) { 189 | const { headers, data } = response 190 | const contentType = headers['content-type'] 191 | if (!contentType || !contentType.includes('application/json')) { 192 | return DEFAULT_ERROR_MESSAGE 193 | } 194 | 195 | switch (returnAs) { 196 | case 'BUFFER': 197 | try { 198 | return JSON.parse(data)[errorMessageKey] 199 | } catch (error) { 200 | return DEFAULT_ERROR_MESSAGE 201 | } 202 | case 'JSON': 203 | default: 204 | return data[errorMessageKey] || DEFAULT_ERROR_MESSAGE 205 | } 206 | } 207 | 208 | function getResponseType({ returnAs = 'JSON' }) { 209 | switch (returnAs) { 210 | case 'JSON': 211 | return 'json' 212 | case 'BUFFER': 213 | return 'arraybuffer' 214 | case 'STREAM': 215 | return 'stream' 216 | default: 217 | throw new Error(`Unknown returnAs: ${returnAs}`) 218 | } 219 | } 220 | 221 | function isMiaHeaderToInject(baseOptions, options) { 222 | if (options.isMiaHeaderInjected !== undefined) { 223 | return options.isMiaHeaderInjected 224 | } 225 | if (baseOptions.isMiaHeaderInjected === undefined && options.isMiaHeaderInjected === undefined) { 226 | return true 227 | } 228 | return baseOptions.isMiaHeaderInjected 229 | } 230 | 231 | function getHeaders(options, miaHeaders, baseOptions, payload) { 232 | const isMiaHeaderInjected = isMiaHeaderToInject(baseOptions, options) 233 | return { 234 | ...typeof payload === 'object' ? { 'content-type': 'application/json;charset=utf-8' } : {}, 235 | ...payload === undefined ? { 'content-length': '0', 'content-type': 'text/html' } : {}, 236 | ...isMiaHeaderInjected ? miaHeaders : {}, 237 | ...baseOptions.headers || {}, 238 | ...options.headers || {}, 239 | } 240 | } 241 | 242 | function getMetricsOptions(metricsOptions = {}, url, metrics) { 243 | return { 244 | enabled: Boolean(metrics.requestDuration && metricsOptions.disabled !== true), 245 | urlLabel: metricsOptions.urlLabel ?? url, 246 | } 247 | } 248 | 249 | function decorateResponseWithDuration(axiosInstance) { 250 | axiosInstance.interceptors.response.use( 251 | (response) => { 252 | response.config.metadata.endTime = new Date() 253 | response.duration = response.config.metadata.endTime - response.config.metadata.startTime 254 | return response 255 | }, 256 | (error) => { 257 | error.config.metadata.endTime = new Date() 258 | error.duration = error.config.metadata.endTime - error.config.metadata.startTime 259 | return Promise.reject(error) 260 | } 261 | ) 262 | 263 | axiosInstance.interceptors.request.use( 264 | (config) => { 265 | config.metadata = { startTime: new Date() } 266 | return config 267 | }, (error) => { 268 | return Promise.reject(error) 269 | }) 270 | } 271 | 272 | module.exports = HttpClient 273 | 274 | -------------------------------------------------------------------------------- /lib/postDecorator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const LEAVE_UNCHANGED_RESPONSE_STATUS_CODE = 204 20 | 21 | const { 22 | ABORT_CHAIN_STATUS_CODE, 23 | getUserIdFromBody, 24 | getUserPropertiesFromBody, 25 | getGroupsFromBody, 26 | getClientTypeFromBody, 27 | isFromBackOfficeFromBody, 28 | } = require('./decoratorsCommonFunctions') 29 | 30 | const POST_DECORATOR_SCHEMA = { 31 | body: { 32 | type: 'object', 33 | required: ['request', 'response'], 34 | properties: { 35 | request: { 36 | type: 'object', 37 | required: ['method', 'path', 'headers', 'query'], 38 | properties: { 39 | method: { type: 'string' }, 40 | path: { type: 'string' }, 41 | headers: { type: 'object' }, 42 | query: { type: 'object' }, 43 | body: { }, 44 | }, 45 | }, 46 | response: { 47 | type: 'object', 48 | required: ['headers', 'statusCode'], 49 | properties: { 50 | headers: { type: 'object' }, 51 | statusCode: { type: 'number' }, 52 | body: { }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | } 58 | function getRequest() { 59 | return this.body.request 60 | } 61 | function getMethod() { 62 | return this.body.request.method 63 | } 64 | function getPath() { 65 | return this.body.request.path 66 | } 67 | function getHeaders() { 68 | return this.body.request.headers 69 | } 70 | function getQuery() { 71 | return this.body.request.query 72 | } 73 | function getBody() { 74 | return this.body.request.body 75 | } 76 | function getResponse() { 77 | return this.body.response 78 | } 79 | function getResponseBody() { 80 | return this.body.response.body 81 | } 82 | function getResponseHeaders() { 83 | return this.body.response.headers 84 | } 85 | function getResponseStatusCode() { 86 | return this.body.response.statusCode 87 | } 88 | 89 | async function postDecoratorHandler(request, reply) { 90 | const ret = await reply.context.config.handler(request, reply) 91 | if (ret === undefined) { 92 | reply.code(LEAVE_UNCHANGED_RESPONSE_STATUS_CODE).send() 93 | return 94 | } 95 | if (ret.type === 'CHANGE_RESPONSE') { 96 | return ret.getResponseBody() 97 | } 98 | if (ret.type === 'ABORT_CHAIN') { 99 | reply.code(ABORT_CHAIN_STATUS_CODE) 100 | return ret.getResponseBody() 101 | } 102 | reply.internalServerError('Unknown return type') 103 | } 104 | 105 | function change() { 106 | const data = { 107 | body: undefined, 108 | statusCode: undefined, 109 | headers: undefined, 110 | setBody(body) { 111 | data.body = body 112 | return data 113 | }, 114 | setStatusCode(statusCode) { 115 | data.statusCode = statusCode 116 | return data 117 | }, 118 | setHeaders(headers) { 119 | data.headers = headers 120 | return data 121 | }, 122 | type: 'CHANGE_RESPONSE', 123 | getResponseBody() { 124 | const { body, statusCode, headers } = this 125 | return { body, statusCode, headers } 126 | }, 127 | } 128 | return data 129 | } 130 | 131 | function leaveOriginalRequestUnmodified() { 132 | return undefined 133 | } 134 | 135 | function abortChain(statusCode, body, headers = {}) { 136 | return { 137 | type: 'ABORT_CHAIN', 138 | getResponseBody() { 139 | return { 140 | statusCode, 141 | body, 142 | headers, 143 | } 144 | }, 145 | } 146 | } 147 | 148 | function addPostDecorator(path, handler) { 149 | this.register(function postDecoratorPlugin(fastify, opts, next) { 150 | fastify.decorateRequest('getOriginalRequest', getRequest) 151 | fastify.decorateRequest('getOriginalRequestMethod', getMethod) 152 | fastify.decorateRequest('getOriginalRequestPath', getPath) 153 | fastify.decorateRequest('getOriginalRequestHeaders', getHeaders) 154 | fastify.decorateRequest('getOriginalRequestQuery', getQuery) 155 | fastify.decorateRequest('getOriginalRequestBody', getBody) 156 | 157 | fastify.decorateRequest('getOriginalResponse', getResponse) 158 | fastify.decorateRequest('getOriginalResponseBody', getResponseBody) 159 | fastify.decorateRequest('getOriginalResponseHeaders', getResponseHeaders) 160 | fastify.decorateRequest('getOriginalResponseStatusCode', getResponseStatusCode) 161 | 162 | fastify.decorateRequest('getUserId', getUserIdFromBody) 163 | fastify.decorateRequest('getUserProperties', getUserPropertiesFromBody) 164 | fastify.decorateRequest('getGroups', getGroupsFromBody) 165 | fastify.decorateRequest('getClientType', getClientTypeFromBody) 166 | fastify.decorateRequest('isFromBackOffice', isFromBackOfficeFromBody) 167 | 168 | fastify.decorateRequest('changeOriginalResponse', change) 169 | fastify.decorateRequest('leaveOriginalResponseUnmodified', leaveOriginalRequestUnmodified) 170 | fastify.decorateRequest('abortChain', abortChain) 171 | 172 | fastify.post(path, { 173 | schema: POST_DECORATOR_SCHEMA, 174 | config: { handler: handler.bind(fastify) }, 175 | }, postDecoratorHandler) 176 | 177 | next() 178 | }) 179 | // chainable 180 | return this 181 | } 182 | 183 | addPostDecorator[Symbol.for('plugin-meta')] = { 184 | decorators: { 185 | request: ['USERID_HEADER_KEY', 'USER_PROPERTIES_HEADER_KEY', 'GROUPS_HEADER_KEY', 'CLIENTTYPE_HEADER_KEY', 'BACKOFFICE_HEADER_KEY'], 186 | }, 187 | } 188 | 189 | module.exports = addPostDecorator 190 | -------------------------------------------------------------------------------- /lib/preDecorator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const LEAVE_UNCHANGED_REQUEST_STATUS_CODE = 204 20 | 21 | const { 22 | ABORT_CHAIN_STATUS_CODE, 23 | getUserIdFromBody, 24 | getUserPropertiesFromBody, 25 | getGroupsFromBody, 26 | getClientTypeFromBody, 27 | isFromBackOfficeFromBody, 28 | } = require('./decoratorsCommonFunctions') 29 | 30 | const PRE_DECORATOR_SCHEMA = { 31 | body: { 32 | type: 'object', 33 | required: ['method', 'path', 'headers', 'query'], 34 | properties: { 35 | method: { type: 'string' }, 36 | path: { type: 'string' }, 37 | headers: { type: 'object' }, 38 | query: { type: 'object' }, 39 | body: { }, 40 | }, 41 | }, 42 | } 43 | function getRequest() { 44 | return this.body 45 | } 46 | function getMethod() { 47 | return this.body.method 48 | } 49 | function getPath() { 50 | return this.body.path 51 | } 52 | function getHeaders() { 53 | return this.body.headers 54 | } 55 | function getQuery() { 56 | return this.body.query 57 | } 58 | function getBody() { 59 | return this.body.body 60 | } 61 | 62 | async function preDecoratorHandler(request, reply) { 63 | const ret = await reply.context.config.handler(request, reply) 64 | if (ret === undefined) { 65 | reply.code(LEAVE_UNCHANGED_REQUEST_STATUS_CODE).send() 66 | return 67 | } 68 | if (ret.type === 'CHANGE_REQUEST') { 69 | return ret.getResponseBody() 70 | } 71 | if (ret.type === 'ABORT_CHAIN') { 72 | reply.code(ABORT_CHAIN_STATUS_CODE) 73 | return ret.getResponseBody() 74 | } 75 | reply.internalServerError('Unknown return type') 76 | } 77 | 78 | function change() { 79 | const data = { 80 | body: undefined, 81 | query: undefined, 82 | headers: undefined, 83 | setBody(body) { 84 | data.body = body 85 | return data 86 | }, 87 | setQuery(query) { 88 | data.query = query 89 | return data 90 | }, 91 | setHeaders(headers) { 92 | data.headers = headers 93 | return data 94 | }, 95 | type: 'CHANGE_REQUEST', 96 | getResponseBody() { 97 | const { body, query, headers } = this 98 | return { body, query, headers } 99 | }, 100 | } 101 | return data 102 | } 103 | 104 | function leaveOriginalRequestUnmodified() { 105 | return undefined 106 | } 107 | 108 | function abortChain(statusCode, body, headers = {}) { 109 | return { 110 | type: 'ABORT_CHAIN', 111 | getResponseBody() { 112 | return { 113 | statusCode, 114 | body, 115 | headers, 116 | } 117 | }, 118 | } 119 | } 120 | 121 | function addPreDecorator(path, handler) { 122 | this.register(function preDecoratorPlugin(fastify, opts, next) { 123 | fastify.decorateRequest('getOriginalRequest', getRequest) 124 | fastify.decorateRequest('getOriginalRequestMethod', getMethod) 125 | fastify.decorateRequest('getOriginalRequestPath', getPath) 126 | fastify.decorateRequest('getOriginalRequestHeaders', getHeaders) 127 | fastify.decorateRequest('getOriginalRequestQuery', getQuery) 128 | fastify.decorateRequest('getOriginalRequestBody', getBody) 129 | 130 | fastify.decorateRequest('getUserId', getUserIdFromBody) 131 | fastify.decorateRequest('getUserProperties', getUserPropertiesFromBody) 132 | fastify.decorateRequest('getGroups', getGroupsFromBody) 133 | fastify.decorateRequest('getClientType', getClientTypeFromBody) 134 | fastify.decorateRequest('isFromBackOffice', isFromBackOfficeFromBody) 135 | 136 | fastify.decorateRequest('changeOriginalRequest', change) 137 | fastify.decorateRequest('leaveOriginalRequestUnmodified', leaveOriginalRequestUnmodified) 138 | fastify.decorateRequest('abortChain', abortChain) 139 | 140 | fastify.post(path, { 141 | schema: PRE_DECORATOR_SCHEMA, 142 | config: { handler: handler.bind(fastify) }, 143 | }, preDecoratorHandler) 144 | 145 | next() 146 | }) 147 | // chainable 148 | return this 149 | } 150 | 151 | addPreDecorator[Symbol.for('plugin-meta')] = { 152 | decorators: { 153 | request: ['USERID_HEADER_KEY', 'USER_PROPERTIES_HEADER_KEY', 'GROUPS_HEADER_KEY', 'CLIENTTYPE_HEADER_KEY', 'BACKOFFICE_HEADER_KEY'], 154 | }, 155 | } 156 | 157 | module.exports = addPreDecorator 158 | -------------------------------------------------------------------------------- /lib/rawCustomPlugin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const { getUserId, getGroups, getClientType, isFromBackOffice, getUserProperties } = require('./util') 20 | 21 | function getUserIdFromHeader() { 22 | return getUserId(this.headers[this.USERID_HEADER_KEY]) 23 | } 24 | 25 | function getUserPropertiesFromHeader() { 26 | return getUserProperties(this.headers[this.USER_PROPERTIES_HEADER_KEY]) 27 | } 28 | 29 | function getGroupsFromHeaders() { 30 | return getGroups(this.headers[this.GROUPS_HEADER_KEY] || '') 31 | } 32 | 33 | function getClientTypeFromHeaders() { 34 | return getClientType(this.headers[this.CLIENTTYPE_HEADER_KEY]) 35 | } 36 | 37 | function isFromBackOfficeFromHeaders() { 38 | return isFromBackOffice(this.headers[this.BACKOFFICE_HEADER_KEY]) 39 | } 40 | 41 | function addRawCustomPlugin(method, path, handler, schema, advancedConfigs) { 42 | this.register(function rawCustomPlugin(fastify, options, next) { 43 | fastify.decorateRequest('getUserId', getUserIdFromHeader) 44 | fastify.decorateRequest('getUserProperties', getUserPropertiesFromHeader) 45 | fastify.decorateRequest('getGroups', getGroupsFromHeaders) 46 | fastify.decorateRequest('getClientType', getClientTypeFromHeaders) 47 | fastify.decorateRequest('isFromBackOffice', isFromBackOfficeFromHeaders) 48 | 49 | fastify.route({ ...advancedConfigs, method, path, handler, schema }) 50 | next() 51 | }) 52 | return this 53 | } 54 | 55 | addRawCustomPlugin[Symbol.for('plugin-meta')] = { 56 | decorators: { 57 | request: ['USERID_HEADER_KEY', 'USER_PROPERTIES_HEADER_KEY', 'GROUPS_HEADER_KEY', 'CLIENTTYPE_HEADER_KEY', 'BACKOFFICE_HEADER_KEY', 'ADDITIONAL_HEADERS_TO_PROXY'], 58 | }, 59 | } 60 | 61 | module.exports = addRawCustomPlugin 62 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | function longerThan(len) { 20 | return function longer(arraylike) { 21 | return arraylike.length > len 22 | } 23 | } 24 | 25 | function getUserId(userId) { 26 | return userId || null 27 | } 28 | 29 | function getUserProperties(userPropertiesAsString) { 30 | if (!userPropertiesAsString) { 31 | return null 32 | } 33 | 34 | try { 35 | return JSON.parse(userPropertiesAsString) 36 | } catch (error) { 37 | return null 38 | } 39 | } 40 | 41 | function getGroups(groups) { 42 | return groups.split(',').filter(longerThan(0)) 43 | } 44 | 45 | function getClientType(clientType) { 46 | return clientType || null 47 | } 48 | 49 | function isFromBackOffice(isFBO) { 50 | return Boolean(isFBO) 51 | } 52 | 53 | const extraHeadersKeys = [ 54 | 'x-request-id', 55 | 'x-forwarded-for', 56 | 'x-forwarded-proto', 57 | 'x-forwarded-host', 58 | ] 59 | 60 | module.exports = { 61 | getUserId, 62 | getUserProperties, 63 | getGroups, 64 | getClientType, 65 | isFromBackOffice, 66 | extraHeadersKeys, 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mia-platform/custom-plugin-lib", 3 | "version": "7.0.1", 4 | "description": "Library that allows you to define Mia-Platform custom plugins easily", 5 | "keywords": [ 6 | "mia-platform", 7 | "plugin", 8 | "http-server" 9 | ], 10 | "homepage": "https://www.mia-platform.eu/", 11 | "bugs": { 12 | "url": "https://github.com/mia-platform/custom-plugin-lib/issues", 13 | "email": "core@mia-platform.eu" 14 | }, 15 | "license": "Apache-2.0", 16 | "author": "Mia Platform Core Team ", 17 | "contributors": [ 18 | "Tommaso Allevi ", 19 | "Luca Scannapieco ", 20 | "Paolo Sarti ", 21 | "Jacopo Andrea Giola ", 22 | "Davide Bianchi ", 23 | "Riccardo Di Benedetto ", 24 | "Luca Marzi ", 25 | "Fabio Percivaldi ", 26 | "Francesco Francomano " 27 | ], 28 | "main": "index.js", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/mia-platform/custom-plugin-lib.git" 32 | }, 33 | "scripts": { 34 | "checkonly": "! grep -R '\\.only' tests/", 35 | "coverage": "npm run unit -- --coverage-report=text-summary", 36 | "postcoverage": "tap --coverage-report=html --no-browser", 37 | "lint": "eslint . --ignore-path .gitignore", 38 | "start": "echo 'unable to start the library directly' && exit 1", 39 | "test": "npm run lint && npm run unit && npm run checkonly && npm run typescript", 40 | "typescript": "tsc --project ./tests/types/tsconfig.json", 41 | "unit": "tap -b -o tap.log tests/*.test.js", 42 | "version": "./scripts/update-version.sh ${npm_package_version} && git add CHANGELOG.md" 43 | }, 44 | "dependencies": { 45 | "@fastify/env": "^4.2.0", 46 | "@fastify/formbody": "^7.0.0", 47 | "@mia-platform/lc39": "^8.0.0", 48 | "@types/node": "^18.15.3", 49 | "ajv": "^8.12.0", 50 | "ajv-formats": "^2.1.1", 51 | "axios": "^1.7.9", 52 | "fastify-plugin": "^4.5.0", 53 | "http-errors": "^2.0.0", 54 | "pino": "^8.0.0", 55 | "simple-get": "^4.0.1" 56 | }, 57 | "devDependencies": { 58 | "@fastify/routes": "^5.1.0", 59 | "@mia-platform/eslint-config-mia": "^3.0.0", 60 | "eslint": "^8.40.0", 61 | "hpagent": "^1.2.0", 62 | "nock": "^13.3.1", 63 | "proxy": "^2.0.1", 64 | "split2": "^4.2.0", 65 | "swagger-parser": "^10.0.3", 66 | "tap": "^16.3.4", 67 | "typescript": "^5.0.4" 68 | }, 69 | "engines": { 70 | "node": ">=14" 71 | }, 72 | "eslintConfig": { 73 | "extends": "@mia-platform/eslint-config-mia" 74 | }, 75 | "tap": { 76 | "check-coverage": false 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /scripts/update-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Copyright 2019 Mia srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | 20 | umask 077 21 | 22 | # Automatically get the folder where the source files must be placed. 23 | __DIR=$(dirname "${0}") 24 | SOURCE_DIR="${__DIR}/../" 25 | TAG_VALUE="${1}" 26 | 27 | # Create a variable that contains the current date in UTC 28 | # Different flow if this script is running on Darwin or Linux machines. 29 | if [ "$(uname)" = "Darwin" ]; then 30 | NOW_DATE="$(date -u +%F)" 31 | else 32 | NOW_DATE="$(date -u -I)" 33 | fi 34 | 35 | sed -i.bck "s|## Unreleased|## Unreleased\n\n## v${TAG_VALUE} - ${NOW_DATE}|g" "${SOURCE_DIR}/CHANGELOG.md" 36 | rm -fr "${SOURCE_DIR}/CHANGELOG.md.bck" 37 | -------------------------------------------------------------------------------- /tests/environment.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const tap = require('tap') 20 | const lc39 = require('@mia-platform/lc39') 21 | 22 | const USERID_HEADER_KEY = 'userid-header-key' 23 | const GROUPS_HEADER_KEY = 'groups-header-key' 24 | const CLIENTTYPE_HEADER_KEY = 'clienttype-header-key' 25 | const BACKOFFICE_HEADER_KEY = 'backoffice-header-key' 26 | const MICROSERVICE_GATEWAY_SERVICE_NAME = 'microservice-gateway' 27 | const baseEnv = { 28 | USERID_HEADER_KEY, 29 | GROUPS_HEADER_KEY, 30 | CLIENTTYPE_HEADER_KEY, 31 | BACKOFFICE_HEADER_KEY, 32 | MICROSERVICE_GATEWAY_SERVICE_NAME, 33 | } 34 | 35 | let fastify 36 | async function setupFastify(filePath, envVariables) { 37 | fastify = await lc39(filePath, { 38 | logLevel: 'silent', 39 | envVariables, 40 | }) 41 | 42 | return fastify 43 | } 44 | 45 | function testEnvVariableIsNotEmptyString(test, envVariableName) { 46 | test.test(`Should fail on empty ${envVariableName}`, async assert => { 47 | try { 48 | await setupFastify('./tests/services/plain-custom-service.js', { ...baseEnv, [envVariableName]: '' }) 49 | assert.fail() 50 | } catch (error) { 51 | assert.ok(error) 52 | } 53 | }) 54 | } 55 | 56 | tap.test('Test Environment variables', test => { 57 | const headersToTest = ['USERID_HEADER_KEY', 'GROUPS_HEADER_KEY', 'CLIENTTYPE_HEADER_KEY', 'BACKOFFICE_HEADER_KEY', 'MICROSERVICE_GATEWAY_SERVICE_NAME'] 58 | headersToTest.forEach(header => testEnvVariableIsNotEmptyString(test, header)) 59 | 60 | tap.afterEach(async() => { 61 | if (fastify) { fastify.close() } 62 | }) 63 | 64 | test.test('Should fail if required properties are missing', async assert => { 65 | const { ...badEnv } = baseEnv 66 | delete badEnv.USERID_HEADER_KEY 67 | 68 | try { 69 | await setupFastify('./tests/services/plain-custom-service.js', badEnv) 70 | assert.fail() 71 | } catch (error) { 72 | assert.ok(error) 73 | } 74 | }) 75 | 76 | test.test('Should fail on invalid microservice gateway name (special characters)', async assert => { 77 | try { 78 | await setupFastify('./tests/services/plain-custom-service.js', { 79 | ...baseEnv, 80 | MICROSERVICE_GATEWAY_SERVICE_NAME: '%$£!"', 81 | }) 82 | assert.fail() 83 | } catch (error) { 84 | assert.strictSame(error.message, 'env/MICROSERVICE_GATEWAY_SERVICE_NAME must match pattern "^(?=.{1,253}.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*.?$"') 85 | } 86 | }) 87 | 88 | test.test('Should not fail when microservice gateway name is a valid IP', async assert => { 89 | const options = { 90 | ...baseEnv, 91 | MICROSERVICE_GATEWAY_SERVICE_NAME: '172.16.0.0', 92 | } 93 | 94 | await assert.resolves(async() => { 95 | await setupFastify('./tests/services/plain-custom-service.js', options) 96 | }) 97 | }) 98 | 99 | test.test('Should fail since BASE_REQUIRED_FIELD is not present and CONDITION_FIELD is true and CONDITION_TRUE_REQUIRED_FIELD is not present', async assert => { 100 | const env = { 101 | ...baseEnv, 102 | CONDITION_FIELD: true, 103 | } 104 | 105 | // NOTE: use try catch instead of assert.reject to customize error message assertion 106 | assert.plan(1) 107 | try { 108 | await setupFastify('./tests/services/if-then-else-env-validation-custom-service.js', env) 109 | } catch (error) { 110 | const errorMessage = 'env must have required property \'CONDITION_TRUE_REQUIRED_FIELD\', env must match "then" schema, env must have required property \'BASE_REQUIRED_FIELD\'' 111 | assert.strictSame(error.message, errorMessage) 112 | } 113 | }) 114 | 115 | test.test('Should fail since CONDITION_FIELD is false and CONDITION_FALSE_REQUIRED_FIELD is not present', async assert => { 116 | const env = { 117 | ...baseEnv, 118 | CONDITION_FIELD: false, 119 | } 120 | 121 | // NOTE: use try catch instead of assert.reject to customize error message assertion 122 | assert.plan(1) 123 | try { 124 | await setupFastify('./tests/services/if-then-else-env-validation-custom-service.js', env) 125 | } catch (error) { 126 | const errorMessage = 'env must have required property \'CONDITION_FALSE_REQUIRED_FIELD\', env must match "else" schema, env must have required property \'BASE_REQUIRED_FIELD\'' 127 | assert.strictSame(error.message, errorMessage) 128 | } 129 | }) 130 | 131 | test.test('Should pass since CONDITION_FIELD is true and CONDITION_TRUE_REQUIRED_FIELD is present', async assert => { 132 | const env = { 133 | ...baseEnv, 134 | BASE_REQUIRED_FIELD: 'some-value', 135 | CONDITION_FIELD: true, 136 | CONDITION_TRUE_REQUIRED_FIELD: 'some-value', 137 | } 138 | 139 | await assert.resolves(async() => { 140 | await setupFastify('./tests/services/if-then-else-env-validation-custom-service.js', env) 141 | }) 142 | }) 143 | 144 | test.test('Should fail since none of the anyOf required fields are present', async assert => { 145 | // NOTE: use try catch instead of assert.reject to customize error message assertion 146 | assert.plan(1) 147 | try { 148 | await setupFastify('./tests/services/any-of-env-validation-custom-service.js', baseEnv) 149 | } catch (error) { 150 | const errorMessage = 'env must have required property \'ANY_OF_REQUIRED_FIELD_1\', env must have required property \'ANY_OF_REQUIRED_FIELD_2\', env must match a schema in anyOf' 151 | assert.strictSame(error.message, errorMessage) 152 | } 153 | }) 154 | 155 | test.test('Should pass since one of the anyOf required fields is present', async assert => { 156 | const env = { 157 | ...baseEnv, 158 | ANY_OF_REQUIRED_FIELD_1: 'some-value', 159 | } 160 | 161 | await assert.resolves(async() => { 162 | await setupFastify('./tests/services/any-of-env-validation-custom-service.js', env) 163 | }) 164 | }) 165 | 166 | test.test('Should fail since not all of the allOf required fields are present', async assert => { 167 | const env = { 168 | ...baseEnv, 169 | ALL_OF_REQUIRED_FIELD_1: 'some-value', 170 | } 171 | 172 | // NOTE: use try catch instead of assert.reject to customize error message assertion 173 | assert.plan(1) 174 | try { 175 | await setupFastify('./tests/services/all-of-env-validation-custom-service.js', env) 176 | } catch (error) { 177 | const errorMessage = 'env must have required property \'ALL_OF_REQUIRED_FIELD_2\'' 178 | assert.strictSame(error.message, errorMessage) 179 | } 180 | }) 181 | 182 | test.test('Should pass since all of the allOf required fields are present', async assert => { 183 | const env = { 184 | ...baseEnv, 185 | ALL_OF_REQUIRED_FIELD_1: 'some-value', 186 | ALL_OF_REQUIRED_FIELD_2: 'some-value', 187 | } 188 | 189 | await assert.resolves(async() => { 190 | await setupFastify('./tests/services/all-of-env-validation-custom-service.js', env) 191 | }) 192 | }) 193 | 194 | test.test('Should fail since all of the oneOf required fields are present', async assert => { 195 | const env = { 196 | ...baseEnv, 197 | ONE_OF_REQUIRED_FIELD_1: 'some-value', 198 | ONE_OF_REQUIRED_FIELD_2: 'some-value', 199 | } 200 | 201 | // NOTE: use try catch instead of assert.reject to customize error message assertion 202 | assert.plan(1) 203 | try { 204 | await setupFastify('./tests/services/one-of-env-validation-custom-service.js', env) 205 | } catch (error) { 206 | const errorMessage = 'env must match exactly one schema in oneOf' 207 | assert.strictSame(error.message, errorMessage) 208 | } 209 | }) 210 | 211 | test.test('Should pass since only one of the oneOf required fields is present', async assert => { 212 | const env = { 213 | ...baseEnv, 214 | ONE_OF_REQUIRED_FIELD_1: 'some-value', 215 | } 216 | 217 | await assert.resolves(async() => { 218 | await setupFastify('./tests/services/one-of-env-validation-custom-service.js', env) 219 | }) 220 | }) 221 | 222 | test.test('Should fail since the env has properties already present in the baseEnv of the lib', async assert => { 223 | // NOTE: use try catch instead of assert.reject to customize error message assertion 224 | assert.plan(1) 225 | try { 226 | await setupFastify('./tests/services/overlapping-env-validation-custom-service.js', baseEnv) 227 | } catch (error) { 228 | const errorMessage = 'The provided Environment JSON Schema includes properties declared in the Base JSON Schema of the custom-plugin-lib, please remove them from your schema. The properties to remove are: USERID_HEADER_KEY, USER_PROPERTIES_HEADER_KEY, GROUPS_HEADER_KEY, CLIENTTYPE_HEADER_KEY' 229 | assert.strictSame(error.message, errorMessage) 230 | } 231 | }) 232 | 233 | test.test('Should pass since all of the required fields are present', async assert => { 234 | const env = { 235 | ...baseEnv, 236 | MY_REQUIRED_ENV_VAR: 'value', 237 | } 238 | 239 | await assert.resolves(async() => { 240 | await setupFastify('./tests/services/advanced-first-level-properties-service.js', env) 241 | }) 242 | }) 243 | 244 | test.test('Should fail since one of the required fields is not present', async assert => { 245 | // NOTE: use try catch instead of assert.reject to customize error message assertion 246 | assert.plan(1) 247 | try { 248 | await setupFastify('./tests/services/advanced-first-level-properties-service.js', baseEnv) 249 | } catch (error) { 250 | const errorMessage = 'env must have required property \'MY_REQUIRED_ENV_VAR\'' 251 | assert.strictSame(error.message, errorMessage) 252 | } 253 | }) 254 | 255 | test.end() 256 | }) 257 | -------------------------------------------------------------------------------- /tests/fixtures/keys/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDuzCCAqOgAwIBAgIUEtAjKOuAYiTSm3ye/Fg+y/rLljAwDQYJKoZIhvcNAQEL 3 | BQAwbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk9IMQ8wDQYDVQQHDAZEYXl0b24x 4 | DzANBgNVBAoMBk15IEluYzEPMA0GA1UECwwGRGV2T3BzMR0wGwYJKoZIhvcNAQkB 5 | Fg5yb290QGxvY2FsaG9zdDAgFw0yMTA0MDgwODM1MTdaGA8zMDYxMDkwMzA4MzUx 6 | N1owbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk9IMQ8wDQYDVQQHDAZEYXl0b24x 7 | DzANBgNVBAoMBk15IEluYzEPMA0GA1UECwwGRGV2T3BzMR0wGwYJKoZIhvcNAQkB 8 | Fg5yb290QGxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 9 | AKJPrHFRwjcRtD5aHIdDYc2QGJZaIr5ig6G06w1z/kR+msSZnLlhja0WOzWQ5Q+x 10 | Rj+ShHqFZosp3uEnTP0xTOIdK0FMgGddr+Xcm7dK17bR8faGSHdt0VelqEJsFFWz 11 | 7oNq8bU511cLphHjcmv91jV6iSLlCiN9mDQHSSI77EId5o/3MIDM0VKZ5AR1g+ey 12 | 7FFe7+HWVnUijr4dReJS3RTWqBXMpPVxUXjXwCiKUuYUdycRUAX4MZkb0t/oKe54 13 | 0UTQWS2ghSqub1Oas/B/tA05RkH4QZe/l1i036eHYkA0Go75otLkU01lAVERC8pa 14 | EWe/8gHomggtQf1zmF23l00CAwEAAaNTMFEwHQYDVR0OBBYEFHKGvr7WIYQ3youZ 15 | zYPPmUm1zX8BMB8GA1UdIwQYMBaAFHKGvr7WIYQ3youZzYPPmUm1zX8BMA8GA1Ud 16 | EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFvHvZsMSDscDLHU3Oal6Mp9 17 | BE5IkeHI/sFYWfEBNT56b8tmF491bnl2FeC+/k569/gnXu6cmfms4Mrk34j+fya4 18 | VnB8ws/HRw6RwZRhZZT4m+3F1aSzYNX9uvbUJL4PcmaoMZzlRQ1V3XSB2DKq588L 19 | wkbOIWpt+hn+pfTkc2RKi9/ZZnJ+bLtrIVYqKSVGk33INjR/jmYnR9DpP/+acrdM 20 | 8kq5PD3WKEC1IsI2iD7xdi5k0/dcuzApeTRdkV1DQ5VuSo78xVufKv7qwE760OR4 21 | j/sCP9GPZ1zvu1fjwD6hdGX67cZDDCuy0lAxG+2b5i1+3T82Z6MV6SYCLs3V6JY= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /tests/fixtures/keys/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8jCCAdoCFGWyAxRiRI3BbW+PA+12rbyFFnk0MA0GCSqGSIb3DQEBCwUAMGwx 3 | CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJPSDEPMA0GA1UEBwwGRGF5dG9uMQ8wDQYD 4 | VQQKDAZNeSBJbmMxDzANBgNVBAsMBkRldk9wczEdMBsGCSqGSIb3DQEJARYOcm9v 5 | dEBsb2NhbGhvc3QwIBcNMjEwNDA4MDg0MDQ0WhgPMzA2MTA5MDMwODQwNDRaMIGA 6 | MQswCQYDVQQGEwJVUzELMAkGA1UECAwCT0gxDzANBgNVBAcMBkRheXRvbjEPMA0G 7 | A1UECgwGTXkgSW5jMQ8wDQYDVQQLDAZEZXZPcHMxEjAQBgNVBAMMCWxvY2FsaG9z 8 | dDEdMBsGCSqGSIb3DQEJARYOcm9vdEBsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB 9 | BQADgY0AMIGJAoGBAOpHKvIIKgE9QQ7QRsdViX6YPJ0KgcB+j7fypr3o5JuOCIy8 10 | YOsyGCm8ltgrA1coyaaxfXe2aEB1nlI5RbK8MaIZldIyLU7ib6gz3WeBgoEwYIBV 11 | pUUQ9PM+mPKoh8zag3DKeBC7Uo5bMcaLVStBO5/cHPdlnwxtPJp/kuqik9spAgMB 12 | AAEwDQYJKoZIhvcNAQELBQADggEBABfZe7SazqAks9/zt1jduchf70n0HgZyubao 13 | DjzCcC5YwYR/zBpJI9WvXobg4OuK7zUm+Jt1ekxu7smwZ3IQH5mwtuajvoAu6Nhb 14 | T2pIyxlvGUxDksSEUjE9HGOM0I9ueFZrWZtlJp3ZfyH5/kQd5hp4mYEmf5MRAH36 15 | ROfEMPmeU4vhQOZ6sbXDUK/AUUirYp2yA/hOkAsQCbtKjshJRJbYhBtdmxe0DFlq 16 | 1AcfI4aIcIEpDVImvBQmlbWIpJJY7NpuWrSvgVY4cFW4A21FmXB2kP5IqVfMd7NC 17 | UR3qOvShvVndDp7BrDUt3d7Y2hHi0T05/xRW0BeKOEXkAMi04Fc= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /tests/fixtures/keys/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOpHKvIIKgE9QQ7Q 3 | RsdViX6YPJ0KgcB+j7fypr3o5JuOCIy8YOsyGCm8ltgrA1coyaaxfXe2aEB1nlI5 4 | RbK8MaIZldIyLU7ib6gz3WeBgoEwYIBVpUUQ9PM+mPKoh8zag3DKeBC7Uo5bMcaL 5 | VStBO5/cHPdlnwxtPJp/kuqik9spAgMBAAECgYAAxpWM0ZwZ/vLtMqFZXZ4j7isd 6 | NgCx+3lMWaBVtqSzIyhCFwmXHcZYtQgm1RWgv0DTrXjF4bO8qucITKo/akIbzqyR 7 | lQm2YP2Epw/BULUiGicD+o5I+Mp6pt+SdDLtuWFIYaQe4PDJKtRrAD3znnQdnads 8 | gddyE+ZzMwShe1EGtQJBAPspNdyGW9dEG4lbQlGIZrfSAehAnnkPKKlwIEWdnCmf 9 | 0oCq2XfItNuaV14dRguXLL30Z+lt8EskdtMC3hE4mU8CQQDuyq+smqkSXrLpu4HW 10 | 9/fsB9nFj62VAZUUTi/ay5uuhYWanDvpGHgI7cD6i9HbHeEakouh/6gcwLjyXek8 11 | /zYHAkEA76cE8+VoYOFl/QyNHUsv6BqFI131WMI0JGkPAh4/0h/kNk3Pr9JdcGW3 12 | E+CO46ReVuqM9FmovaqPjwzfZENDVQJBAJHV9a5LkoBml4wJ+UUpkh9zt/thQVjk 13 | a4DIM4/Dk+PlJfCEBE7ao8yIL7iUlejlean54A9jDO/qf8l9s0mOcBcCQFNQQu/g 14 | byiPspJrsCriNhGxFJ/fVubR3l+p+eUDwjMY5JTkdCN6sN/sChL9v5EE0s2eHoBL 15 | bUEWt61lERTeKrI= 16 | -----END PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /tests/fixtures/keys/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8jCCAdoCFGWyAxRiRI3BbW+PA+12rbyFFnkzMA0GCSqGSIb3DQEBCwUAMGwx 3 | CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJPSDEPMA0GA1UEBwwGRGF5dG9uMQ8wDQYD 4 | VQQKDAZNeSBJbmMxDzANBgNVBAsMBkRldk9wczEdMBsGCSqGSIb3DQEJARYOcm9v 5 | dEBsb2NhbGhvc3QwIBcNMjEwNDA4MDgzNzAwWhgPMzA2MTA5MDMwODM3MDBaMIGA 6 | MQswCQYDVQQGEwJVUzELMAkGA1UECAwCT0gxDzANBgNVBAcMBkRheXRvbjEPMA0G 7 | A1UECgwGTXkgSW5jMQ8wDQYDVQQLDAZEZXZPcHMxEjAQBgNVBAMMCWxvY2FsaG9z 8 | dDEdMBsGCSqGSIb3DQEJARYOcm9vdEBsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEB 9 | BQADgY0AMIGJAoGBANA65xPRecdouGwVQTxZwdmT2DjGEpsstrr4IBbjjki3H1Sm 10 | qcLfTuPR/8Bb/8kRB2qszu8M6YURHo6J+FOfP9zumk/cJmxfQWJxuO/NmS/jbYM7 11 | fXZGHkbPNJK3J0Ej+tYipwi2yNESmEWDI3GqK+0Wf6fChI3YjOiehl7o355BAgMB 12 | AAEwDQYJKoZIhvcNAQELBQADggEBABrmIejE1CFKXMZbTO9P7prKIQS5VJAxzfiF 13 | aCeBjW98W0BuA1EVyZI55dphZOnAv6gnKJ7ln0pkaRzBou7TJddpshMxfysALAin 14 | lhT8+IfsrJ7VsCJpzOmh63h8iUDiMCHu+Y73Js/EJMwvwUxJTmHvC/aTHopJAFit 15 | w0KUssggBaIEKmxQm/EXcamcjJOy7o0/gXYCppvztY7iOHTDsLgtemctAHW5SyB4 16 | O4JwS8cr9uSyUq0vUkXXOQIM+v0ciHs1yMtXrTj1tbrube6WqOS8iv6u7J0RuTDS 17 | To5DMjWYz00htbpYy74xE6NDP9Mguqi4EFfPnyLM8eDllO8zKew= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /tests/fixtures/keys/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdAIBADANBgkqhkiG9w0BAQEFAASCAl4wggJaAgEAAoGBANA65xPRecdouGwV 3 | QTxZwdmT2DjGEpsstrr4IBbjjki3H1SmqcLfTuPR/8Bb/8kRB2qszu8M6YURHo6J 4 | +FOfP9zumk/cJmxfQWJxuO/NmS/jbYM7fXZGHkbPNJK3J0Ej+tYipwi2yNESmEWD 5 | I3GqK+0Wf6fChI3YjOiehl7o355BAgMBAAECgYAFi1v1wiEc9yPZ9OLsyXMI6Q/b 6 | yyJZsWIYLl0kXuC9/Oo/pcRlZO7D0CagJ5Ly9poc9Ab6hHx/R4ppvzC2gUoA+qfJ 7 | zlkblN6cIKdh9VaGvI17UZZzBBUiHN8AatFAXFdXm5w7vY8mAHXpRd4a5Y2BKQml 8 | 5g8dNNmEYRzfA+pwFQJBAO9lINd+YZRA1Q9FNeR+X4axvkgs3LBPHbs63bokkJIk 9 | 4iou/XJomtjAVYZSF8daOvtubXUrKagC/QPEIdO/IYsCQQDerGMUErT5VTOQAJ6q 10 | GOVUVtryLNyysC1U8uqP6SMPKlDXe86PzGXQ0Mm65E+FKZ0xsB5TEZ0dzRPbIBxm 11 | CqDjAkAkYdIj7ekWhuPadkJCf5I0/j5U6byAbwWttryL1ZLDIyfcEVgjUxJ1boWQ 12 | 7FkAyw27uISaEf06s3mQYPZjH+ERAkBTubnfWFFX1uN2Z+VAy++e0LGukZPCVGAX 13 | KudriRu3ng+bll/Kze4SoA7aCPKlfUov3qroTR0okW2/3MkQzTpFAj8aiUoESQ6d 14 | UT0X+Wviikjb8f5PWMmE5/rXrh/ayZWGDIUIaSvpQ4JZJuKzUVD9fWCXS3tVNUDK 15 | kq54XfysiwY= 16 | -----END PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /tests/getHeadersToProxy.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const tap = require('tap') 20 | const lc39 = require('@mia-platform/lc39') 21 | 22 | const USERID_HEADER_KEY = 'userid-header-key' 23 | const GROUPS_HEADER_KEY = 'groups-header-key' 24 | const CLIENTTYPE_HEADER_KEY = 'clienttype-header-key' 25 | const BACKOFFICE_HEADER_KEY = 'backoffice-header-key' 26 | const USER_PROPERTIES_HEADER_KEY = 'userproperties-header-key' 27 | const MICROSERVICE_GATEWAY_SERVICE_NAME = 'microservice-gateway' 28 | const ADDITIONAL_HEADERS_TO_PROXY = 'additionalheader1,additionalheader2' 29 | 30 | const X_REQUEST_ID_HEADER_KEY = 'x-request-id' 31 | const X_FORWARDED_FOR_HEADER_KEY = 'x-forwarded-for' 32 | const X_FORWARDED_PROTO_HEADER_KEY = 'x-forwarded-proto' 33 | const X_FORWARDED_HOST_HEADER_KEY = 'x-forwarded-host' 34 | 35 | const baseEnv = { 36 | USERID_HEADER_KEY, 37 | GROUPS_HEADER_KEY, 38 | CLIENTTYPE_HEADER_KEY, 39 | BACKOFFICE_HEADER_KEY, 40 | USER_PROPERTIES_HEADER_KEY, 41 | MICROSERVICE_GATEWAY_SERVICE_NAME, 42 | } 43 | 44 | async function setupFastify(filePath, envVariables) { 45 | const fastify = await lc39(filePath, { 46 | logLevel: 'silent', 47 | envVariables, 48 | }) 49 | 50 | return fastify 51 | } 52 | 53 | const headers = { 54 | [CLIENTTYPE_HEADER_KEY]: 'CMS', 55 | [USERID_HEADER_KEY]: 'userid', 56 | [GROUPS_HEADER_KEY]: 'group-to-greet,group', 57 | [BACKOFFICE_HEADER_KEY]: '', 58 | [USER_PROPERTIES_HEADER_KEY]: '{"foo":"bar"}', 59 | [X_REQUEST_ID_HEADER_KEY]: 'request-id', 60 | [X_FORWARDED_FOR_HEADER_KEY]: '8.8.8.8, 10.0.0.1, 172.16.0.1, 192.168.0.1', 61 | [X_FORWARDED_PROTO_HEADER_KEY]: 'https', 62 | [X_FORWARDED_HOST_HEADER_KEY]: 'www.hostname.tld', 63 | donotproxy: 'foo', 64 | additionalheader1: 'bar', 65 | } 66 | 67 | tap.test('getHeadersToProxy basic', async assert => { 68 | const fastify = await setupFastify('./tests/services/get-headers-to-proxy.js', baseEnv) 69 | const response = await fastify.inject({ 70 | method: 'POST', 71 | url: '/default', 72 | headers, 73 | }) 74 | 75 | assert.equal(response.statusCode, 200) 76 | assert.strictSame(JSON.parse(response.payload), { 77 | [CLIENTTYPE_HEADER_KEY]: 'CMS', 78 | [USERID_HEADER_KEY]: 'userid', 79 | [GROUPS_HEADER_KEY]: 'group-to-greet,group', 80 | [USER_PROPERTIES_HEADER_KEY]: '{"foo":"bar"}', 81 | [X_REQUEST_ID_HEADER_KEY]: 'request-id', 82 | [X_FORWARDED_FOR_HEADER_KEY]: '8.8.8.8, 10.0.0.1, 172.16.0.1, 192.168.0.1', 83 | [X_FORWARDED_PROTO_HEADER_KEY]: 'https', 84 | [X_FORWARDED_HOST_HEADER_KEY]: 'www.hostname.tld', 85 | }) 86 | }) 87 | 88 | tap.test('getHeadersToProxy with isMiaHeaderInjected to false', async assert => { 89 | const fastify = await setupFastify('./tests/services/get-headers-to-proxy.js', baseEnv) 90 | const response = await fastify.inject({ 91 | method: 'POST', 92 | payload: { 93 | options: { 94 | isMiaHeaderInjected: false, 95 | }, 96 | }, 97 | url: '/default', 98 | headers, 99 | }) 100 | 101 | assert.equal(response.statusCode, 200) 102 | assert.strictSame(JSON.parse(response.payload), {}) 103 | }) 104 | 105 | tap.test('getHeadersToProxy - with isMiaHeaderInjected to false and additional headers to proxy', async assert => { 106 | const fastify = await setupFastify('./tests/services/get-headers-to-proxy.js', { 107 | ...baseEnv, 108 | ADDITIONAL_HEADERS_TO_PROXY, 109 | }) 110 | const response = await fastify.inject({ 111 | method: 'POST', 112 | payload: { 113 | options: { 114 | isMiaHeaderInjected: false, 115 | }, 116 | }, 117 | url: '/default', 118 | headers, 119 | }) 120 | 121 | assert.equal(response.statusCode, 200) 122 | assert.strictSame(JSON.parse(response.payload), { 123 | additionalheader1: 'bar', 124 | }) 125 | }) 126 | 127 | tap.test('getHeadersToProxy - additional headers to proxy', async assert => { 128 | const fastify = await setupFastify('./tests/services/get-headers-to-proxy.js', { 129 | ...baseEnv, 130 | ADDITIONAL_HEADERS_TO_PROXY, 131 | }) 132 | const response = await fastify.inject({ 133 | method: 'POST', 134 | url: '/default', 135 | headers, 136 | }) 137 | 138 | assert.equal(response.statusCode, 200) 139 | assert.strictSame(JSON.parse(response.payload), { 140 | [CLIENTTYPE_HEADER_KEY]: 'CMS', 141 | [USERID_HEADER_KEY]: 'userid', 142 | [GROUPS_HEADER_KEY]: 'group-to-greet,group', 143 | [USER_PROPERTIES_HEADER_KEY]: '{"foo":"bar"}', 144 | [X_REQUEST_ID_HEADER_KEY]: 'request-id', 145 | [X_FORWARDED_FOR_HEADER_KEY]: '8.8.8.8, 10.0.0.1, 172.16.0.1, 192.168.0.1', 146 | [X_FORWARDED_PROTO_HEADER_KEY]: 'https', 147 | [X_FORWARDED_HOST_HEADER_KEY]: 'www.hostname.tld', 148 | additionalheader1: 'bar', 149 | }) 150 | }) 151 | 152 | tap.test('getHeadersToProxy - no request headers', async assert => { 153 | const fastify = await setupFastify('./tests/services/get-headers-to-proxy.js', { 154 | ...baseEnv, 155 | ADDITIONAL_HEADERS_TO_PROXY, 156 | }) 157 | const response = await fastify.inject({ 158 | method: 'POST', 159 | url: '/default', 160 | }) 161 | 162 | assert.equal(response.statusCode, 200) 163 | assert.strictSame(JSON.parse(response.payload), {}) 164 | }) 165 | 166 | tap.test('getHeadersToProxy - backoffice header to true', async assert => { 167 | const fastify = await setupFastify('./tests/services/get-headers-to-proxy.js', { 168 | ...baseEnv, 169 | ADDITIONAL_HEADERS_TO_PROXY, 170 | }) 171 | const response = await fastify.inject({ 172 | method: 'POST', 173 | url: '/default', 174 | headers: { 175 | [BACKOFFICE_HEADER_KEY]: '1', 176 | }, 177 | }) 178 | 179 | assert.equal(response.statusCode, 200) 180 | assert.strictSame(JSON.parse(response.payload), { 181 | [BACKOFFICE_HEADER_KEY]: '1', 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /tests/getHttpClient.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const nock = require('nock') 5 | 6 | const { getHttpClient } = require('../index') 7 | 8 | const MY_AWESOME_SERVICE_PROXY_HTTP_URL = 'http://my-awesome-service' 9 | const MY_AWESOME_SERVICE_PROXY_HTTPS_URL = 'https://my-awesome-service' 10 | const MY_AWESOME_SERVICE_PROXY_HTTP_URL_CUSTOM_PORT = 'http://my-awesome-service:3000' 11 | const MY_AWESOME_SERVICE_PROXY_HTTPS_URL_CUSTOM_PORT = 'https://my-awesome-service:3001' 12 | 13 | const fastifyMock = { 14 | httpClientMetrics: { 15 | requestDuration: { 16 | observe: () => { /* no-op*/ }, 17 | }, 18 | }, 19 | } 20 | 21 | tap.test('getHttpClient available for testing - complete url passed', async t => { 22 | nock.disableNetConnect() 23 | t.teardown(() => { 24 | nock.enableNetConnect() 25 | }) 26 | 27 | const RETURN_MESSAGE = 'OK' 28 | const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTP_URL) 29 | const awesomeHttpServiceScope = nock(`${MY_AWESOME_SERVICE_PROXY_HTTP_URL}:80`) 30 | .get('/test-endpoint') 31 | .reply(200, { 32 | message: RETURN_MESSAGE, 33 | }) 34 | 35 | const result = await customProxy.get('/test-endpoint') 36 | 37 | t.strictSame(result.statusCode, 200) 38 | t.strictSame(result.payload.message, RETURN_MESSAGE) 39 | awesomeHttpServiceScope.done() 40 | }) 41 | 42 | tap.test('getHttpClient available for testing - timeout passed', async t => { 43 | nock.disableNetConnect() 44 | t.teardown(() => { 45 | nock.enableNetConnect() 46 | }) 47 | 48 | const RETURN_MESSAGE = 'OK' 49 | const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTP_URL, { 50 | timeout: 100, 51 | }) 52 | const awesomeHttpServiceScope = nock(`${MY_AWESOME_SERVICE_PROXY_HTTP_URL}:80`) 53 | .get('/test-endpoint') 54 | .delay(101) 55 | .reply(200, { 56 | message: RETURN_MESSAGE, 57 | }) 58 | 59 | try { 60 | await customProxy.get('/test-endpoint') 61 | t.fail('should not reach this point') 62 | } catch (error) { 63 | t.strictSame(error.message, 'timeout of 100ms exceeded') 64 | } 65 | 66 | awesomeHttpServiceScope.done() 67 | }) 68 | 69 | tap.test('getHttpClient available for testing - https url', async t => { 70 | nock.disableNetConnect() 71 | t.teardown(() => { 72 | nock.enableNetConnect() 73 | }) 74 | 75 | const RETURN_MESSAGE = 'OK' 76 | const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTPS_URL) 77 | const awesomeHttpsServiceScope = nock(`${MY_AWESOME_SERVICE_PROXY_HTTPS_URL}:443`) 78 | .get('/test-endpoint') 79 | .reply(200, { 80 | message: RETURN_MESSAGE, 81 | }) 82 | 83 | const result = await customProxy.get('/test-endpoint') 84 | 85 | t.strictSame(result.statusCode, 200) 86 | t.strictSame(result.payload.message, RETURN_MESSAGE) 87 | awesomeHttpsServiceScope.done() 88 | }) 89 | 90 | tap.test('getHttpClient available for testing - custom port 3000 - custom headers', async t => { 91 | nock.disableNetConnect() 92 | t.teardown(() => { 93 | nock.enableNetConnect() 94 | }) 95 | 96 | const RETURN_MESSAGE = 'OK' 97 | const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTP_URL_CUSTOM_PORT, 98 | { 99 | headers: { 100 | 'test-header': 'test header works', 101 | }, 102 | }) 103 | const awesomeHttpServiceScope = nock(`${MY_AWESOME_SERVICE_PROXY_HTTP_URL}:3000`) 104 | .matchHeader('test-header', 'test header works') 105 | .get('/test-endpoint') 106 | .reply(200, { 107 | message: RETURN_MESSAGE, 108 | }) 109 | 110 | const result = await customProxy.get('/test-endpoint') 111 | 112 | t.strictSame(result.statusCode, 200) 113 | t.strictSame(result.payload.message, RETURN_MESSAGE) 114 | awesomeHttpServiceScope.done() 115 | }) 116 | 117 | tap.test('getHttpClient available for testing - https url - custom port 3001', async t => { 118 | nock.disableNetConnect() 119 | t.teardown(() => { 120 | nock.enableNetConnect() 121 | }) 122 | 123 | const RETURN_MESSAGE = 'OK' 124 | const customProxy = getHttpClient.call(fastifyMock, MY_AWESOME_SERVICE_PROXY_HTTPS_URL_CUSTOM_PORT) 125 | const awesomeHttpsServiceScope = nock(`${MY_AWESOME_SERVICE_PROXY_HTTPS_URL}:3001`) 126 | .get('/test-endpoint') 127 | .reply(200, { 128 | message: RETURN_MESSAGE, 129 | }) 130 | 131 | const result = await customProxy.get('/test-endpoint') 132 | 133 | t.strictSame(result.statusCode, 200) 134 | t.strictSame(result.payload.message, RETURN_MESSAGE) 135 | awesomeHttpsServiceScope.done() 136 | }) 137 | 138 | tap.test('getHttpClient throws on invalid url', async t => { 139 | const invalidUrl = 'httpnot-a-complete-url' 140 | try { 141 | getHttpClient.call(fastifyMock, invalidUrl) 142 | } catch (error) { 143 | t.notOk(true, 'The function should not throw anymore if the url is not a valid one, bet return the standard proxy') 144 | } 145 | }) 146 | -------------------------------------------------------------------------------- /tests/oas.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const lc39 = require('@mia-platform/lc39') 5 | const Swagger = require('swagger-parser') 6 | 7 | async function setupFastify(filePath, envVariables) { 8 | return lc39(filePath, { 9 | logLevel: 'silent', 10 | envVariables, 11 | }) 12 | } 13 | 14 | const USERID_HEADER_KEY = 'userid-header-key' 15 | const GROUPS_HEADER_KEY = 'groups-header-key' 16 | const CLIENTTYPE_HEADER_KEY = 'clienttype-header-key' 17 | const BACKOFFICE_HEADER_KEY = 'backoffice-header-key' 18 | const MICROSERVICE_GATEWAY_SERVICE_NAME = 'microservice-gateway' 19 | 20 | const baseEnv = { 21 | USERID_HEADER_KEY, 22 | GROUPS_HEADER_KEY, 23 | CLIENTTYPE_HEADER_KEY, 24 | BACKOFFICE_HEADER_KEY, 25 | MICROSERVICE_GATEWAY_SERVICE_NAME, 26 | } 27 | 28 | tap.test('create a valid docs with the support of $ref schema', async t => { 29 | const fastify = await setupFastify('./tests/services/plain-custom-service.js', baseEnv) 30 | 31 | const res = await fastify.inject({ 32 | method: 'GET', 33 | url: '/documentation/json', 34 | }) 35 | 36 | await Swagger.validate(JSON.parse(res.payload)) 37 | 38 | t.end() 39 | }) 40 | 41 | 42 | tap.test('create a valid docs with custom vocabulary', async t => { 43 | const fastify = await setupFastify('./tests/services/service-with-formats.js', baseEnv) 44 | 45 | const res = await fastify.inject({ 46 | method: 'GET', 47 | url: '/documentation/json', 48 | }) 49 | 50 | await Swagger.validate(JSON.parse(res.payload)) 51 | 52 | t.end() 53 | }) 54 | -------------------------------------------------------------------------------- /tests/postDecorator.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const tap = require('tap') 20 | const lc39 = require('@mia-platform/lc39') 21 | 22 | const USERID_HEADER_KEY = 'userid-header-key' 23 | const USER_PROPERTIES_HEADER_KEY = 'miauserproperties' 24 | const GROUPS_HEADER_KEY = 'groups-header-key' 25 | const CLIENTTYPE_HEADER_KEY = 'clienttype-header-key' 26 | const BACKOFFICE_HEADER_KEY = 'backoffice-header-key' 27 | const MICROSERVICE_GATEWAY_SERVICE_NAME = 'microservice-gateway' 28 | const baseEnv = { 29 | USERID_HEADER_KEY, 30 | USER_PROPERTIES_HEADER_KEY, 31 | GROUPS_HEADER_KEY, 32 | CLIENTTYPE_HEADER_KEY, 33 | BACKOFFICE_HEADER_KEY, 34 | MICROSERVICE_GATEWAY_SERVICE_NAME, 35 | } 36 | 37 | async function setupFastify(envVariables) { 38 | const fastify = await lc39('./tests/services/post-decorator.js', { 39 | logLevel: 'silent', 40 | envVariables, 41 | }) 42 | return fastify 43 | } 44 | 45 | tap.test('Test Post Decorator function', test => { 46 | test.test('Return the signal to not change the response', async assert => { 47 | const fastify = await setupFastify(baseEnv) 48 | 49 | const response = await fastify.inject({ 50 | method: 'POST', 51 | url: '/response-unmodified', 52 | payload: { 53 | request: { 54 | method: 'GET', 55 | path: '/the-original-request-path', 56 | query: { my: 'query' }, 57 | body: { the: 'body' }, 58 | headers: { my: 'headers' }, 59 | }, 60 | response: { 61 | body: { the: 'response body' }, 62 | headers: { my: 'response headers' }, 63 | statusCode: 200, 64 | }, 65 | }, 66 | }) 67 | assert.strictSame(response.statusCode, 204) 68 | assert.strictSame(response.payload, '') 69 | assert.end() 70 | }) 71 | 72 | test.test('Return a modified body response', async assert => { 73 | const fastify = await setupFastify(baseEnv) 74 | 75 | const response = await fastify.inject({ 76 | method: 'POST', 77 | url: '/change-original-body', 78 | payload: { 79 | request: { 80 | method: 'GET', 81 | path: '/the-original-request-path', 82 | query: { my: 'query' }, 83 | body: { the: 'body' }, 84 | headers: { my: 'headers' }, 85 | }, 86 | response: { 87 | body: { the: 'response body' }, 88 | headers: { my: 'response headers' }, 89 | statusCode: 200, 90 | }, 91 | }, 92 | }) 93 | assert.strictSame(response.statusCode, 200) 94 | assert.ok(/application\/json/.test(response.headers['content-type'])) 95 | assert.ok(/charset=utf-8/.test(response.headers['content-type'])) 96 | assert.strictSame(response.payload, JSON.stringify({ 97 | body: { the: 'new-body' }, 98 | statusCode: 201, 99 | })) 100 | assert.end() 101 | }) 102 | 103 | test.test('Return a modified headers response', async assert => { 104 | const fastify = await setupFastify(baseEnv) 105 | 106 | const response = await fastify.inject({ 107 | method: 'POST', 108 | url: '/change-original-headers', 109 | payload: { 110 | request: { 111 | method: 'GET', 112 | path: '/the-original-request-path', 113 | query: { my: 'query' }, 114 | body: { the: 'body' }, 115 | headers: { my: 'headers' }, 116 | }, 117 | response: { 118 | body: { the: 'response body' }, 119 | headers: { my: 'response headers' }, 120 | statusCode: 200, 121 | }, 122 | }, 123 | }) 124 | assert.strictSame(response.statusCode, 200) 125 | assert.ok(/application\/json/.test(response.headers['content-type'])) 126 | assert.ok(/charset=utf-8/.test(response.headers['content-type'])) 127 | assert.strictSame(JSON.parse(response.payload), { 128 | headers: { the: 'new-header' }, 129 | }) 130 | assert.end() 131 | }) 132 | 133 | test.test('Test a bad handler that doesn\'t return the right type', async assert => { 134 | const fastify = await setupFastify(baseEnv) 135 | 136 | const response = await fastify.inject({ 137 | method: 'POST', 138 | url: '/bad-hook', 139 | payload: { 140 | request: { 141 | method: 'GET', 142 | path: '/the-original-request-path', 143 | query: { my: 'query' }, 144 | body: { the: 'body' }, 145 | headers: { my: 'headers' }, 146 | }, 147 | response: { 148 | body: { the: 'response body' }, 149 | headers: { my: 'response headers' }, 150 | statusCode: 200, 151 | }, 152 | }, 153 | }) 154 | assert.strictSame(response.statusCode, 500) 155 | assert.ok(/application\/json/.test(response.headers['content-type'])) 156 | assert.strictSame(JSON.parse(response.payload), { 157 | error: 'Internal Server Error', 158 | message: 'Unknown return type', 159 | statusCode: 500, 160 | }) 161 | }) 162 | 163 | test.test('abortChain', async assert => { 164 | const fastify = await setupFastify(baseEnv) 165 | 166 | const response = await fastify.inject({ 167 | method: 'POST', 168 | url: '/abort-chain', 169 | payload: { 170 | request: { 171 | method: 'GET', 172 | path: '/the-original-request-path', 173 | query: { my: 'query' }, 174 | body: { the: 'body' }, 175 | headers: { my: 'headers' }, 176 | }, 177 | response: { 178 | body: { the: 'response body' }, 179 | headers: { my: 'response headers' }, 180 | statusCode: 200, 181 | }, 182 | }, 183 | }) 184 | assert.strictSame(response.statusCode, 418) 185 | assert.ok(/application\/json/.test(response.headers['content-type'])) 186 | assert.ok(/charset=utf-8/.test(response.headers['content-type'])) 187 | assert.strictSame(response.payload, JSON.stringify({ 188 | statusCode: 406, 189 | body: { the: 'final body' }, 190 | headers: {}, 191 | })) 192 | assert.end() 193 | }) 194 | 195 | test.test('is able to access to the mia headers correctly', async assert => { 196 | const fastify = await setupFastify(baseEnv) 197 | 198 | const response = await fastify.inject({ 199 | method: 'POST', 200 | url: '/can-access-data', 201 | payload: { 202 | request: { 203 | method: 'GET', 204 | path: '/the-original-request-path', 205 | query: { my: 'query' }, 206 | body: { the: 'body' }, 207 | headers: { 208 | [CLIENTTYPE_HEADER_KEY]: 'CMS', 209 | [USERID_HEADER_KEY]: 'userid', 210 | [USER_PROPERTIES_HEADER_KEY]: JSON.stringify({ prop1: 'value1' }), 211 | [GROUPS_HEADER_KEY]: 'group-to-greet,group', 212 | [BACKOFFICE_HEADER_KEY]: '1', 213 | my: 'headers', 214 | }, 215 | }, 216 | response: { 217 | body: { the: 'response body' }, 218 | headers: { my: 'response headers' }, 219 | statusCode: 200, 220 | }, 221 | }, 222 | }) 223 | assert.strictSame(response.statusCode, 204, response.payload) 224 | assert.end() 225 | }) 226 | 227 | test.test('addPostDecorator is chainable', async assert => { 228 | const fastify = await setupFastify(baseEnv) 229 | 230 | const response = await fastify.inject({ 231 | method: 'POST', 232 | url: '/chained1', 233 | payload: { 234 | request: { 235 | method: 'GET', 236 | path: '/the-original-request-path', 237 | query: { my: 'query' }, 238 | body: { the: 'body' }, 239 | headers: { my: 'headers' }, 240 | }, 241 | response: { 242 | body: { the: 'response body' }, 243 | headers: { my: 'response headers' }, 244 | statusCode: 200, 245 | }, 246 | }, 247 | }) 248 | assert.strictSame(response.statusCode, 204) 249 | 250 | const response1 = await fastify.inject({ 251 | method: 'POST', 252 | url: '/chained2', 253 | payload: { 254 | request: { 255 | method: 'GET', 256 | path: '/the-original-request-path', 257 | query: { my: 'query' }, 258 | body: { the: 'body' }, 259 | headers: { my: 'headers' }, 260 | }, 261 | response: { 262 | body: { the: 'response body' }, 263 | headers: { my: 'response headers' }, 264 | statusCode: 200, 265 | }, 266 | }, 267 | }) 268 | assert.strictSame(response1.statusCode, 204) 269 | }) 270 | 271 | test.end() 272 | }) 273 | -------------------------------------------------------------------------------- /tests/preDecorator.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const tap = require('tap') 20 | const lc39 = require('@mia-platform/lc39') 21 | 22 | const USERID_HEADER_KEY = 'userid-header-key' 23 | const USER_PROPERTIES_HEADER_KEY = 'miauserproperties' 24 | const GROUPS_HEADER_KEY = 'groups-header-key' 25 | const CLIENTTYPE_HEADER_KEY = 'clienttype-header-key' 26 | const BACKOFFICE_HEADER_KEY = 'backoffice-header-key' 27 | const MICROSERVICE_GATEWAY_SERVICE_NAME = 'microservice-gateway' 28 | const baseEnv = { 29 | USERID_HEADER_KEY, 30 | USER_PROPERTIES_HEADER_KEY, 31 | GROUPS_HEADER_KEY, 32 | CLIENTTYPE_HEADER_KEY, 33 | BACKOFFICE_HEADER_KEY, 34 | MICROSERVICE_GATEWAY_SERVICE_NAME, 35 | } 36 | 37 | async function setupFastify(envVariables) { 38 | const fastify = await lc39('./tests/services/pre-decorator.js', { 39 | logLevel: 'silent', 40 | envVariables, 41 | }) 42 | return fastify 43 | } 44 | 45 | tap.test('preDecorator', test => { 46 | test.test('leaveOriginalRequestUnmodified', async assert => { 47 | const fastify = await setupFastify(baseEnv) 48 | 49 | const response = await fastify.inject({ 50 | method: 'POST', 51 | url: '/response-unmodified', 52 | payload: { 53 | method: 'GET', 54 | path: '/the-original-request-path', 55 | query: { my: 'query' }, 56 | body: { the: 'body' }, 57 | headers: { my: 'headers' }, 58 | }, 59 | }) 60 | 61 | assert.strictSame(response.statusCode, 204) 62 | assert.strictSame(response.payload, '') 63 | assert.end() 64 | }) 65 | 66 | test.test('changeOriginalRequest', async assert => { 67 | const fastify = await setupFastify(baseEnv) 68 | const response = await fastify.inject({ 69 | method: 'POST', 70 | url: '/change-original-request', 71 | payload: { 72 | method: 'GET', 73 | path: '/the-original-request-path', 74 | query: { my: 'query' }, 75 | body: { the: 'body' }, 76 | headers: { my: 'headers' }, 77 | }, 78 | }) 79 | assert.strictSame(response.statusCode, 200) 80 | assert.ok(/application\/json/.test(response.headers['content-type'])) 81 | assert.ok(/charset=utf-8/.test(response.headers['content-type'])) 82 | assert.strictSame(response.payload, JSON.stringify({ 83 | body: { the: 'new-body' }, 84 | query: { new: 'querystring' }, 85 | })) 86 | assert.end() 87 | }) 88 | 89 | test.test('changeOriginalRequest with headers', async assert => { 90 | const fastify = await setupFastify(baseEnv) 91 | const response = await fastify.inject({ 92 | method: 'POST', 93 | url: '/change-original-headers', 94 | payload: { 95 | method: 'GET', 96 | path: '/the-original-request-path', 97 | query: { my: 'query' }, 98 | body: { the: 'body' }, 99 | headers: { my: 'headers' }, 100 | }, 101 | }) 102 | assert.strictSame(response.statusCode, 200) 103 | assert.ok(/application\/json/.test(response.headers['content-type'])) 104 | assert.ok(/charset=utf-8/.test(response.headers['content-type'])) 105 | assert.strictSame(response.payload, JSON.stringify({ 106 | body: { the: 'new-body' }, 107 | headers: { new: 'headers' }, 108 | })) 109 | assert.end() 110 | }) 111 | 112 | test.test('test bad handler that doesn\'t return the right type', async assert => { 113 | const fastify = await setupFastify(baseEnv) 114 | const response = await fastify.inject({ 115 | method: 'POST', 116 | url: '/bad-hook', 117 | payload: { 118 | method: 'GET', 119 | path: '/the-original-request-path', 120 | query: { my: 'query' }, 121 | body: { the: 'body' }, 122 | headers: { my: 'headers' }, 123 | }, 124 | }) 125 | assert.strictSame(response.statusCode, 500) 126 | assert.ok(/application\/json/.test(response.headers['content-type'])) 127 | assert.strictSame(JSON.parse(response.payload), { 128 | error: 'Internal Server Error', 129 | message: 'Unknown return type', 130 | statusCode: 500, 131 | }) 132 | }) 133 | 134 | test.test('abortChain', async assert => { 135 | const fastify = await setupFastify(baseEnv) 136 | const response = await fastify.inject({ 137 | method: 'POST', 138 | url: '/abort-chain', 139 | payload: { 140 | method: 'GET', 141 | path: '/the-original-request-path', 142 | query: { my: 'query' }, 143 | body: { the: 'body' }, 144 | headers: { my: 'headers' }, 145 | }, 146 | }) 147 | assert.strictSame(response.statusCode, 418) 148 | assert.ok(/application\/json/.test(response.headers['content-type'])) 149 | assert.ok(/charset=utf-8/.test(response.headers['content-type'])) 150 | assert.strictSame(response.payload, JSON.stringify({ 151 | statusCode: 406, 152 | body: { the: 'final body' }, 153 | headers: {}, 154 | })) 155 | assert.end() 156 | }) 157 | 158 | test.test('is able to access to the mia headers correctly', async assert => { 159 | const fastify = await setupFastify(baseEnv) 160 | const response = await fastify.inject({ 161 | method: 'POST', 162 | url: '/can-access-data', 163 | payload: { 164 | method: 'GET', 165 | path: '/the-original-request-path', 166 | query: { my: 'query' }, 167 | body: { the: 'body' }, 168 | headers: { 169 | [CLIENTTYPE_HEADER_KEY]: 'CMS', 170 | [USERID_HEADER_KEY]: 'userid', 171 | [USER_PROPERTIES_HEADER_KEY]: JSON.stringify({ prop1: 'value1' }), 172 | [GROUPS_HEADER_KEY]: 'group-to-greet,group', 173 | [BACKOFFICE_HEADER_KEY]: '1', 174 | my: 'headers', 175 | }, 176 | }, 177 | }) 178 | assert.strictSame(response.statusCode, 204) 179 | assert.strictSame(response.payload, '') 180 | assert.end() 181 | }) 182 | 183 | test.test('addPreDecorator is Chainable', async assert => { 184 | const fastify = await setupFastify(baseEnv) 185 | const response = await fastify.inject({ 186 | method: 'POST', 187 | url: '/chained1', 188 | payload: { 189 | method: 'GET', 190 | path: '/the-original-request-path', 191 | query: { my: 'query' }, 192 | body: { the: 'body' }, 193 | headers: { my: 'headers' }, 194 | }, 195 | }) 196 | assert.strictSame(response.statusCode, 204) 197 | 198 | const response1 = await fastify.inject({ 199 | method: 'POST', 200 | url: '/chained2', 201 | payload: { 202 | method: 'GET', 203 | path: '/the-original-request-path', 204 | query: { my: 'query' }, 205 | body: { the: 'body' }, 206 | headers: { my: 'headers' }, 207 | }, 208 | }) 209 | assert.strictSame(response1.statusCode, 204) 210 | }) 211 | 212 | test.end() 213 | }) 214 | -------------------------------------------------------------------------------- /tests/services/advanced-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const customService = require('../../index')() 20 | 21 | module.exports = customService(async function clientGroups(service) { 22 | function handlerAdvancedConfig(request, reply) { 23 | reply.send({ 24 | error: request.validationError.message, 25 | config: reply.context.config, 26 | }) 27 | } 28 | 29 | const advancedConfig = { 30 | attachValidation: true, 31 | config: { myConfig: [1, 2, 3, 4] }, 32 | method: 'overwritten property', 33 | path: 'overwritten property', 34 | handler: 'overwritten property', 35 | schema: 'overwritten property', 36 | } 37 | 38 | const schema = { 39 | body: { 40 | type: 'object', 41 | additionalProperties: false, 42 | required: ['foo'], 43 | properties: { 44 | foo: { type: 'number' }, 45 | }, 46 | }, 47 | } 48 | service.addRawCustomPlugin('POST', '/advanced-config', handlerAdvancedConfig, schema, advancedConfig) 49 | }) 50 | -------------------------------------------------------------------------------- /tests/services/advanced-custom-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | const envSchema = { 19 | type: 'object', 20 | required: ['MY_AWESOME_ENV', 'MY_REQUIRED_ENV_VAR'], 21 | properties: { 22 | MY_AWESOME_ENV: { type: 'string', default: 'the default value' }, 23 | MY_REQUIRED_ENV_VAR: { type: 'string' }, 24 | }, 25 | } 26 | 27 | const customService = require('../../index')(envSchema) 28 | 29 | module.exports = customService(async function clientGroups(service) { 30 | const customFunctionality = request => request.body 31 | service.decorate('customFunctionality', customFunctionality) 32 | async function handlerCustom(request, reply) { 33 | reply.send(this.customFunctionality(request)) 34 | } 35 | 36 | function handlerEnv(request, reply) { 37 | reply.send(this.config.MY_AWESOME_ENV) 38 | } 39 | 40 | service.addRawCustomPlugin('GET', '/env', handlerEnv) 41 | service.addRawCustomPlugin('POST', '/custom', handlerCustom) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/services/advanced-first-level-properties-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | const envSchema = { 19 | type: 'object', 20 | required: ['MY_AWESOME_ENV', 'MY_REQUIRED_ENV_VAR'], 21 | properties: { 22 | MY_AWESOME_ENV: { type: 'string', default: 'the default value' }, 23 | MY_REQUIRED_ENV_VAR: { type: 'string' }, 24 | }, 25 | patternProperties: { 26 | '^S_': { 'type': 'string' }, 27 | '^I_': { 'type': 'number' }, 28 | }, 29 | minProperties: 1, 30 | additionalProperties: true, 31 | } 32 | 33 | const customService = require('../../index')(envSchema) 34 | 35 | module.exports = customService(async function clientGroups(service) { 36 | async function handlerApiRest(_, reply) { 37 | reply.send(this.config.required) 38 | } 39 | 40 | service.addRawCustomPlugin('GET', '/api-rest', handlerApiRest) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/services/all-of-env-validation-custom-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const fastifyRoutes = require('@fastify/routes') 20 | 21 | const properties = { 22 | ALL_OF_REQUIRED_FIELD_1: { type: 'string' }, 23 | ALL_OF_REQUIRED_FIELD_2: { type: 'string' }, 24 | } 25 | 26 | const envJsonSchema = { 27 | type: 'object', 28 | properties, 29 | required: [], 30 | allOf: [ 31 | { 32 | required: [ 33 | 'ALL_OF_REQUIRED_FIELD_1', 34 | ], 35 | }, 36 | { 37 | required: [ 38 | 'ALL_OF_REQUIRED_FIELD_2', 39 | ], 40 | }, 41 | ], 42 | } 43 | 44 | const customService = require('../../index')(envJsonSchema) 45 | 46 | module.exports = customService(async function clientGroups(service) { 47 | service.register(fastifyRoutes) 48 | service.addRawCustomPlugin('GET', '/', (request, reply) => { 49 | reply.send('Hello world!') 50 | }) 51 | }) 52 | 53 | module.exports.healthinessHandler = async function healthinessHandler(fastify) { 54 | fastify.assert.ok(fastify.getHttpClient) 55 | fastify.assert.ok(fastify.routes) 56 | return { 57 | statusOk: true, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/services/any-of-env-validation-custom-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const fastifyRoutes = require('@fastify/routes') 20 | 21 | const properties = { 22 | ANY_OF_REQUIRED_FIELD_1: { type: 'string' }, 23 | ANY_OF_REQUIRED_FIELD_2: { type: 'string' }, 24 | } 25 | 26 | const envJsonSchema = { 27 | type: 'object', 28 | properties, 29 | required: [], 30 | anyOf: [ 31 | { 32 | required: [ 33 | 'ANY_OF_REQUIRED_FIELD_1', 34 | ], 35 | }, 36 | { 37 | required: [ 38 | 'ANY_OF_REQUIRED_FIELD_2', 39 | ], 40 | }, 41 | ], 42 | } 43 | 44 | const customService = require('../../index')(envJsonSchema) 45 | 46 | module.exports = customService(async function clientGroups(service) { 47 | service.register(fastifyRoutes) 48 | service.addRawCustomPlugin('GET', '/', (request, reply) => { 49 | reply.send('Hello world!') 50 | }) 51 | }) 52 | 53 | module.exports.healthinessHandler = async function healthinessHandler(fastify) { 54 | fastify.assert.ok(fastify.getHttpClient) 55 | fastify.assert.ok(fastify.routes) 56 | return { 57 | statusOk: true, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/services/get-headers-to-proxy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const customService = require('../../index')() 20 | 21 | module.exports = customService(async function clientGroups(service) { 22 | service.addRawCustomPlugin('POST', '/default', async function handler(req) { 23 | return req.getHeadersToProxy(req.body?.options) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /tests/services/http-client.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const customService = require('../../index')() 20 | 21 | const httpOtherService = 'http://other-service' 22 | const httpsOtherService = 'https://other-service' 23 | 24 | module.exports = customService(async function clientGroups(service) { 25 | service.addRawCustomPlugin('POST', '/default', async function handler(req) { 26 | const { some } = req.body 27 | const proxy = req.getHttpClient(httpOtherService) 28 | const res = await proxy.get('/res') 29 | const { id } = res.payload 30 | return { id, some } 31 | }) 32 | 33 | service.addRawCustomPlugin('POST', '/custom', async function handler(req) { 34 | const { some } = req.body 35 | const proxy = req.getHttpClient(`${httpOtherService}:3000`) 36 | const res = await proxy.get('/res') 37 | const { id } = res.payload 38 | return { id, some } 39 | }) 40 | 41 | service.addPreDecorator('/pre', async function handler(req) { 42 | const { some } = req.getOriginalRequestBody() 43 | const proxy = req.getHttpClient(`${httpsOtherService}:3000`) 44 | const res = await proxy.get('/res') 45 | const { id } = res.payload 46 | return req.changeOriginalRequest().setBody({ id, some }) 47 | }) 48 | 49 | service.addPostDecorator('/post', async function handler(req) { 50 | const { some } = req.getOriginalResponseBody() 51 | const proxy = req.getHttpClient(`${httpsOtherService}:3000`) 52 | const res = await proxy.get('/res') 53 | const { id } = res.payload 54 | return req.changeOriginalResponse().setBody({ id, some }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/services/if-then-else-env-validation-custom-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const fastifyRoutes = require('@fastify/routes') 20 | 21 | const baseProperties = { 22 | BASE_REQUIRED_FIELD: { type: 'string' }, 23 | CONDITION_FIELD: { type: 'boolean' }, 24 | } 25 | 26 | const thenProperties = { 27 | CONDITION_TRUE_REQUIRED_FIELD: { type: 'string' }, 28 | } 29 | 30 | const elseProperties = { 31 | CONDITION_FALSE_REQUIRED_FIELD: { type: 'string' }, 32 | } 33 | 34 | const thenRequired = [ 35 | 'CONDITION_TRUE_REQUIRED_FIELD', 36 | ] 37 | 38 | const baseRequired = [ 39 | 'BASE_REQUIRED_FIELD', 40 | ] 41 | 42 | const elseRequired = [ 43 | 'CONDITION_FALSE_REQUIRED_FIELD', 44 | ] 45 | 46 | const envJsonSchema = { 47 | type: 'object', 48 | properties: baseProperties, 49 | required: baseRequired, 50 | if: { 51 | properties: { 52 | CONDITION_FIELD: { const: true }, 53 | }, 54 | }, 55 | then: { 56 | properties: thenProperties, 57 | required: thenRequired, 58 | }, 59 | else: { 60 | properties: elseProperties, 61 | required: elseRequired, 62 | }, 63 | } 64 | 65 | const customService = require('../../index')(envJsonSchema) 66 | 67 | module.exports = customService(async function clientGroups(service) { 68 | service.register(fastifyRoutes) 69 | service.addRawCustomPlugin('GET', '/', (request, reply) => { 70 | reply.send('Hello world!') 71 | }) 72 | }) 73 | 74 | module.exports.healthinessHandler = async function healthinessHandler(fastify) { 75 | fastify.assert.ok(fastify.getHttpClient) 76 | fastify.assert.ok(fastify.routes) 77 | return { 78 | statusOk: true, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/services/one-of-env-validation-custom-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const fastifyRoutes = require('@fastify/routes') 20 | 21 | const properties = { 22 | ONE_OF_REQUIRED_FIELD_1: { type: 'string' }, 23 | ONE_OF_REQUIRED_FIELD_2: { type: 'string' }, 24 | } 25 | 26 | const envJsonSchema = { 27 | type: 'object', 28 | properties, 29 | required: [], 30 | oneOf: [ 31 | { 32 | required: [ 33 | 'ONE_OF_REQUIRED_FIELD_1', 34 | ], 35 | }, 36 | { 37 | required: [ 38 | 'ONE_OF_REQUIRED_FIELD_2', 39 | ], 40 | }, 41 | ], 42 | } 43 | 44 | const customService = require('../../index')(envJsonSchema) 45 | 46 | module.exports = customService(async function clientGroups(service) { 47 | service.register(fastifyRoutes) 48 | service.addRawCustomPlugin('GET', '/', (request, reply) => { 49 | reply.send('Hello world!') 50 | }) 51 | }) 52 | 53 | module.exports.healthinessHandler = async function healthinessHandler(fastify) { 54 | fastify.assert.ok(fastify.getHttpClient) 55 | fastify.assert.ok(fastify.routes) 56 | return { 57 | statusOk: true, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/services/overlapping-env-validation-custom-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const fastifyRoutes = require('@fastify/routes') 20 | 21 | const USERID_HEADER_KEY = 'USERID_HEADER_KEY' 22 | const USER_PROPERTIES_HEADER_KEY = 'USER_PROPERTIES_HEADER_KEY' 23 | const GROUPS_HEADER_KEY = 'GROUPS_HEADER_KEY' 24 | const CLIENTTYPE_HEADER_KEY = 'CLIENTTYPE_HEADER_KEY' 25 | 26 | const envJsonSchema = { 27 | type: 'object', 28 | properties: { 29 | USERID_HEADER_KEY, 30 | USER_PROPERTIES_HEADER_KEY, 31 | GROUPS_HEADER_KEY, 32 | CLIENTTYPE_HEADER_KEY, 33 | }, 34 | required: [], 35 | } 36 | 37 | const customService = require('../../index')(envJsonSchema) 38 | 39 | module.exports = customService(async function clientGroups(service) { 40 | service.register(fastifyRoutes) 41 | service.addRawCustomPlugin('GET', '/', (request, reply) => { 42 | reply.send('Hello world!') 43 | }) 44 | }) 45 | 46 | module.exports.healthinessHandler = async function healthinessHandler(fastify) { 47 | fastify.assert.ok(fastify.getHttpClient) 48 | fastify.assert.ok(fastify.routes) 49 | return { 50 | statusOk: true, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/services/plain-custom-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const fs = require('fs') 20 | const fastifyRoutes = require('@fastify/routes') 21 | 22 | const customService = require('../../index')() 23 | 24 | const schema = { 25 | body: { 26 | type: 'object', 27 | required: ['some'], 28 | properties: { 29 | some: { type: 'string' }, 30 | foobar: { 31 | $ref: 'foobar#', 32 | }, 33 | nested: { 34 | type: 'object', 35 | properties: { 36 | field: { 37 | type: 'string', 38 | }, 39 | }, 40 | additionalProperties: false, 41 | required: ['field'], 42 | }, 43 | }, 44 | additionalProperties: false, 45 | }, 46 | querystring: { 47 | type: 'object', 48 | properties: { 49 | some: { type: 'number' }, 50 | }, 51 | }, 52 | headers: { 53 | type: 'object', 54 | properties: { 55 | foobar: { 56 | $ref: 'foobar#', 57 | }, 58 | }, 59 | }, 60 | response: { 61 | 200: { 62 | type: 'object', 63 | properties: { 64 | foobar: { 65 | $ref: 'foobar#', 66 | }, 67 | }, 68 | additionalProperties: true, 69 | }, 70 | }, 71 | } 72 | 73 | function customParser(req, payload, done) { 74 | let data = '' 75 | payload.on('data', chunk => { 76 | data += chunk 77 | }) 78 | 79 | payload.on('end', () => { 80 | done(null, data) 81 | }) 82 | } 83 | 84 | module.exports = customService(async function clientGroups(service) { 85 | service.addValidatorSchema({ 86 | $id: 'foobar', 87 | type: 'string', 88 | enum: ['foo1', 'bar2', 'taz3'], 89 | }) 90 | const retrievedSchema = service.getValidatorSchema('foobar') 91 | service.assert.ok(retrievedSchema) 92 | 93 | service.register(fastifyRoutes) 94 | function handler(request, reply) { 95 | reply.send('Hello world!') 96 | } 97 | 98 | function handlePlatformValues(request, reply) { 99 | reply.send({ 100 | userId: request.getUserId(), 101 | userGroups: request.getGroups(), 102 | userProperties: request.getUserProperties(), 103 | clientType: request.getClientType(), 104 | backoffice: request.isFromBackOffice(), 105 | }) 106 | } 107 | 108 | function handleMiaHeaders(request, reply) { 109 | reply.send(request.getMiaHeaders()) 110 | } 111 | 112 | function handlerRespondWithBody(request, reply) { 113 | reply.send(request.body) 114 | } 115 | 116 | function handlerStream(request, reply) { 117 | reply.header('Content-Type', 'application/octet-stream') 118 | reply.send(fs.createReadStream('./tests/services/plain-custom-service.js')) 119 | } 120 | 121 | service.addRawCustomPlugin('GET', '/', handler) 122 | service.addRawCustomPlugin('GET', '/platform-values', handlePlatformValues) 123 | service.addRawCustomPlugin('POST', '/', handlerRespondWithBody) 124 | service.addRawCustomPlugin('GET', '/stream', handlerStream) 125 | service.addRawCustomPlugin('GET', '/mia-headers', handleMiaHeaders) 126 | service.addRawCustomPlugin('POST', '/validation', handlerRespondWithBody, schema) 127 | service.addContentTypeParser('application/custom-type', customParser) 128 | service.addRawCustomPlugin('POST', '/customValidation', handlerRespondWithBody) 129 | }) 130 | 131 | module.exports.healthinessHandler = async function healthinessHandler(fastify) { 132 | fastify.assert.ok(fastify.getHttpClient) 133 | fastify.assert.ok(fastify.routes) 134 | return { 135 | statusOk: true, 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/services/post-decorator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const customService = require('../../index')() 20 | 21 | const USERID_HEADER_KEY = 'userid-header-key' 22 | const USER_PROPERTIES_HEADER_KEY = 'miauserproperties' 23 | const GROUPS_HEADER_KEY = 'groups-header-key' 24 | const CLIENTTYPE_HEADER_KEY = 'clienttype-header-key' 25 | const BACKOFFICE_HEADER_KEY = 'backoffice-header-key' 26 | 27 | module.exports = customService(async function clientGroups(service) { 28 | function responseUnmodifiedHandler(request) { 29 | return request.leaveOriginalResponseUnmodified() 30 | } 31 | service.addPostDecorator('/response-unmodified', responseUnmodifiedHandler) 32 | 33 | function changeOriginalBodyResponseHandler(request) { 34 | return request.changeOriginalResponse() 35 | .setBody({ the: 'new-body' }) 36 | .setStatusCode(201) 37 | } 38 | service.addPostDecorator('/change-original-body', changeOriginalBodyResponseHandler) 39 | 40 | function changeOriginalHeadersResponseHandler(request) { 41 | return request.changeOriginalResponse() 42 | .setHeaders({ the: 'new-header' }) 43 | } 44 | service.addPostDecorator('/change-original-headers', changeOriginalHeadersResponseHandler) 45 | 46 | function badHookHandler() { 47 | return { 48 | not: 'using the decorated functions', 49 | } 50 | } 51 | service.addPostDecorator('/bad-hook', badHookHandler) 52 | 53 | function abortChainHandler(request) { 54 | return request.abortChain(406, { the: 'final body' }) 55 | } 56 | service.addPostDecorator('/abort-chain', abortChainHandler) 57 | 58 | function accessDataHandler(request) { 59 | this.assert.equal(request.getUserId(), 'userid') 60 | this.assert.deepEqual(request.getUserProperties(), { prop1: 'value1' }) 61 | this.assert.deepEqual(request.getGroups(), ['group-to-greet', 'group']) 62 | this.assert.equal(request.getClientType(), 'CMS') 63 | this.assert.equal(request.isFromBackOffice(), true) 64 | 65 | this.assert.deepEqual(request.getOriginalRequest(), { 66 | method: 'GET', 67 | path: '/the-original-request-path', 68 | query: { my: 'query' }, 69 | body: { the: 'body' }, 70 | headers: { 71 | [CLIENTTYPE_HEADER_KEY]: 'CMS', 72 | [USERID_HEADER_KEY]: 'userid', 73 | [USER_PROPERTIES_HEADER_KEY]: JSON.stringify({ prop1: 'value1' }), 74 | [GROUPS_HEADER_KEY]: 'group-to-greet,group', 75 | [BACKOFFICE_HEADER_KEY]: '1', 76 | my: 'headers', 77 | }, 78 | }) 79 | this.assert.deepEqual(request.getOriginalRequestBody(), { the: 'body' }) 80 | this.assert.deepEqual(request.getOriginalRequestHeaders(), { 81 | [CLIENTTYPE_HEADER_KEY]: 'CMS', 82 | [USERID_HEADER_KEY]: 'userid', 83 | [USER_PROPERTIES_HEADER_KEY]: JSON.stringify({ prop1: 'value1' }), 84 | [GROUPS_HEADER_KEY]: 'group-to-greet,group', 85 | [BACKOFFICE_HEADER_KEY]: '1', 86 | my: 'headers', 87 | }) 88 | this.assert.deepEqual(request.getOriginalRequestQuery(), { my: 'query' }) 89 | this.assert.equal(request.getOriginalRequestMethod(), 'GET') 90 | this.assert.equal(request.getOriginalRequestPath(), '/the-original-request-path') 91 | 92 | this.assert.deepEqual(request.getOriginalResponse(), { 93 | body: { the: 'response body' }, 94 | headers: { my: 'response headers' }, 95 | statusCode: 200, 96 | }) 97 | this.assert.deepEqual(request.getOriginalResponseBody(), { the: 'response body' }) 98 | this.assert.deepEqual(request.getOriginalResponseHeaders(), { my: 'response headers' }) 99 | this.assert.equal(request.getOriginalResponseStatusCode(), 200) 100 | 101 | this.assert.ok(this.config) 102 | 103 | return request.leaveOriginalResponseUnmodified() 104 | } 105 | service.addPostDecorator('/can-access-data', accessDataHandler) 106 | 107 | service 108 | .addPostDecorator('/chained1', responseUnmodifiedHandler) 109 | .addPostDecorator('/chained2', responseUnmodifiedHandler) 110 | }) 111 | -------------------------------------------------------------------------------- /tests/services/pre-decorator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const customService = require('../../index')() 20 | 21 | const USERID_HEADER_KEY = 'userid-header-key' 22 | const USER_PROPERTIES_HEADER_KEY = 'miauserproperties' 23 | const GROUPS_HEADER_KEY = 'groups-header-key' 24 | const CLIENTTYPE_HEADER_KEY = 'clienttype-header-key' 25 | const BACKOFFICE_HEADER_KEY = 'backoffice-header-key' 26 | 27 | module.exports = customService(async function clientGroups(service) { 28 | function requestUnmodifiedHandler(request) { 29 | return request.leaveOriginalRequestUnmodified() 30 | } 31 | service.addPreDecorator('/response-unmodified', requestUnmodifiedHandler) 32 | 33 | function changeOriginalBodyRequestHandler(request) { 34 | return request.changeOriginalRequest() 35 | .setBody({ the: 'new-body' }) 36 | .setQuery({ new: 'querystring' }) 37 | } 38 | service.addPreDecorator('/change-original-request', changeOriginalBodyRequestHandler) 39 | 40 | function changeOriginalHeadersRequestHandler(request) { 41 | return request.changeOriginalRequest() 42 | .setBody({ the: 'new-body' }) 43 | .setHeaders({ new: 'headers' }) 44 | } 45 | service.addPreDecorator('/change-original-headers', changeOriginalHeadersRequestHandler) 46 | 47 | function badHookHandler() { 48 | return { 49 | not: 'using the decorated functions', 50 | } 51 | } 52 | service.addPreDecorator('/bad-hook', badHookHandler) 53 | 54 | function abortChainHandler(request) { 55 | return request.abortChain(406, { the: 'final body' }) 56 | } 57 | service.addPreDecorator('/abort-chain', abortChainHandler) 58 | 59 | function accessDataHandler(request) { 60 | this.assert.equal(request.getUserId(), 'userid') 61 | this.assert.deepEqual(request.getUserProperties(), { prop1: 'value1' }) 62 | this.assert.deepEqual(request.getGroups(), ['group-to-greet', 'group']) 63 | this.assert.equal(request.getClientType(), 'CMS') 64 | this.assert.equal(request.isFromBackOffice(), true) 65 | 66 | this.assert.deepEqual(request.getOriginalRequest(), { 67 | method: 'GET', 68 | path: '/the-original-request-path', 69 | query: { my: 'query' }, 70 | body: { the: 'body' }, 71 | headers: { 72 | [CLIENTTYPE_HEADER_KEY]: 'CMS', 73 | [USERID_HEADER_KEY]: 'userid', 74 | [USER_PROPERTIES_HEADER_KEY]: JSON.stringify({ prop1: 'value1' }), 75 | [GROUPS_HEADER_KEY]: 'group-to-greet,group', 76 | [BACKOFFICE_HEADER_KEY]: '1', 77 | my: 'headers', 78 | }, 79 | }) 80 | this.assert.deepEqual(request.getOriginalRequestBody(), { the: 'body' }) 81 | this.assert.deepEqual(request.getOriginalRequestHeaders(), { 82 | [CLIENTTYPE_HEADER_KEY]: 'CMS', 83 | [USERID_HEADER_KEY]: 'userid', 84 | [USER_PROPERTIES_HEADER_KEY]: JSON.stringify({ prop1: 'value1' }), 85 | [GROUPS_HEADER_KEY]: 'group-to-greet,group', 86 | [BACKOFFICE_HEADER_KEY]: '1', 87 | my: 'headers', 88 | }) 89 | this.assert.deepEqual(request.getOriginalRequestQuery(), { my: 'query' }) 90 | this.assert.equal(request.getOriginalRequestMethod(), 'GET') 91 | this.assert.equal(request.getOriginalRequestPath(), '/the-original-request-path') 92 | this.assert.ok(this.config) 93 | 94 | return request.leaveOriginalRequestUnmodified() 95 | } 96 | service.addPreDecorator('/can-access-data', accessDataHandler) 97 | 98 | service 99 | .addPreDecorator('/chained1', requestUnmodifiedHandler) 100 | .addPreDecorator('/chained2', requestUnmodifiedHandler) 101 | }) 102 | -------------------------------------------------------------------------------- /tests/services/service-with-formats.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | const customService = require('../../index')() 20 | 21 | const serviceOptions = { 22 | ajv: { 23 | plugins: { 24 | 'ajv-formats': { formats: ['date-time'] }, 25 | }, 26 | vocabulary: ['x-name'], 27 | }, 28 | } 29 | 30 | module.exports = customService(async function clientGroups(service) { 31 | function handler(request, reply) { 32 | reply.send({ message: `hello there, it is ${request.body.someDate}` }) 33 | } 34 | 35 | service.addRawCustomPlugin('POST', '/hello', handler, { 36 | body: { 37 | type: 'object', 38 | properties: { 39 | someDate: { 40 | type: 'string', 41 | 'x-name': 'My custom name in schema', 42 | format: 'date-time', 43 | description: 'some date description', 44 | }, 45 | }, 46 | }, 47 | }) 48 | }, serviceOptions) 49 | -------------------------------------------------------------------------------- /tests/types/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Mia srl 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | import * as stream from 'stream' 20 | 21 | import * as cpl from '../../index' 22 | 23 | cpl({ 24 | type: 'object', 25 | required: [ 'foo'], 26 | properties: { 27 | foo: { type: 'string' } 28 | } 29 | }) 30 | cpl() 31 | 32 | const a = cpl() 33 | const { getHttpClient } = cpl 34 | 35 | async function invokeSomeApisServiceProxy(service: cpl.Service) { 36 | service.get('/path') 37 | service.get('/path', { query: 'params' }) 38 | service.get('/path', { query: 'params' }, { returnAs: 'JSON' }) 39 | service.get('/path', { query: 'params' }, { returnAs: 'BUFFER' }) 40 | service.get('/path', { query: 'params' }, { returnAs: 'STREAM' }) 41 | 42 | service.post('/path', {my: 'body'}) 43 | service.post('/path', 'My body as string') 44 | service.post('/path', Buffer.from('My body as buffer')) 45 | service.post('/path', new stream.Readable()) 46 | service.post('/path', {my: 'body'}, { query: 'params' }) 47 | service.post('/path', {my: 'body'}, { query: 'params' }, { returnAs: 'STREAM' }) 48 | 49 | service.put('/path', {my: 'body'}) 50 | service.put('/path', 'My body as string') 51 | service.put('/path', Buffer.from('My body as buffer')) 52 | service.put('/path', new stream.Readable()) 53 | service.put('/path', {my: 'body'}, { query: 'params' }) 54 | service.put('/path', {my: 'body'}, { query: 'params' }, { returnAs: 'STREAM' }) 55 | 56 | service.patch('/path', {my: 'body'}) 57 | service.patch('/path', 'My body as string') 58 | service.patch('/path', Buffer.from('My body as buffer')) 59 | service.patch('/path', new stream.Readable()) 60 | service.patch('/path', {my: 'body'}, { query: 'params' }) 61 | service.patch('/path', {my: 'body'}, { query: 'params' }, { returnAs: 'STREAM' }) 62 | 63 | service.delete('/path', {my: 'body'}) 64 | service.delete('/path', 'My body as string') 65 | service.delete('/path', Buffer.from('My body as buffer')) 66 | service.delete('/path', new stream.Readable()) 67 | service.delete('/path', {my: 'body'}, { query: 'params' }) 68 | service.delete('/path', {my: 'body'}, { query: 'params' }, { returnAs: 'STREAM' }) 69 | 70 | const response = await service.get('/path') as cpl.JSONServiceResponse 71 | console.log(response.statusCode) 72 | console.log(response.headers) 73 | console.log(response.payload) 74 | 75 | const responseAsBuffer = await service.get('/path') as cpl.BufferServiceResponse 76 | console.log(responseAsBuffer.statusCode) 77 | console.log(responseAsBuffer.headers) 78 | console.log(responseAsBuffer.payload) 79 | 80 | const responseAsStream = await service.get('/path', {}, { returnAs: 'STREAM' }) as cpl.StreamedServiceResponse 81 | let d: string = '' 82 | responseAsStream.on('data', data => { d += data.toString() }) 83 | responseAsStream.on('end', () => console.log(d)) 84 | } 85 | 86 | async function invokeSomeApis(service: cpl.HttpClient) { 87 | service.get('/path') 88 | service.get('/path', { query: { query: 'params'} }) 89 | service.get('/path', { returnAs: 'JSON', query: { query: 'params' } }) 90 | service.get('/path', { returnAs: 'BUFFER', query: { query: 'params' } }) 91 | service.get('/path', { returnAs: 'STREAM', query: { query: 'params' } }) 92 | 93 | service.post('/path', {my: 'body'}) 94 | service.post('/path', 'My body as string') 95 | service.post('/path', Buffer.from('My body as buffer')) 96 | service.post('/path', new stream.Readable()) 97 | service.post('/path', {my: 'body'}, { query: { query: 'params'} }) 98 | service.post('/path', {my: 'body'}, { returnAs: 'STREAM', query: { query: 'params'} }) 99 | 100 | service.put('/path', {my: 'body'}) 101 | service.put('/path', 'My body as string') 102 | service.put('/path', Buffer.from('My body as buffer')) 103 | service.put('/path', new stream.Readable()) 104 | service.put('/path', {my: 'body'}, { query: { query: 'params'} }) 105 | service.put('/path', {my: 'body'}, { returnAs: 'STREAM', query: { query: 'params'} }) 106 | 107 | service.patch('/path', {my: 'body'}) 108 | service.patch('/path', 'My body as string') 109 | service.patch('/path', Buffer.from('My body as buffer')) 110 | service.patch('/path', new stream.Readable()) 111 | service.patch('/path', {my: 'body'}, {query: { query: 'params'}}) 112 | service.patch('/path', {my: 'body'}, { returnAs: 'STREAM', query: { query: 'params'} }) 113 | 114 | service.delete('/path', {my: 'body'}) 115 | service.delete('/path', 'My body as string') 116 | service.delete('/path', Buffer.from('My body as buffer')) 117 | service.delete('/path', new stream.Readable()) 118 | service.delete('/path', {my: 'body'}, { query: { query: 'params'} }) 119 | service.delete('/path', {my: 'body'}, { returnAs: 'STREAM', query: { query: 'params'} }) 120 | 121 | interface Res { 122 | foo: string 123 | } 124 | const response = await service.get>('/path') 125 | console.log(response.statusCode) 126 | console.log(response.headers) 127 | console.log(response.payload.foo) 128 | 129 | const responseAsBuffer = await service.get('/path') 130 | console.log(responseAsBuffer.statusCode) 131 | console.log(responseAsBuffer.headers) 132 | console.log(responseAsBuffer.payload) 133 | 134 | const responseAsStream = await service.get('/path', { returnAs: 'STREAM' }) 135 | let d: string = '' 136 | responseAsStream.headers 137 | responseAsStream.payload.on('data', data => { d += data.toString() }) 138 | responseAsStream.payload.on('end', () => console.log(d)) 139 | } 140 | 141 | a(async function (service) { 142 | 143 | console.log(service.config) 144 | 145 | service.addRawCustomPlugin('POST', '/path-attach-validation', function handlerPath1(request, reply) { 146 | reply.send({ hi: 'hi' }) 147 | }, { 148 | body: { 149 | type: 'object', 150 | additionalProperties: false, 151 | required: ['foo'], 152 | properties: { 153 | foo: { type: 'number' }, 154 | }, 155 | } 156 | }, { attachValidation: true }) 157 | 158 | type RequestGeneric = { 159 | Body: {body: string} 160 | Querystring: {querystring: string} 161 | Params: {params: string} 162 | Headers: {headers: string} 163 | } 164 | 165 | const httpClientFromService = service.getHttpClient('http://service-name') 166 | await invokeSomeApis(httpClientFromService) 167 | 168 | service 169 | .addRawCustomPlugin('GET', '/path1', function handlerPath1(request, reply) { 170 | console.log(this.config) 171 | if (request.getUserId() === null) { 172 | reply.send({}) 173 | return 174 | } 175 | reply.send({ hi: request.getUserId() }) 176 | }) 177 | .addRawCustomPlugin('GET', '/', async function handler(request, reply) { 178 | console.log(this.config) 179 | 180 | const userId: string | null = request.getUserId() 181 | const groups: string[] = request.getGroups() 182 | const clientType: string | null = request.getClientType() 183 | const isFromBackOffice: boolean = request.isFromBackOffice() 184 | 185 | await invokeProxies() 186 | 187 | return { 'aa': 'boo' } 188 | }, { 189 | headers: { 190 | type:'object' 191 | } 192 | }) 193 | .addRawCustomPlugin('POST', '/', async function handler(request, reply) { 194 | console.log(this.config) 195 | 196 | const userId: string | null = request.getUserId() 197 | const groups: string[] = request.getGroups() 198 | const clientType: string | null = request.getClientType() 199 | const isFromBackOffice: boolean = request.isFromBackOffice() 200 | 201 | const httpClient = request.getHttpClient('http://my-service') 202 | await invokeSomeApis(httpClient) 203 | 204 | const httpClientWithOptions = request.getHttpClient('http://my-service', { 205 | headers: {'accept': 'application/json'}, 206 | }) 207 | await invokeSomeApis(httpClientWithOptions) 208 | 209 | const httpClientFromService = service.getHttpClient('http://service-name') 210 | await invokeSomeApis(httpClientFromService) 211 | 212 | return { 'aa': 'boo' } 213 | }, { 214 | headers: { 215 | type:'object' 216 | } 217 | }) 218 | .addRawCustomPlugin('GET', '/ts', async function handler(request, reply) { 219 | console.log(request.body.body) 220 | console.log(request.query.querystring) 221 | console.log(request.params.params) 222 | console.log(request.headers.headers) 223 | 224 | return { 'aa': 'boo' } 225 | }) 226 | 227 | service 228 | .addPreDecorator('/decorators/my-pre1', async function myHandlerPreDecorator(request, reply) { 229 | const originalRequest : cpl.OriginalRequest = request.getOriginalRequest() 230 | console.log(originalRequest) 231 | console.log(originalRequest.body) 232 | console.log(originalRequest.headers) 233 | console.log(originalRequest.method) 234 | console.log(originalRequest.path) 235 | console.log(originalRequest.query) 236 | console.log(request.getOriginalRequestBody()) 237 | console.log(request.getOriginalRequestHeaders()) 238 | console.log(request.getOriginalRequestQuery()) 239 | console.log(request.getOriginalRequestPath()) 240 | console.log(request.getOriginalRequestMethod()) 241 | 242 | console.log(request.getUserId()) 243 | console.log(request.getGroups()) 244 | console.log(request.getClientType()) 245 | console.log(request.isFromBackOffice()) 246 | console.log(request.getMiaHeaders()) 247 | 248 | return request.leaveOriginalRequestUnmodified() 249 | }) 250 | .addPreDecorator('/decorators/my-pre2', async function myHandlerPreDecorator(request, reply) { 251 | return request.changeOriginalRequest() 252 | .setBody({ new: 'body' }) 253 | .setQuery({ rewrite: 'the querystring completely' }) 254 | .setHeaders({ rewrite: 'the headers completely' }) 255 | }) 256 | service.addPreDecorator('/decorators/my-pre3', async function myHandlerPreDecorator(request, reply) { 257 | return request.abortChain(200, { final: 'body' }) 258 | }) 259 | service.addPreDecorator('/decorators/my-pre4', async function myHandlerPreDecorator(request, reply) { 260 | return request.abortChain(200, { final: 'body' }, { some: 'other headers' }) 261 | }) 262 | service.addPreDecorator('/decorators/my-pre5', async function myHandlerPreDecorator(request, response) { 263 | console.log(request.body.body) 264 | console.log(request.query.querystring) 265 | console.log(request.params.params) 266 | console.log(request.headers.headers) 267 | 268 | return { 'aa': 'boo' } 269 | }) 270 | 271 | service 272 | .addPostDecorator('/decorators/my-post1', async function myHandlerPostDecorator(request, reply) { 273 | const originalRequest : cpl.OriginalRequest = request.getOriginalRequest() 274 | console.log(originalRequest) 275 | console.log(originalRequest.body) 276 | console.log(originalRequest.headers) 277 | console.log(originalRequest.method) 278 | console.log(originalRequest.path) 279 | console.log(originalRequest.query) 280 | console.log(request.getOriginalRequestBody()) 281 | console.log(request.getOriginalRequestHeaders()) 282 | console.log(request.getOriginalRequestQuery()) 283 | console.log(request.getOriginalRequestPath()) 284 | console.log(request.getOriginalRequestMethod()) 285 | 286 | const originalResponse : cpl.OriginalResponse = request.getOriginalResponse() 287 | console.log(originalResponse) 288 | console.log(originalResponse.statusCode) 289 | console.log(originalResponse.headers) 290 | console.log(originalResponse.body) 291 | console.log(request.getOriginalResponseBody()) 292 | console.log(request.getOriginalResponseHeaders()) 293 | console.log(request.getOriginalResponseStatusCode()) 294 | 295 | console.log(request.getUserId()) 296 | console.log(request.getGroups()) 297 | console.log(request.getClientType()) 298 | console.log(request.isFromBackOffice()) 299 | console.log(request.getMiaHeaders()) 300 | 301 | return request.leaveOriginalResponseUnmodified() 302 | }) 303 | .addPostDecorator('/decorators/my-post2', async function myHandlerPostDecorator(request, reply) { 304 | return request.changeOriginalResponse() 305 | .setBody({ new: 'body' }) 306 | .setStatusCode(201) 307 | .setHeaders({ rewrite: 'the headers completely' }) 308 | }) 309 | service.addPostDecorator('/decorators/my-post3', async function myHandlerPostDecorator(request, reply) { 310 | return request.abortChain(200, { final: 'body' }) 311 | }) 312 | service.addPostDecorator('/decorators/my-post4', async function myHandlerPostDecorator(request, reply) { 313 | return request.abortChain(200, { final: 'body' }, { some: 'other headers' }) 314 | }) 315 | service.addPostDecorator('/decorators/my-post5', async function myHandlerPostDecorator(request, response) { 316 | console.log(request.body.body) 317 | console.log(request.query.querystring) 318 | console.log(request.params.params) 319 | console.log(request.headers.headers) 320 | 321 | return { 'aa': 'boo' } 322 | }) 323 | }) 324 | 325 | const b = cpl() 326 | b(async function (service) {}, { 327 | ajv: { 328 | plugins: {'ajv-formats': {formats: ['date-time']}} 329 | }, 330 | vocabulary: ['my-keyword'] 331 | }) 332 | 333 | async function invokeProxies() { 334 | const httpClient = getHttpClient('http://service_name') 335 | const httpClientWithOptions = getHttpClient('http://service_name:3000', { 336 | timeout: 1000, 337 | }) 338 | await httpClient.get('/path') 339 | await httpClientWithOptions.get('/path') 340 | } 341 | -------------------------------------------------------------------------------- /tests/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "noEmit": true, 6 | "strict": true 7 | }, 8 | "files": [ 9 | "./index.ts" 10 | ] 11 | } 12 | --------------------------------------------------------------------------------