├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── closeStale.yml │ ├── codeql-analysis.yml │ ├── nodejs-ci.yml │ ├── npm-publish.yml │ ├── update-license-year.sh │ └── update-license-year.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── UPGRADING.md ├── bin └── openapi-glue-cli.js ├── biome.json ├── docs ├── AJVstrictMode.md ├── bindings.md ├── cookieValidationHowTo.md ├── operationResolver.md ├── schema2020.md ├── securityHandlers.md ├── servers.md ├── serviceHandlers.md └── subSchemas.md ├── examples ├── generated-javascript-project │ ├── README.md │ ├── index.js │ ├── openApi.json │ ├── package.json │ ├── security.js │ ├── service.js │ └── test │ │ └── test-plugin.js ├── generated-standaloneJS-project │ ├── README.md │ ├── index.js │ ├── openApi.json │ ├── package.json │ ├── security.js │ ├── securityHandlers.js │ ├── service.js │ └── test │ │ └── test-plugin.js └── petstore │ ├── index.js │ ├── petstore-openapi.v3.json │ └── service.js ├── index.d.ts ├── index.js ├── lib ├── Parser.js ├── Parser.v2.js ├── Parser.v3.js ├── ParserBase.js ├── generator.js ├── securityHandlers.js └── templates │ ├── js │ ├── README.md.js │ ├── index.js │ ├── instructions.js │ ├── package.json │ ├── projectData.js │ ├── security.js │ ├── service.js │ └── test-plugin.js │ ├── standaloneJS │ ├── README.md.js │ ├── index.js │ ├── instructions.js │ ├── package.json │ ├── projectData.js │ ├── security.js │ ├── service.js │ └── test-plugin.js │ ├── templateTypes.js │ └── templateUtils.js ├── package-lock.json ├── package.json └── test ├── petstore-openapi.v3.json ├── petstore-openapi.v3.yaml ├── petstore-swagger.v2.json ├── security.js ├── service.js ├── service.multipleMimeTypes.js ├── test-cli.js ├── test-cookie-param.v3.js ├── test-custom-route-options.js ├── test-debuglogging.js ├── test-fastify-cli.js ├── test-fastify-recursive.js ├── test-fastify.js ├── test-generate-project.js ├── test-generator.js ├── test-import-plugin.cjs ├── test-import-plugin.js ├── test-openapi-v3-cookie-param.json ├── test-openapi-v3-generic-path-items.json ├── test-openapi-v3-recursive.json ├── test-openapi.v3.content.json ├── test-openapi.v3.json ├── test-openapi.v3.multipleMimeTypes.json ├── test-openapi.v3.yaml ├── test-parser-base.js ├── test-petstore-example.js ├── test-plugin.v2.js ├── test-plugin.v3.content.js ├── test-plugin.v3.js ├── test-plugin.v3.multipleMimeTypes.js ├── test-recursive.v3.js ├── test-securityHandlers.js ├── test-swagger-noBasePath.v2.javascript.checksums.json ├── test-swagger-noBasePath.v2.json ├── test-swagger-noBasePath.v2.standaloneJS.checksums.json ├── test-swagger-v2-generic-path-items.json ├── test-swagger.v2.javascript.checksums.json ├── test-swagger.v2.json ├── test-swagger.v2.standaloneJS.checksums.json ├── test-swagger.v2.yaml ├── test-warnings.js └── update-checksums.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every week 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/closeStale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | permissions: 7 | issues: write 8 | 9 | jobs: 10 | stale: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/stale@v9 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | days-before-stale: 30 17 | days-before-close: 5 18 | stale-issue-message: This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days' 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '32 22 * * 3' 22 | 23 | permissions: {} 24 | 25 | jobs: 26 | analyze: 27 | name: Analyze 28 | runs-on: ubuntu-latest 29 | permissions: 30 | actions: read 31 | contents: read 32 | security-events: write 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: [ 'javascript' ] 38 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 39 | # Learn more: 40 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v4 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v3 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v3 74 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-ci.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: [push, pull_request] 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [ 'lts/-1', 'lts/*', 'node'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci --ignore-scripts --no-audit --no-progress --prefer-offline 24 | # set exec bit on biome binary (because of --ignore-scripts above) 25 | - run: node ./node_modules/@biomejs/biome/scripts/postinstall.js 26 | - run: npm run lint 27 | - run: npm run build --if-present 28 | - run: npm run covtest 29 | # test typescript definitions 30 | - run: npm install @types/node 31 | - run: npx --package typescript tsc index.d.ts 32 | - name: Coveralls Parallel 33 | uses: coverallsapp/github-action@master 34 | with: 35 | github-token: ${{ secrets.github_token }} 36 | flag-name: run-${{ matrix.test_number }} 37 | parallel: true 38 | 39 | finish: 40 | needs: build 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Coveralls Finished 44 | uses: coverallsapp/github-action@master 45 | with: 46 | github-token: ${{ secrets.github_token }} 47 | parallel-finished: true 48 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | id-token: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 'lts/*' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm ci 18 | - run: npm publish --provenance --access public 19 | env: 20 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/update-license-year.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | license='LICENSE.txt' 4 | prefix='2016-' 5 | current_year=$(date +%Y) 6 | sed -i -e "s/$prefix\([0-9\]\+\)/$prefix$current_year/" $license 7 | 8 | if [ "$(git diff $license)" ]; then 9 | git add $license 10 | git commit -m "Update license year to $current_year" 11 | git push 12 | else 13 | echo "No changes detected in $license" 14 | fi -------------------------------------------------------------------------------- /.github/workflows/update-license-year.yml: -------------------------------------------------------------------------------- 1 | name: Update License Year 2 | 3 | on: 4 | schedule: 5 | - cron: '5 12 1 1 *' 6 | 7 | jobs: 8 | update_license: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Update license year 13 | run: ./update_license.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directories 28 | node_modules 29 | jspm_packages 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # ========================= 38 | # Operating System Files 39 | # ========================= 40 | 41 | # OSX 42 | # ========================= 43 | 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | 48 | # Thumbnails 49 | ._* 50 | 51 | # Files that might appear in the root of a volume 52 | .DocumentRevisions-V100 53 | .fseventsd 54 | .Spotlight-V100 55 | .TemporaryItems 56 | .Trashes 57 | .VolumeIcon.icns 58 | 59 | # Directories potentially created on remote AFP share 60 | .AppleDB 61 | .AppleDesktop 62 | Network Trash Folder 63 | Temporary Items 64 | .apdisk 65 | 66 | # Windows 67 | # ========================= 68 | 69 | # Windows image file caches 70 | Thumbs.db 71 | ehthumbs.db 72 | 73 | # Folder config file 74 | Desktop.ini 75 | 76 | # Recycle Bin used on file shares 77 | $RECYCLE.BIN/ 78 | 79 | # Windows Installer files 80 | *.cab 81 | *.msi 82 | *.msm 83 | *.msp 84 | 85 | # Windows shortcuts 86 | *.lnk 87 | 88 | # vscode config 89 | .vscode 90 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2025 Hans Klunder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fastify OpenApi Glue 2 | [![CI status](https://github.com/seriousme/fastify-openapi-glue/workflows/Node.js%20CI/badge.svg)](https://github.com/seriousme/fastify-openapi-glue/actions?query=workflow%3A%22Node.js+CI%22) 3 | [![Coverage Status](https://coveralls.io/repos/github/seriousme/fastify-openapi-glue/badge.svg?branch=master)](https://coveralls.io/github/seriousme/fastify-openapi-glue?branch=master) 4 | [![NPM version](https://img.shields.io/npm/v/fastify-openapi-glue.svg)](https://www.npmjs.com/package/fastify-openapi-glue) 5 | ![npm](https://img.shields.io/npm/dm/fastify-openapi-glue) 6 | 7 | 8 | 9 | A plugin for [fastify](https://fastify.dev) to autogenerate a configuration based on a [OpenApi](https://www.openapis.org/)(v2/v3) specification. 10 | 11 | It aims at facilitating ["design first" API development](https://swagger.io/blog/api-design/design-first-or-code-first-api-development/) i.e. you write or obtain an API specification and use that to generate code. Given an OpenApi specification Fastify-openapi-glue handles the fastify configuration of routes and validation schemas etc. You can also [generate](#generator) your own project from a OpenApi specification. 12 | 13 | 14 | ## Upgrading 15 | 16 | If you are upgrading from a previous major version of `fastify-openapi-glue` then please check out [UPGRADING.md](UPGRADING.md). 17 | 18 | 19 | ## Install 20 | ``` 21 | npm i fastify-openapi-glue --save 22 | ``` 23 | 24 | ## Plugin 25 | 26 | ### Usage 27 | 28 | Add the plugin to your project with `register` and pass it some basic options and you are done ! 29 | ```javascript 30 | import openapiGlue from "fastify-openapi-glue"; 31 | import { Service } from "./service.js"; 32 | import { Security } from "./security.js"; 33 | 34 | const options = { 35 | specification: `${currentDir}/petstore-openapi.v3.json`, 36 | serviceHandlers: new Service(), 37 | securityHandlers: new Security(), 38 | prefix: "v1", 39 | }; 40 | 41 | 42 | fastify.register(openapiGlue, options); 43 | ``` 44 | 45 | All schema and routes will be taken from the OpenApi specification listed in the options. No need to specify them in your code. 46 | 47 | ### Options 48 | - `specification`: this can be a JSON object, or the name of a JSON or YAML file containing a valid OpenApi(v2/v3) file 49 | - `serviceHandlers`: this can be a javascript object or class instance. See the [serviceHandlers documentation](docs/serviceHandlers.md) for more details. 50 | - `securityHandlers`: this can be a javascript object or class instance. See the [securityHandlers documentation](docs/securityHandlers.md) for more details. 51 | - `prefix`: this is a string that can be used to prefix the routes, it is passed verbatim to fastify. E.g. if the path to your operation is specified as "/operation" then a prefix of "v1" will make it available at "/v1/operation". This setting overrules any "basePath" setting in a v2 specification. See the [servers documentation](docs/servers.md) for more details on using prefix with a v3 specification. 52 | - `operationResolver`: a custom operation resolver function, `(operationId, method, openapiPath) => handler | routeOptions` where method is the uppercase HTTP method (e.g. "GET") and openapiPath is the path taken from the specification without prefix (e.g. "/operation"). Mutually exclusive with `serviceHandlers`. See the [operationResolver documentation](docs/operationResolver.md) for more details. 53 | - `addEmptySchema`: a boolean that allows empty bodies schemas to be passed through. This might be useful for status codes like 204 or 304. Default is `false`. 54 | 55 | `specification` and either `serviceHandlers` or `operationResolver` are mandatory, `securityHandlers` and `prefix` are optional. 56 | See the [examples](#examples) section for a demo. 57 | 58 | Please be aware that `this` will refer to your serviceHandlers object or your securityHandler object and not to Fastify as explained in the [bindings documentation](docs/bindings.md) 59 | 60 | 61 | ### OpenAPI extensions 62 | The OpenAPI specification supports [extending an API spec](https://spec.openapis.org/oas/latest.html#specification-extensions) to describe extra functionality that isn't covered by the official specification. Extensions start with `x-` (e.g., `x-myapp-logo`) and can contain a primitive, an array, an object, or `null`. 63 | 64 | The following extensions are provided by the plugin: 65 | - `x-fastify-config` (object): any properties will be added to the `routeOptions.config` property of the Fastify route. 66 | 67 | For example, if you wanted to use the fastify-raw-body plugin to compute a checksum of the request body, you could add the following extension to your OpenAPI spec to signal the plugin to specially handle this route: 68 | 69 | ```yaml 70 | paths: 71 | /webhooks: 72 | post: 73 | operationId: processWebhook 74 | x-fastify-config: 75 | rawBody: true 76 | responses: 77 | 204: 78 | description: Webhook processed successfully 79 | ``` 80 | 81 | - `x-no-fastify-config` (true): this will ignore this specific route as if it was not present in your OpenAPI specification: 82 | 83 | ```yaml 84 | paths: 85 | /webhooks: 86 | post: 87 | operationId: processWebhook 88 | x-no-fastify-config: true 89 | responses: 90 | 204: 91 | description: Webhook processed successfully 92 | ``` 93 | 94 | You can also set custom OpenAPI extensions (e.g., `x-myapp-foo`) for use within your app's implementation. These properties are passed through unmodified to the Fastify route on `{req,reply}.routeOptions.config`. Extensions specified on a schema are also accessible (e.g., `routeOptions.schema.body` or `routeOptions.schema.responses[]`). 95 | 96 | 97 | ## Generator 98 | 99 | To make life even more easy there is the `openapi-glue` cli. The `openapi-glue` cli takes a valid OpenApi (v2/v3) file (JSON or YAML) and generates a project including a fastify plugin that you can use on any fastify server, a stub of the serviceHandlers class and a skeleton of a test harness to test the plugin. 100 | 101 | 102 | ### Usage 103 | ``` 104 | openapi-glue [options] 105 | ``` 106 | or if you don't have `openapi-glue` installed: 107 | ``` 108 | npx github:seriousme/fastify-openapi-glue 109 | ``` 110 | This will generate a project based on the provided OpenApi specification. 111 | Any existing files in the project folder will be overwritten! 112 | See the [generator examples](#examples) section for a demo. 113 | 114 | ### Options 115 | ``` 116 | 117 | -p The name of the project to generate 118 | --projectName= [default: generated-javascript-project] 119 | 120 | -b --baseDir= Directory to generate the project in. 121 | This directory must already exist. 122 | [default: "."] 123 | 124 | The following options are only usefull for testing the openapi-glue plugin: 125 | -c --checksumOnly Don't generate the project on disk but 126 | return checksums only. 127 | -l --localPlugin Use a local path to the plugin. 128 | ``` 129 | See the [generator example](#generatorExamples) section for a demo. 130 | 131 | 132 | 133 | ## Examples 134 | Clone this repository and run `npm i` 135 | 136 | 137 | ### Plugin 138 | Executing `npm start` will start fastify on localhost port 3000 with the 139 | routes extracted from the [petstore example](examples/petstore/petstore-openapi.v3.json) and the [accompanying serviceHandlers definition](examples/petstore/service.js) 140 | 141 | * http://localhost:3000/v2/pet/24 will return a pet as specified in service.js 142 | * http://localhost:3000/v2/pet/myPet will return a fastify validation error: 143 | 144 | ```json 145 | { 146 | "statusCode": 400, 147 | "error": "Bad Request", 148 | "message": "params.petId should be integer" 149 | } 150 | ``` 151 | 152 | * http://localhost:3000/v2/pet/findByStatus?status=available&status=pending will return 153 | the following error: 154 | 155 | ```json 156 | { 157 | "statusCode": 500, 158 | "error": "Internal Server Error", 159 | "message": "Operation findPetsByStatus not implemented" 160 | } 161 | ``` 162 | 163 | * http://localhost:3000/v2/pet/0 will return the following error: 164 | 165 | ```json 166 | { 167 | "statusCode": 500, 168 | "error": "Internal Server Error", 169 | "message":"\"name\" is required!" 170 | } 171 | ``` 172 | 173 | as the pet returned by service.js does not match the response schema. 174 | 175 | 176 | ### Generator 177 | The folder [examples/generated-javascript-project](examples/generated-javascript-project) contains the result of running `openapi-glue -l --baseDir=examples examples/petstore/petstore-swagger.v2.yaml`. The generated code can be started using `npm start` in `examples/generated-javascript-project` (you will need to run `npm i` in the generated folder first) 178 | 179 | ## Notes 180 | - the plugin ignores information in a v3 specification under `server/url` as there could be multiple values here, use the `prefix` [option](#pluginOptions) if you need to prefix your routes. See the [servers documentation](docs/servers.md) for more details. 181 | - fastify only supports `application/json` and `text/plain` out of the box. The default charset is `utf-8`. If you need to support different content types, you can use the fastify `addContentTypeParser` API. 182 | - fastify will by default coerce types, e.g when you expect a number a string like `"1"` will also pass validation, this can be reconfigured, see [Validation and Serialization](https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/). 183 | - the plugin aims to follow fastify and does not compensate for features that are possible according to the OpenApi specification but not possible in standard fastify (without plugins). This will keep the plugin lightweigth and maintainable. E.g. Fastify does not support cookie validation, while OpenApi v3 does. 184 | - in some cases however, the plugin may be able to provide you with data which could be used to enhance OpenApi support within your own Fastify application. Here is one possible way to perform [cookie validation](docs/cookieValidationHowTo.md) yourself. 185 | - if you have special needs on querystring handling (e.g. arrays, objects etc) then fastify supports a [custom querystring parser](https://fastify.dev/docs/latest/Reference/Server/#querystringparser). You might need to pass the AJV option `coerceTypes: 'array'` as an option to Fastify. 186 | - the plugin is an ECMAscript Module (aka ESM). If you are using Typescript then make sure that you have read: https://www.typescriptlang.org/docs/handbook/esm-node.html to avoid any confusion. 187 | - If you want to use a specification that consists of multiple files then please check out the page on [subschemas](docs/subSchemas.md) 188 | - Fastify uses AJV strict mode in validating schemas. If you get an error like `....due to error strict mode: unknown keyword: "..."` then please check out the page on [AJV strict mode](docs/AJVstrictMode.md) 189 | - Fastify does not support `multipart/form-data` out of the box. If you want to use it then have a look at: [@fastify/multipart](https://github.com/fastify/fastify-multipart). 190 | - Fastify uses AJV with JSON schema draft-07 out of the box. If you want to use JSON schema draft-2020-12 features in your OpenApi 3.1+ schema then have a look at [using JSONschema 2020](docs/schema2020.md) 191 | 192 | 193 | ## Contributing 194 | - contributions are always welcome. 195 | - if you plan on submitting new features then please create an issue first to discuss and avoid disappointments. 196 | - main development is done on the master branch therefore PRs to that branch are preferred. 197 | - please make sure you have run `npm test` before you submit a PR. 198 | 199 | ## Fastify-swaggergen 200 | Fastify-openapi-glue is the successor to the now deprecated [fastify-swaggergen](https://github.com/seriousme/fastify-swaggergen) project. 201 | Main difference is that it: 202 | - aims to support OpenApi and not just Swagger V2 (hence the name change) 203 | - does not include fastify-swagger support anymore. If you need to show the swagger UI you can include it yourself. Removing the swagger UI clears up a number of dependencies. 204 | 205 | # License 206 | Licensed under [MIT](LICENSE.txt) 207 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading Fastify OpenApi Glue 2 | 3 | ## From 3.x to 4.x 4 | 5 | This is a major change with the following breaking change: 6 | 7 | ### Empty security handlers 8 | 9 | If your OpenAPI 3.x specification contains: 10 | 11 | ```yaml 12 | ... 13 | security: 14 | - ApiKeyAuth: [] 15 | - OAuth2: 16 | - read 17 | - write 18 | paths: 19 | /ping: 20 | get: 21 | security: [] # No security 22 | ``` 23 | The route should be accessible publicly. However in 3.x the route would still (incorrectly) be blocked due to the definitions at top level. As this update changes the security behaviour of the plugin and could accidentally expose routes that were (incorrectly) tested as private in 3.x this update is labeled as semver major. 24 | If you have such a route with empty handlers and it needs protection either add a security scheme to the array or remove the security property altogether. 25 | 26 | ## From 2.x to 3.x 27 | 28 | This is a major change with the following breaking changes: 29 | 30 | ### ESM module 31 | 32 | Version 3.x is an ESM module. If your code contains: 33 | 34 | ```javascript 35 | const openapiGlue = require("fastify-openapi-glue"); 36 | ``` 37 | 38 | You now need to use: 39 | 40 | ```javascript 41 | const openapiGlue = await import("fastify-openapi-glue"); 42 | ``` 43 | 44 | ### AJV 45 | Version 3.x fully relies on AJV instance provided by Fastify. 46 | So if you want to change AJV's behaviour you need to add your configuration to Fastify instead of passing it to `Fastify OpenApi Glue`. The options `noAdditional`, `ajvOptions` and `defaultAJV` have been deprecated. The new behaviour is identical to `defaultAJV:true` in 2.x. 47 | 48 | E.g. if you had: 49 | 50 | ```javascript 51 | import openapiGlue from "fastify-openapi-glue"; 52 | import Service from "./service.js"; 53 | import Security from "./security.js"; 54 | 55 | const options = { 56 | specification: `${currentDir}/petstore-openapi.v3.json`, 57 | service: new Service(), 58 | securityHandlers: new Security(), 59 | prefix: "v1", 60 | noAdditional: true, 61 | ajvOptions: { 62 | formats: { 63 | "custom-format": /\d{2}-\d{4}/ 64 | } 65 | } 66 | }; 67 | 68 | fastify.register(openapiGlue, options); 69 | 70 | ``` 71 | 72 | You now need to pass the AJV options to Fastify at startup. (see: https://fastify.dev/docs/latest/Reference/Server/#ajv) or add them to your own AJV instance (see: https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/#schema-validator). The `noAdditional:true` flag should be mapped to AJV's `removeAdditional:false` option (see: https://ajv.js.org/options.html#removeadditional) 73 | 74 | ### service and securityhandlers options 75 | 76 | In version 2.x you could either pass: 77 | - an object or a class instance 78 | - a name (to be interpreted as file name) 79 | - a function (that would be executed) 80 | 81 | as values to these parameters. 82 | In 3.x the `name` and `function` values have been removed. So you need to `import` or `require` code files yourself and if you passed a function that resulted in an object or class instance you now need to call that function yourself. 83 | 84 | 85 | ## From 1.x to 2.x 86 | Just make sure that you use Fastify > 3.0.0 as 1.x is only compatible with Fastify 2.x. 87 | 88 | -------------------------------------------------------------------------------- /bin/openapi-glue-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { basename, resolve } from "node:path"; 4 | import { exit } from "node:process"; 5 | import { fileURLToPath } from "node:url"; 6 | import { parseArgs } from "node:util"; 7 | import { Generator } from "../lib/generator.js"; 8 | const __filename = fileURLToPath(import.meta.url); 9 | 10 | const validTypes = new Set(["javascript", "standaloneJS"]); 11 | 12 | function usage() { 13 | console.log(` 14 | Usage: 15 | ${basename(__filename)} [options] 16 | 17 | Generate a project based on the provided openapi specification. 18 | Any existing files in the project folder will be overwritten! 19 | 20 | Options: 21 | 22 | -p The name of the project to generate 23 | --projectName= [default: ${process.cwd()}] 24 | 25 | -b --baseDir= Directory to generate the project in. 26 | This directory must already exist. 27 | [default: "."] 28 | 29 | -t --type= Type of project to generate, possible options: 30 | javascript (default) 31 | standaloneJS 32 | 33 | The following options are only usefull for testing the openapi-glue plugin: 34 | -c --checksumOnly Don't generate the project on disk but 35 | return checksums only. 36 | -l --localPlugin Use a local path to the plugin. 37 | 38 | `); 39 | exit(1); 40 | } 41 | 42 | const options = { 43 | baseDir: { 44 | type: "string", 45 | short: "b", 46 | default: process.cwd(), 47 | }, 48 | projectName: { 49 | type: "string", 50 | short: "p", 51 | }, 52 | type: { 53 | type: "string", 54 | short: "t", 55 | default: "javascript", 56 | }, 57 | checksumOnly: { 58 | type: "boolean", 59 | short: "c", 60 | default: false, 61 | }, 62 | localPlugin: { 63 | type: "boolean", 64 | short: "l", 65 | default: false, 66 | }, 67 | }; 68 | 69 | const { values: argv, positionals } = parseArgs({ 70 | options, 71 | allowPositionals: true, 72 | }); 73 | 74 | argv.specification = positionals[0]; 75 | 76 | if (!argv.specification) { 77 | usage(); 78 | } 79 | 80 | if (!validTypes.has(argv.type)) { 81 | console.log(`Unknown type: ${argv.type}`); 82 | usage(); 83 | } 84 | 85 | const projectName = argv.projectName || `generated-${argv.type}-project`; 86 | const specPath = resolve(process.cwd(), argv.specification); 87 | const generator = new Generator(argv.checksumOnly, argv.localPlugin); 88 | const handler = (str) => 89 | /* c8 ignore next */ 90 | argv.checksumOnly ? JSON.stringify(str, null, "\t") : str; 91 | if (generator.localPlugin) { 92 | console.log(`Using local plugin at: ${generator.localPlugin} 93 | `); 94 | } 95 | 96 | try { 97 | await generator.parse(specPath); 98 | console.log( 99 | handler( 100 | await generator.generateProject(argv.baseDir, projectName, argv.type), 101 | ), 102 | ); 103 | } catch (e) { 104 | console.log(e.message); 105 | exit(1); 106 | } 107 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "ignore": ["coverage", "package.json"] 4 | }, 5 | "linter": { 6 | "enabled": true, 7 | "rules": { 8 | "recommended": true, 9 | "complexity": { "useLiteralKeys": "off" } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/AJVstrictMode.md: -------------------------------------------------------------------------------- 1 |

fastify-openapi-glue

2 | 3 | ## AJV strict mode 4 | 5 | Fastify uses [AJV](https://ajv.js.org/) to check JSONschemas that are used for 6 | [validation and serialization](https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/). 7 | 8 | The [OpenApi](https://www.openapis.org/) specification allows for additions to 9 | the schema specification that do not match JSONschema , e.g: 10 | 11 | ```json 12 | "responses": { 13 | "200": { 14 | "description": "Did something", 15 | "schema": { 16 | "type": "string", 17 | "example": "Done" 18 | } 19 | }, 20 | "404": { 21 | "description": "Didnt do something", 22 | "schema": { 23 | "type": "string", 24 | "example": "Failed" 25 | } 26 | } 27 | } 28 | ``` 29 | 30 | In this case the `example` attribute is not part of JSONschema and Fastify 31 | (actually AJV) will throw an error along the lines of: 32 | 33 | ``` 34 | Error while starting the application: Error: Could not initiate module due to error: FastifyError: Failed building the validation schema for POST: /api/v1/user/{userId}/something, due to error strict mode: unknown keyword: "example" 35 | ``` 36 | 37 | This error can be prevented by telling Fastify to use AJV in non-strict mode. 38 | This does not change the actual validation but it does allow for extra 39 | attributes in the schema. See: https://ajv.js.org/options.html#strict for more details. 40 | 41 | There are 2 ways to achieve this: 42 | 43 | ### Fastify start 44 | 45 | If your code is a Fastify plug-in then adding: 46 | 47 | ```javascript 48 | export const options = { 49 | ajv: { 50 | customOptions: { 51 | strict: false, 52 | }, 53 | }, 54 | }; 55 | ``` 56 | 57 | Will tell fastify start to apply this option. See: 58 | [examples/petstore/index.js](../examples/petstore/index.js) 59 | 60 | Make sure that you start your plugin using `--options`, e.g.: `fastify start --options `, or else the options won't be loaded. 61 | 62 | See also: also https://github.com/fastify/fastify-cli/#options. 63 | 64 | ### Fastify factory 65 | 66 | If you are setting up fastify in your own code then you can pass the non-strict 67 | option to AJV: 68 | 69 | ```javascript 70 | import Fastify from "fastify"; 71 | const fastify = Fastify( 72 | { 73 | ajv: { 74 | customOptions: { 75 | strict: false, 76 | }, 77 | }, 78 | }, 79 | ); 80 | ``` 81 | 82 | See also: https://fastify.dev/docs/latest/Reference/Server#factory-ajv 83 | -------------------------------------------------------------------------------- /docs/bindings.md: -------------------------------------------------------------------------------- 1 |

fastify-openapi-glue

2 | 3 | ## Bindings 4 | When creating your service and/or your security handlers please beware of the bindings. 5 | That means that when your service is called, `this` refers to the service object and not to Fastify ! 6 | Similarly when a security handler is called, `this` refers to the securityHandlers object and not to Fastify ! 7 | 8 | The rationale behind this is that any service integrations (e.g. database) should be handled by your service and not by Fastify. This enables you to test your service integration independently of Fastify. 9 | 10 | ### Workaround 11 | If you ever have a need to have a reference to the Fastify instance you can pass the instance to your constructor. 12 | 13 | E.g. something like: 14 | 15 | ```javascript 16 | import Fastify from "fastify"; 17 | import openapiGlue from "fastify-openapi-glue"; 18 | import Service from "./service.js"; 19 | 20 | const fastify = new Fastify(); 21 | const service = new Service(fastify); 22 | 23 | const specification = `myServiceSpec.json`; 24 | 25 | fastify.register( 26 | openapiGlue, 27 | { 28 | specification, 29 | service 30 | } 31 | ) 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/cookieValidationHowTo.md: -------------------------------------------------------------------------------- 1 | ### Implementing cookie validation 2 | 3 | The [OpenApi](https://www.openapis.org/) specification allows cookie validation, but Fastify itself does not validate or even parse cookies. 4 | 5 | The `fastify-openapi-glue` plugin is intentionally designed to work without requiring additional 3rd party plugins. 6 | However, it does provide a boolean option `addCookieSchema` which tells it to insert JSON Schema describing OpenApi cookies into the Fastify [Routes options](https://fastify.dev/docs/latest/Reference/Routes/#routes-options). 7 | 8 | Using this `addCookieSchema` option, one possible way to implement cookie validation in your application might be: 9 | - Register a plugin for cookie parsing with Fastify (perhaps [fastify cookie plugin](https://github.com/fastify/fastify-cookie)). 10 | - Listen for Fastify's [`onRoute` Application Hook](https://fastify.dev/docs/latest/Reference/Hooks/#onroute). 11 | - In your `onRoute` handler: 12 | - Check to see if `fastify-openapi-glue` found cookie specifications that it added to the `routeOptions`. 13 | - If cookie schema is present, pre-compile it with Ajv and add the compiled schema to the `routeOptions.config` object. 14 | - Register a global Fastify [`preHandler`](https://fastify.dev/docs/latest/Reference/Hooks/#prehandler) 15 | - In your global `preHandler`: 16 | - See if the invoked route has a cookie validator (pre-compiled by your `onRoute` handler). 17 | - Validate the cookie (which your cookie parser should have already added to the `request`). 18 | - With your customizations in place, register `fastify-openapi-glue`. 19 | 20 | Example: 21 | ```javascript 22 | // Register a plugin for cookie parsing 23 | fastify.register(cookie); 24 | 25 | // Hook into the route registration process to compile cookie schemas 26 | fastify.addHook('onRoute', (routeOptions) => { 27 | const schema = routeOptions.schema; 28 | /* 29 | * schema.cookies will be added to the schema object if the 30 | * 'addCookieSchema' option is passed to fastify-openapi-glue. 31 | */ 32 | if (schema?.cookies) { 33 | // Compile the cookie schema and store it in the route's context 34 | routeOptions.config = routeOptions.config || {}; 35 | routeOptions.config.cookieValidator = ajv.compile(schema.cookies); 36 | } 37 | }); 38 | 39 | // Pre-handler hook to validate cookies using the precompiled schema 40 | fastify.addHook('preHandler', async (request, reply) => { 41 | // See if this route has been configured with a cookie validator. 42 | const cookieValidator = request.routeOptions.config?.cookieValidator; 43 | if (cookieValidator) { 44 | const valid = cookieValidator(request.cookies); 45 | if (!valid) { 46 | reply.status(400).send({error: 'Invalid cookies', details: cookieValidator.errors}); 47 | throw new Error('Invalid cookies'); 48 | } 49 | } 50 | }); 51 | 52 | // Magic! 53 | fastify.register(openapiGlue, options); 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/operationResolver.md: -------------------------------------------------------------------------------- 1 |

fastify-openapi-glue

2 | 3 | ## operationResolver 4 | The easiest way to use `fastify-openapi-glue` is to use the [serviceHandlers](serviceHandlers.md) option. 5 | 6 | However if you need more flexibility in mapping operationId's to methods then the `operationResolver` can be convenient. 7 | 8 | You provide a function and that function returns the method that will handle the request. 9 | 10 | An example of a simple resolver is: 11 | ```javascript 12 | const myObject = { getPetbyId: async () => { pet: "Doggie the dog" }}; 13 | 14 | function (operationId) { 15 | if (operationId in myObject) { 16 | return myObject[operationId]; 17 | } 18 | }; 19 | ``` 20 | But you can make the logic as complex as you like. 21 | 22 | In this example `myObject[operationId]` points to a function, but you can also point it to a full Fastify route definition, e.g.: 23 | ```javascript 24 | const myObject = { getPetbyId: { 25 | onSend: async (req, res) => { 26 | res.code(304); 27 | return null; 28 | }, 29 | handler: async () => { 30 | return { pet: "Doggie the dog" }; 31 | }, 32 | }; 33 | } 34 | ``` 35 | In that case any properties returned by the operationResolver are added to the route configuration that the plugin already created itself, overriding any already existing properties. 36 | 37 | The operationResolver will only be called during the creation of the Fastify configuration. -------------------------------------------------------------------------------- /docs/schema2020.md: -------------------------------------------------------------------------------- 1 |

fastify-openapi-glue

2 | 3 | ## OpenApi 3.1 and JSON schema draft 2020-12 4 | 5 | Fastify uses [AJV](https://ajv.js.org/) to check JSON schemas that are used for 6 | [validation and serialization](https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/). 7 | 8 | At the time of writing (April 2025) AJV uses [JSON schema draft-07 as default version of JSONschema](https://ajv.js.org/json-schema.html#json-schema-versions). 9 | 10 | The OpenApi specifications until 3.1 (e.g. [3.0.4](https://spec.openapis.org/oas/v3.0.4.html#schema-object)) specify JSON schema draft-05, this works fine with AJV's draft-07. 11 | 12 | The OpenApi specifications from 3.1 (e.g. [3.1.0](https://spec.openapis.org/oas/v3.1.0.html#schema-object)) specify JSON schema draft-2020-12 which works fine with AJV's draft-07 most of the time but has some new additions like `unevaluatedProperties` and even some breaking changes, see https://ajv.js.org/json-schema.html#json-schema-versions. 13 | 14 | If you have/need these new additions in your OpenApi specification you need to tell `Fastify` to use `AJV` with `2020` schema. 15 | 16 | e.g. something like: 17 | ```js 18 | import openapiGlue from "fastify-openapi-glue"; 19 | import { Service } from "./service.js"; 20 | import { Security } from "./security.js"; 21 | import { Ajv2020 } from "ajv/dist/2020.js"; 22 | 23 | const ajv = new Ajv2020({ 24 | removeAdditional: 'all', 25 | useDefaults: true, 26 | coerceTypes: 'array', 27 | strict: true 28 | }); 29 | 30 | const opts = { 31 | specification: `${currentDir}/petstore-openapi.v3.json`, 32 | serviceHandlers: new Service(), 33 | securityHandlers: new Security(), 34 | prefix: "v1", 35 | }; 36 | 37 | const fastify = Fastify(); 38 | fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => { 39 | return ajv.compile(schema) 40 | }) 41 | 42 | fastify.register(fastifyOpenapiGlue, opts); 43 | ``` 44 | 45 | See also: https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/#validation 46 | -------------------------------------------------------------------------------- /docs/securityHandlers.md: -------------------------------------------------------------------------------- 1 |

fastify-openapi-glue

2 | 3 | ## SecurityHandlers 4 | The [OpenApi](https://www.openapis.org/) `Operation` object allows for security requirements that specify which security scheme(s) should be applied to this operation. OpenApi also allows for global security requirements via the `/components/security` property (v3) or the `/security` property (v2). 5 | 6 | You can specify your own securityHandlers with the `securityHandlers` option. 7 | If the provided object has an async `initialize` function then fastify-openapi-glue will call this function with the `/components/securitySchemes` property (v3) or the `/securityDefinitions` property (v2) of the specification provided. 8 | 9 | ### Example 10 | 11 | In the petstore example specification there is a section: 12 | ```json 13 | "/pet": { 14 | "post": { 15 | ... 16 | "summary": "Add a new pet to the store", 17 | "description": "", 18 | "operationId": "addPet", 19 | ... 20 | "security": [ 21 | { 22 | "petstore_auth": [ 23 | "write:pets", 24 | "read:pets" 25 | ] 26 | } 27 | ], 28 | ``` 29 | 30 | If you provide a securityHandler called `petstore_auth` then it will be called as `petstore_auth(request,reply, params)` where request and reply will be fastify request and reply objects and params contains `["write:pets", "read:pets"]`. 31 | 32 | If you want authentication to succeed you can simply return. If you want authentication to fail you can just throw an error. 33 | 34 | If your error contains a `statusCode` property then the status code of the last failing handler will be passed to fastify. The default status code that is returned upon validation failure is `401`. 35 | 36 | Any errors that result from `securityHandlers` are available to registered error handlers. E.g.: 37 | ```javascript 38 | fastify.setErrorHandler((err, req, reply) => { 39 | reply.code(err.statusCode).send(err); 40 | }); 41 | ``` 42 | will return errors originating from the securityHandlers as well in `err.errors`. 43 | **Please make sure this does not expose sensitive information to the requestor!** 44 | 45 | You can use `err.errors` also to trigger other behaviour in a registered error handler. 46 | 47 | Using multiple securityHandlers it is possible to use AND and OR logic: 48 | E.g.: 49 | ```json 50 | ... 51 | { 52 | "petstore_auth1": [ 53 | "read:pets" 54 | ], 55 | "petstore_auth2": [ 56 | "read:pets" 57 | ], 58 | }, 59 | { 60 | "petstore_auth3": [ 61 | "read:pets" 62 | ], 63 | }, 64 | ``` 65 | means that authorization is granted if `((petstore_auth1("read:pets") AND petstore_auth2("read:pets")) OR (petstore_auth3("read:pets")))` returns without error. 66 | 67 | For a more elaborate example see the [examples/generated-javascript-project](/examples/generated-javascript-project) folder. 68 | -------------------------------------------------------------------------------- /docs/servers.md: -------------------------------------------------------------------------------- 1 |

fastify-openapi-glue

2 | 3 | ## OpenApi V3 servers 4 | 5 | The [OpenApi](https://www.openapis.org/) V3 specification allows for multiple servers, e.g: 6 | ```json 7 | { 8 | "servers": [ 9 | { 10 | "url": "https://development.gigantic-server.com/dev/v1", 11 | "description": "Development server" 12 | }, 13 | { 14 | "url": "https://staging.gigantic-server.com/staging/v1", 15 | "description": "Staging server" 16 | }, 17 | { 18 | "url": "https://api.gigantic-server.com/v1", 19 | "description": "Production server" 20 | } 21 | ] 22 | } 23 | 24 | ``` 25 | 26 | but also allows for variables in server urls: 27 | 28 | ```json 29 | { 30 | "servers": [ 31 | { 32 | "url": "https://{username}.gigantic-server.com:{port}/{basePath}", 33 | "description": "The production API server", 34 | "variables": { 35 | "username": { 36 | "default": "demo", 37 | "description": "this value is assigned by the service provider, in this example `gigantic-server.com`" 38 | }, 39 | "port": { 40 | "enum": [ 41 | "8443", 42 | "443" 43 | ], 44 | "default": "8443" 45 | }, 46 | "basePath": { 47 | "default": "v2" 48 | } 49 | } 50 | } 51 | ] 52 | } 53 | ``` 54 | 55 | Fastify-openapi-glue does not know on which server and with what parameters the developer wants to host their service. So if you need to have a prefix then the ['prefix' option](../README.md#options) might be able to help you out. 56 | 57 | ### Multiple prefixes 58 | 59 | The 'prefix' option only allows for a single value. 60 | However since the result produced by fastify-openapi-glue is a fastify plugin there is nothing that prevents you from creating multiple plugins on multiple prefixes and loading them all up in Fastify. E.g. something along the lines of: 61 | 62 | ```javascript 63 | import Fastify from "fastify"; 64 | import openapiGlue from "fastify-openapi-glue"; 65 | import DevService from "./dev/service.js"; 66 | import StagingService from "./staging/service.js"; 67 | import ProdService from "./prod/service.js"; 68 | 69 | const fastify = new Fastify(); 70 | const specification = `./petstore-openapi.v3.json`; 71 | 72 | fastify.register( 73 | openapiGlue, 74 | { 75 | specification, 76 | serviceHandlers: new DevService(), 77 | prefix: "dev/v1" 78 | } 79 | ) 80 | 81 | fastify.register( 82 | openapiGlue, 83 | { 84 | specification, 85 | serviceHandlers: new StagingService(), 86 | prefix: "staging/v1" 87 | } 88 | ) 89 | 90 | fastify.register( 91 | openapiGlue, 92 | { 93 | specification, 94 | serviceHandlers: new ProdService(), 95 | prefix: "v1" 96 | } 97 | ) 98 | 99 | fastify.listen({port:3000}) 100 | ``` 101 | 102 | You might also want to check out the [fastify autoload plugin](https://github.com/fastify/fastify-autoload). 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /docs/serviceHandlers.md: -------------------------------------------------------------------------------- 1 |

fastify-openapi-glue

2 | 3 | ## ServiceHandlers 4 | Each [OpenApi](https://www.openapis.org/) `Operation` object typically contains a unique `operationId` property. E.g. the [PetStore example](../examples/petstore) contains an [OpenApi specification](../examples/petstore/petstore-openapi.v3.json) which contains a section: 5 | ```json 6 | ... 7 | "/pet/{petId}": { 8 | "get": { 9 | "tags": ["pet"], 10 | "summary": "Find pet by ID", 11 | "description": "Returns a single pet", 12 | "operationId": "getPetById", 13 | ... 14 | ``` 15 | 16 | In this Petstore example the `Service` class in [service.js](../examples/petstore/service.js) contains: 17 | ```javascript 18 | async getPetById(req, resp) { 19 | console.log("getPetById", req.params.petId); 20 | if (req.params.petId === 0) { 21 | // missing required data on purpose ! 22 | // this will trigger a server error on serialization 23 | return { pet: "Doggie the dog" }; 24 | } 25 | ... 26 | ``` 27 | 28 | If you provide this class to the `serviceHandlers` option then `fastify-openapi-glue` will create a configuration for Fastify (including validation schema's and potentially securityHandlers) that will map the path `/pet/{petId}` to the method with the name of the `operationId`, in this case `getPetByID`. All parameters that a caller provides to Fastify will be passed on to the method and any data returned by the method will be returned to the caller. 29 | 30 | ### No operationId 31 | If no `operationId` is present in the specification then `fastify-openapi-glue` will try to generate one based on the path and the type of operation. 32 | 33 | If you let `fastify-openapi-glue` [generate](../README.md#generator) a project you can see exactly what methods the plugin will look for. 34 | 35 | ### OperationResolver 36 | If you want to use a different mapping of operationId's to methods then you can use the [operationResolver](operationResolver.md) option. -------------------------------------------------------------------------------- /docs/subSchemas.md: -------------------------------------------------------------------------------- 1 |

fastify-openapi-glue

2 | 3 | ## Using subschemas 4 | 5 | Sometimes a specification is composed of multiple files that each contain parts 6 | of the specification. The specification refers to these subspecifications using 7 | `external references`. Since references are based on URI's (so `Identifier` and not 8 | `Location` as in URL's!) there needs to be a way to resolve those as they are not automatically resolved. A `$ref` does not automatically include the file it is pointing at, it merely points at another schema. 9 | 10 | So when you write: 11 | `$ref: './schemas/aws/SomeSchema.json'` 12 | It will try to find a piece of schema with `$id: './schemas/aws/SomeSchema.json'` (e.g. in the same file) where $id contains the URI instead of trying to load a schema from `./schemas/aws/SomeSchema.json`. 13 | 14 | The JSON schema specification has a page on [schema structuring](https://json-schema.org/understanding-json-schema/structuring) that explains it in more detail. 15 | 16 | One way to integrate these subschemas into one single schema is by using [@seriousme/openapi-schema-validator](https://github.com/seriousme/openapi-schema-validator). 17 | 18 | E.g.: we have a main specification in `main-spec.yaml` (JSON works too) containing: 19 | 20 | ```yaml 21 | ... 22 | paths: 23 | /pet: 24 | post: 25 | tags: 26 | - pet 27 | summary: Add a new pet to the store 28 | description: '' 29 | operationId: addPet 30 | responses: 31 | '405': 32 | description: Invalid input 33 | requestBody: 34 | $ref: 'http://www.example.com/subspec#/components/requestBodies/Pet' 35 | ``` 36 | 37 | And the reference is in `sub-spec.yaml`, containing: 38 | 39 | ```yaml 40 | $id: 'http://www.example.com/subspec' 41 | 42 | components: 43 | requestBodies: 44 | Pet: 45 | content: 46 | application/json: 47 | schema: 48 | $ref: '#/components/schemas/Pet' 49 | application/xml: 50 | schema: 51 | $ref: '#/components/schemas/Pet' 52 | description: Pet object that needs to be added to the store 53 | required: true 54 | ... 55 | ``` 56 | 57 | Then the schemas can be integrated using the following command: 58 | 59 | `npx -p @seriousme/openapi-schema-validator bundle-api main-spec.yaml sub-spec.yaml` 60 | 61 | or using code: 62 | 63 | ```javascript 64 | import { Validator } from "@seriousme/openapi-schema-validator"; 65 | const validator = new Validator(); 66 | const res = await validatorBundle.validate("./main-spec.yaml", "./sub-spec.yaml"]); 67 | // res now contains the results of the validation across main-spec and sub-spec 68 | const specification = validator.specification; 69 | // specification now contains a Javascript object containing the specification 70 | // with the subspec inlined. 71 | ``` 72 | 73 | You can now feed the resulting `specification` to directly to `fastify-openapi-glue`. 74 | -------------------------------------------------------------------------------- /examples/generated-javascript-project/README.md: -------------------------------------------------------------------------------- 1 | # generated-javascript-project 2 | 3 | This directory contains a fastify plugin that was autogenerated using 4 | [fastify-openapi-glue](https://github.com/seriousme/fastify-openapi-glue) and 5 | the OpenApi specifation in [openApi.json](openApi.json) 6 | 7 | In this directory use: 8 | + "npm install" to install its dependencies 9 | + "npm start" to start fastify using fastify-cli 10 | + "npm run dev" to start fastify using fastify-cli with logging to the console 11 | + "npm test" to run tests 12 | 13 | note: the auto generated test scaffolding does not contain any data yet ! 14 | 15 | -------------------------------------------------------------------------------- /examples/generated-javascript-project/index.js: -------------------------------------------------------------------------------- 1 | // Fastify plugin autogenerated by fastify-openapi-glue 2 | import openapiGlue from "../../lib/templates/index.js"; 3 | import { Security } from "./security.js"; 4 | import { Service } from "./service.js"; 5 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 6 | 7 | const pluginOptions = { 8 | specification: localFile("./openApi.json"), 9 | serviceHandlers: new Service(), 10 | securityHandlers: new Security(), 11 | }; 12 | 13 | export default async function (fastify, opts) { 14 | fastify.register(openapiGlue, { ...pluginOptions, ...opts }); 15 | } 16 | 17 | export const options = { 18 | ajv: { 19 | customOptions: { 20 | strict: false, 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/generated-javascript-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swaggerpetstore", 3 | "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "fastify start --options index.js", 8 | "test": "c8 node --test test/test-*.js", 9 | "dev": "fastify start -l info -P --options index.js" 10 | }, 11 | "directories": { 12 | "test": "test" 13 | }, 14 | "dependencies": { 15 | "fastify-plugin": "^5.0.1", 16 | "@seriousme/openapi-schema-validator": "^2.4.1", 17 | "js-yaml": "^4.1.0", 18 | "fastify-openapi-glue": "^4.9.0" 19 | }, 20 | "devDependencies": { 21 | "fastify": "^5.3.2", 22 | "fastify-cli": "^7.4.0", 23 | "c8": "^10.1.3", 24 | "@biomejs/biome": "^1.9.4" 25 | } 26 | } -------------------------------------------------------------------------------- /examples/generated-javascript-project/security.js: -------------------------------------------------------------------------------- 1 | // implementation of the security schemes in the openapi specification 2 | 3 | export class Security { 4 | async initialize(schemes) { 5 | // schemes will contain securitySchemes as found in the openapi specification 6 | console.log("Initialize:", JSON.stringify(schemes)); 7 | } 8 | 9 | // Security scheme: petstore_auth 10 | // Type: oauth2 11 | async petstore_auth(req, reply, params) { 12 | console.log("petstore_auth: Authenticating request"); 13 | // If validation fails: throw new Error('Could not authenticate request') 14 | // Else, simply return. 15 | 16 | // The request object can also be mutated here (e.g. to set 'req.user') 17 | } 18 | 19 | // Security scheme: api_key 20 | // Type: apiKey 21 | async api_key(req, reply, params) { 22 | console.log("api_key: Authenticating request"); 23 | // If validation fails: throw new Error('Could not authenticate request') 24 | // Else, simply return. 25 | 26 | // The request object can also be mutated here (e.g. to set 'req.user') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/generated-standaloneJS-project/README.md: -------------------------------------------------------------------------------- 1 | # generated-standaloneJS-project 2 | 3 | This directory contains a fastify plugin that was autogenerated using 4 | [fastify-openapi-glue](https://github.com/seriousme/fastify-openapi-glue) and 5 | the OpenApi specifation in [openApi.json](openApi.json) 6 | 7 | In this directory use: 8 | + "npm install" to install its dependencies 9 | + "npm start" to start fastify using fastify-cli 10 | + "npm run dev" to start fastify using fastify-cli with logging to the console 11 | + "npm test" to run tests 12 | 13 | note: the auto generated test scaffolding does not contain any data yet ! 14 | 15 | -------------------------------------------------------------------------------- /examples/generated-standaloneJS-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swaggerpetstore", 3 | "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "fastify start --options index.js", 8 | "test": "c8 node --test test/test-*.js", 9 | "dev": "fastify start -l info -P --options index.js" 10 | }, 11 | "directories": { 12 | "test": "test" 13 | }, 14 | "dependencies": { 15 | "fastify-plugin": "^5.0.1", 16 | "@seriousme/openapi-schema-validator": "^2.4.1", 17 | "js-yaml": "^4.1.0" 18 | }, 19 | "devDependencies": { 20 | "fastify": "^5.3.2", 21 | "fastify-cli": "^7.4.0", 22 | "c8": "^10.1.3", 23 | "@biomejs/biome": "^1.9.4" 24 | } 25 | } -------------------------------------------------------------------------------- /examples/generated-standaloneJS-project/security.js: -------------------------------------------------------------------------------- 1 | // implementation of the security schemes in the openapi specification 2 | 3 | export class Security { 4 | async initialize(schemes) { 5 | // schemes will contain securitySchemes as found in the openapi specification 6 | console.log("Initialize:", JSON.stringify(schemes)); 7 | } 8 | 9 | // Security scheme: petstore_auth 10 | // Type: oauth2 11 | async petstore_auth(req, reply, params) { 12 | console.log("petstore_auth: Authenticating request"); 13 | // If validation fails: throw new Error('Could not authenticate request') 14 | // Else, simply return. 15 | 16 | // The request object can also be mutated here (e.g. to set 'req.user') 17 | } 18 | 19 | // Security scheme: api_key 20 | // Type: apiKey 21 | async api_key(req, reply, params) { 22 | console.log("api_key: Authenticating request"); 23 | // If validation fails: throw new Error('Could not authenticate request') 24 | // Else, simply return. 25 | 26 | // The request object can also be mutated here (e.g. to set 'req.user') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/generated-standaloneJS-project/securityHandlers.js: -------------------------------------------------------------------------------- 1 | const emptyScheme = Symbol("emptyScheme"); 2 | 3 | export class SecurityError extends Error { 4 | constructor(message, statusCode, name, errors) { 5 | super(message); 6 | this.statusCode = statusCode; 7 | this.name = name; 8 | this.errors = errors; 9 | } 10 | } 11 | 12 | export class SecurityHandlers { 13 | /** constructor */ 14 | constructor(handlers) { 15 | this.handlers = handlers; 16 | this.handlerMap = new Map(); 17 | this.missingHandlers = []; 18 | } 19 | 20 | add(schemes) { 21 | if (!(schemes?.length > 0)) { 22 | return false; 23 | } 24 | const mapKey = JSON.stringify(schemes); 25 | if (!this.handlerMap.has(mapKey)) { 26 | for (const schemeList of schemes) { 27 | for (const name in schemeList) { 28 | if (!(name in this.handlers)) { 29 | this.handlers[name] = () => { 30 | throw `Missing handler for "${name}" validation`; 31 | }; 32 | this.missingHandlers.push(name); 33 | } 34 | } 35 | } 36 | this.handlerMap.set(mapKey, this._buildHandler(schemes)); 37 | } 38 | return this.handlerMap.has(mapKey); 39 | } 40 | 41 | get(schemes) { 42 | const mapKey = JSON.stringify(schemes); 43 | return this.handlerMap.get(mapKey); 44 | } 45 | 46 | has(schemes) { 47 | const mapKey = JSON.stringify(schemes); 48 | return this.handlerMap.has(mapKey); 49 | } 50 | 51 | getMissingHandlers() { 52 | return this.missingHandlers; 53 | } 54 | 55 | _buildHandler(schemes) { 56 | const securityHandlers = this.handlers; 57 | return async (req, reply) => { 58 | const handlerErrors = []; 59 | const schemeListDone = []; 60 | let statusCode = 401; 61 | for (const schemeList of schemes) { 62 | let name; 63 | const andList = []; 64 | try { 65 | for (name in schemeList) { 66 | const parameters = schemeList[name]; 67 | andList.push(name); 68 | // all the handlers in a scheme list must succeed 69 | await securityHandlers[name](req, reply, parameters); 70 | } 71 | return; // If one list of schemes passes, no need to try any others 72 | } catch (err) { 73 | req.log.debug(`Security handler '${name}' failed: '${err}'`); 74 | handlerErrors.push(err); 75 | if (err.statusCode !== undefined) { 76 | statusCode = err.statusCode; 77 | } 78 | } 79 | schemeListDone.push(andList.toString()); 80 | } 81 | // if we get this far no security handlers validated this request 82 | throw new SecurityError( 83 | `None of the security schemes (${schemeListDone.join( 84 | ", ", 85 | )}) successfully authenticated this request.`, 86 | statusCode, 87 | "Unauthorized", 88 | handlerErrors, 89 | ); 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/petstore/index.js: -------------------------------------------------------------------------------- 1 | // a fastify plugin to demo fastify-openapi-glue 2 | // it can be run as plugin on any fastify server 3 | // or standalone using "fastify start index.js --options" 4 | import openapiGlue from "../../index.js"; 5 | import { Service } from "./service.js"; 6 | const serviceHandlers = new Service(); 7 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 8 | 9 | const pluginOptions = { 10 | specification: localFile("./petstore-openapi.v3.json"), 11 | serviceHandlers, 12 | prefix: "v2", 13 | }; 14 | 15 | export default async function (fastify, opts) { 16 | fastify.register(openapiGlue, pluginOptions); 17 | } 18 | 19 | export const options = { 20 | ajv: { 21 | customOptions: { 22 | strict: false, 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /examples/petstore/service.js: -------------------------------------------------------------------------------- 1 | // an example of implementation of the operations in the openapi specification 2 | 3 | export class Service { 4 | async getPetById(req, resp) { 5 | console.log("getPetById", req.params.petId); 6 | if (req.params.petId === 0) { 7 | // missing required data on purpose ! 8 | // this will trigger a server error on serialization 9 | return { pet: "Doggie the dog" }; 10 | } 11 | return { 12 | id: req.params.petId, 13 | name: "Kitty the cat", 14 | photoUrls: [ 15 | "https://en.wikipedia.org/wiki/Cat#/media/File:Kittyply_edit1.jpg", 16 | ], 17 | status: "available", 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type { 4 | FastifyPluginAsync, 5 | FastifyReply, 6 | FastifyRequest, 7 | RouteOptions, 8 | } from "fastify"; 9 | 10 | type OperationResolver = ( 11 | operationId: string, 12 | method: string, 13 | path: string, 14 | ) => ((req: FastifyRequest, res: FastifyReply) => void) | RouteOptions; 15 | 16 | export interface FastifyOpenapiGlueOptions { 17 | specification: object | string; 18 | serviceHandlers?: object; 19 | /** @deprecated use serviceHandlers field instead */ 20 | service?: object; 21 | securityHandlers?: object; 22 | operationResolver?: OperationResolver; 23 | prefix?: string; 24 | addEmptySchema?: boolean; 25 | /** 26 | * NOTE: 27 | * This does not enable cookie validation (Fastify core does not support cookie validation). 28 | * This is simply a flag which triggers the addition of cookie schema (from the OpenAPI specification), into the 'schema' property of Fastify Routes options. 29 | * You can then hook Fastify's 'onRoute' event to make use of the schema as you wish. 30 | */ 31 | addCookieSchema?: boolean; 32 | } 33 | 34 | declare const fastifyOpenapiGlue: FastifyPluginAsync; 35 | declare interface SecurityError extends Error { 36 | statusCode: number; 37 | name: string; 38 | errors: Error[]; 39 | } 40 | 41 | export default fastifyOpenapiGlue; 42 | export { fastifyOpenapiGlue }; 43 | export type { SecurityError }; 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin"; 2 | import { Parser } from "./lib/Parser.js"; 3 | import { SecurityError, SecurityHandlers } from "./lib/securityHandlers.js"; 4 | 5 | function checkObject(obj, name) { 6 | if (typeof obj === "object" && obj !== null) { 7 | return; 8 | } 9 | throw new Error(`'${name}' parameter must refer to an object`); 10 | } 11 | 12 | function checkParserValidators(instance, contentTypes) { 13 | for (const contentType of contentTypes) { 14 | if (!instance.hasContentTypeParser(contentType)) { 15 | instance.log.warn(`ContentTypeParser for '${contentType}' not found`); 16 | } 17 | } 18 | } 19 | 20 | function notImplemented(operationId) { 21 | return async () => { 22 | throw new Error(`Operation ${operationId} not implemented`); 23 | }; 24 | } 25 | 26 | function defaultOperationResolver(routesInstance, serviceHandlers) { 27 | return (operationId) => { 28 | if (operationId in serviceHandlers) { 29 | routesInstance.log.debug(`serviceHandlers has '${operationId}'`); 30 | return serviceHandlers[operationId].bind(serviceHandlers); 31 | } 32 | }; 33 | } 34 | 35 | function createSecurityHandlers(instance, security, config) { 36 | for (const item of config.routes) { 37 | security.add(item.security); 38 | } 39 | const missingSecurityHandlers = security.getMissingHandlers(); 40 | if (missingSecurityHandlers.length > 0) { 41 | instance.log.warn( 42 | `Handlers for some security requirements were missing: ${missingSecurityHandlers.join( 43 | ", ", 44 | )}`, 45 | ); 46 | } 47 | } 48 | 49 | async function getSecurity(instance, securityHandlers, config) { 50 | if (securityHandlers) { 51 | checkObject(securityHandlers, "securityHandlers"); 52 | const security = new SecurityHandlers(securityHandlers); 53 | if ("initialize" in securityHandlers) { 54 | await securityHandlers.initialize(config.securitySchemes); 55 | } 56 | createSecurityHandlers(instance, security, config); 57 | return security; 58 | } 59 | return undefined; 60 | } 61 | 62 | function getResolver(instance, serviceHandlers, operationResolver) { 63 | if (serviceHandlers && operationResolver) { 64 | throw new Error( 65 | "'serviceHandlers' and 'operationResolver' are mutually exclusive", 66 | ); 67 | } 68 | 69 | if (!(serviceHandlers || operationResolver)) { 70 | throw new Error( 71 | "either 'serviceHandlers' or 'operationResolver' are required", 72 | ); 73 | } 74 | 75 | if (operationResolver) { 76 | return operationResolver; 77 | } 78 | 79 | checkObject(serviceHandlers, "serviceHandlers"); 80 | return defaultOperationResolver(instance, serviceHandlers); 81 | } 82 | 83 | // Apply service handler if present or else a notImplemented error 84 | function serviceHandlerOptions(resolver, item) { 85 | const handler = resolver(item.operationId, item.method, item.openapiPath); 86 | 87 | const routeOptions = 88 | typeof handler === "function" ? { handler } : { ...handler }; 89 | 90 | routeOptions.handler = 91 | routeOptions.handler || notImplemented(item.operationId); 92 | 93 | return routeOptions; 94 | } 95 | 96 | // Apply security requirements if present and at least one security handler is defined 97 | function securityHandler(security, item) { 98 | if (security?.has(item.security)) { 99 | return security.get(item.security).bind(security.handlers); 100 | } 101 | return undefined; 102 | } 103 | 104 | function makeGenerateRoutes(config, resolver, security) { 105 | return async function generateRoutes(routesInstance) { 106 | for (const item of config.routes) { 107 | routesInstance.route({ 108 | method: item.method, 109 | url: item.url, 110 | schema: item.schema, 111 | config: item.config, 112 | preHandler: securityHandler(security, item), 113 | ...serviceHandlerOptions(resolver, item), 114 | }); 115 | } 116 | }; 117 | } 118 | 119 | // this is the main function for the plugin 120 | async function plugin(instance, opts) { 121 | const parser = new Parser(); 122 | const config = await parser.parse(opts.specification, { 123 | addEmptySchema: opts.addEmptySchema ?? false, 124 | addCookieSchema: opts.addCookieSchema ?? false, 125 | }); 126 | checkParserValidators(instance, config.contentTypes); 127 | if (opts.service) { 128 | process.emitWarning( 129 | "The 'service' option is deprecated, use 'serviceHandlers' instead.", 130 | "DeprecationWarning", 131 | "FSTOGDEP001", 132 | ); 133 | opts.serviceHandlers = opts.service; 134 | } 135 | 136 | // use the provided operation resolver or default to looking in the serviceHandlers object 137 | const resolver = getResolver( 138 | instance, 139 | opts.serviceHandlers, 140 | opts.operationResolver, 141 | ); 142 | 143 | const security = await getSecurity(instance, opts.securityHandlers, config); 144 | 145 | const routeConf = {}; 146 | if (opts.prefix) { 147 | routeConf.prefix = opts.prefix; 148 | } else if (config.prefix) { 149 | routeConf.prefix = config.prefix; 150 | } 151 | 152 | await instance.register( 153 | makeGenerateRoutes(config, resolver, security), 154 | routeConf, 155 | ); 156 | } 157 | 158 | const fastifyOpenapiGlue = fp(plugin, { 159 | fastify: ">=4.0.0", 160 | name: "fastify-openapi-glue", 161 | }); 162 | 163 | export default fastifyOpenapiGlue; 164 | export { fastifyOpenapiGlue, SecurityError }; 165 | 166 | export const options = { 167 | specification: "examples/petstore/petstore-openapi.v3.json", 168 | serviceHandlers: "examples/petstore/serviceHandlers.js", 169 | }; 170 | -------------------------------------------------------------------------------- /lib/Parser.js: -------------------------------------------------------------------------------- 1 | // this class is to bridge various parser versions 2 | import { Validator } from "@seriousme/openapi-schema-validator"; 3 | import { ParserV2 } from "./Parser.v2.js"; 4 | import { ParserV3 } from "./Parser.v3.js"; 5 | 6 | export class Parser { 7 | /** 8 | * get the original specification as object 9 | * @returns {object} 10 | */ 11 | specification() { 12 | return this.original; 13 | } 14 | 15 | async preProcessSpec(specification) { 16 | const validator = new Validator(); 17 | try { 18 | const res = await validator.validate(specification); 19 | if (res.valid) { 20 | // clone the original specification as resolveRefs modifies the schema 21 | this.original = JSON.parse( 22 | JSON.stringify(validator.specification, null, 2), 23 | ); 24 | return { 25 | valid: true, 26 | version: validator.version, 27 | spec: validator.resolveRefs(), 28 | }; 29 | } 30 | throw Error(JSON.stringify(res.errors, null, 2)); 31 | } catch (e) { 32 | // eslint-disable-next-line 33 | console.log(e.message); 34 | return { valid: false }; 35 | } 36 | } 37 | 38 | /** 39 | * parse a openapi specification 40 | * @param {string|object} specification Filename of JSON/YAML file or object containing an openapi specification 41 | * @returns {object} fastify configuration information 42 | */ 43 | 44 | async parse(specification, options = {}) { 45 | const supportedVersions = new Set(["2.0", "3.0", "3.1"]); 46 | 47 | const res = await this.preProcessSpec(specification); 48 | if (!(res.valid && supportedVersions.has(res.version))) { 49 | throw new Error( 50 | "'specification' parameter must contain a valid version 2.0 or 3.0.x or 3.1.x specification", 51 | ); 52 | } 53 | 54 | if (res.version === "2.0") { 55 | const parserV2 = new ParserV2(); 56 | return parserV2.parse(res.spec, options); 57 | } 58 | 59 | const parserV3 = new ParserV3(); 60 | return parserV3.parse(res.spec, options); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/Parser.v2.js: -------------------------------------------------------------------------------- 1 | // a class for parsing openapi V2 data into config data for fastify 2 | import { ParserBase } from "./ParserBase.js"; 3 | 4 | const paramSchemaProps = [ 5 | "type", 6 | "description", 7 | "format", 8 | "allowEmptyValue", 9 | "items", 10 | "collectionFormat", 11 | "default", 12 | "maximum", 13 | "exclusiveMaximum", 14 | "minimum", 15 | "exclusiveMinimum", 16 | "maxLength", 17 | "minLength", 18 | "pattern", 19 | "maxItems", 20 | "minItems", 21 | "uniqueItems", 22 | "enum", 23 | "multipleOf", 24 | ]; 25 | 26 | export class ParserV2 extends ParserBase { 27 | parseParams(data) { 28 | const params = { 29 | type: "object", 30 | properties: {}, 31 | }; 32 | const required = []; 33 | for (const item of data) { 34 | // item.type "file" breaks ajv, so treat is as a special here 35 | if (item.type === "file") { 36 | item.type = "string"; 37 | item.isFile = true; 38 | } 39 | // 40 | params.properties[item.name] = {}; 41 | this.copyProps(item, params.properties[item.name], paramSchemaProps); 42 | // ajv wants "required" to be an array, which seems to be too strict 43 | // see https://github.com/json-schema/json-schema/wiki/Properties-and-required 44 | if (item.required) { 45 | required.push(item.name); 46 | } 47 | } 48 | if (required.length > 0) { 49 | params.required = required; 50 | } 51 | return params; 52 | } 53 | 54 | parseParameters(schema, data) { 55 | const params = []; 56 | const querystring = []; 57 | const headers = []; 58 | const formData = []; 59 | for (const item of data) { 60 | switch (item.in) { 61 | case "body": { 62 | schema.body = item.schema; 63 | break; 64 | } 65 | case "formData": { 66 | formData.push(item); 67 | break; 68 | } 69 | case "path": { 70 | params.push(item); 71 | break; 72 | } 73 | case "query": { 74 | querystring.push(item); 75 | break; 76 | } 77 | case "header": { 78 | headers.push(item); 79 | break; 80 | } 81 | } 82 | } 83 | if (params.length > 0) { 84 | schema.params = this.parseParams(params); 85 | } 86 | if (querystring.length > 0) { 87 | schema.querystring = this.parseParams(querystring); 88 | } 89 | if (headers.length > 0) { 90 | schema.headers = this.parseParams(headers); 91 | } 92 | if (formData.length > 0) { 93 | schema.body = this.parseParams(formData); 94 | } 95 | } 96 | 97 | parseResponses(responses) { 98 | const result = {}; 99 | for (const httpCode in responses) { 100 | if (responses[httpCode].schema !== undefined) { 101 | result[httpCode] = responses[httpCode].schema; 102 | continue; 103 | } 104 | if (this.options.addEmptySchema) { 105 | result[httpCode] = {}; 106 | } 107 | } 108 | return result; 109 | } 110 | 111 | makeSchema(genericSchema, data) { 112 | const schema = Object.assign({}, genericSchema); 113 | const copyItems = [ 114 | "tags", 115 | "summary", 116 | "description", 117 | "operationId", 118 | "produces", 119 | "consumes", 120 | "deprecated", 121 | ]; 122 | this.copyProps(data, schema, copyItems, true); 123 | if (data.parameters) { 124 | this.parseParameters(schema, data.parameters); 125 | } 126 | const response = this.parseResponses(data.responses); 127 | if (Object.keys(response).length > 0) { 128 | schema.response = response; 129 | } 130 | 131 | // remove loops from the schema so fastify wont break 132 | this.removeRecursion(schema); 133 | return schema; 134 | } 135 | 136 | parse(spec, options) { 137 | this.spec = spec; 138 | this.options = { 139 | addEmptySchema: options.addEmptySchema ?? false, 140 | }; 141 | 142 | for (const item in spec) { 143 | switch (item) { 144 | case "paths": { 145 | this.processPaths(spec.paths); 146 | break; 147 | } 148 | case "basePath": 149 | this.config.prefix = spec[item]; 150 | break; 151 | case "securityDefinitions": 152 | this.config.securitySchemes = spec[item]; 153 | break; 154 | default: 155 | this.config.generic[item] = spec[item]; 156 | } 157 | } 158 | return this.config; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/Parser.v3.js: -------------------------------------------------------------------------------- 1 | // a class for parsing openapi v3 data into config data for fastify 2 | 3 | import { ParserBase } from "./ParserBase.js"; 4 | 5 | function isExploding(item) { 6 | const explode = !!(item.explode ?? item.style === "form"); 7 | return explode !== false && item.schema?.type === "object"; 8 | } 9 | 10 | export class ParserV3 extends ParserBase { 11 | parseQueryString(data) { 12 | if (data.length === 1) { 13 | if (typeof data[0].content === "object") { 14 | return this.parseContent(data[0], true); 15 | } 16 | if (isExploding(data[0])) { 17 | return data[0].schema; 18 | } 19 | } 20 | 21 | return this.parseParams(data); 22 | } 23 | 24 | parseParams(data) { 25 | const params = { 26 | type: "object", 27 | properties: {}, 28 | }; 29 | const required = []; 30 | for (const item of data) { 31 | params.properties[item.name] = item.schema; 32 | this.copyProps(item, params.properties[item.name], ["description"]); 33 | // ajv wants "required" to be an array, which seems to be too strict 34 | // see https://github.com/json-schema/json-schema/wiki/Properties-and-required 35 | if (item.required) { 36 | required.push(item.name); 37 | } 38 | } 39 | if (required.length > 0) { 40 | params.required = required; 41 | } 42 | return params; 43 | } 44 | 45 | parseParameters(schema, data) { 46 | const params = []; 47 | const querystring = []; 48 | const headers = []; 49 | const cookies = []; 50 | // const formData = []; 51 | for (const item of data) { 52 | switch (item.in) { 53 | // case "body": 54 | // schema.body = item.schema; 55 | // break; 56 | // case "formData": 57 | // formData.push(item); 58 | // break; 59 | case "path": { 60 | item.style = item.style || "simple"; 61 | params.push(item); 62 | break; 63 | } 64 | case "query": { 65 | item.style = item.style || "form"; 66 | querystring.push(item); 67 | break; 68 | } 69 | case "header": { 70 | item.style = item.style || "simple"; 71 | headers.push(item); 72 | break; 73 | } 74 | case "cookie": { 75 | if (this.options.addCookieSchema) { 76 | item.style = item.style || "form"; 77 | cookies.push(item); 78 | } else { 79 | console.warn("cookie parameters are not supported by Fastify"); 80 | } 81 | break; 82 | } 83 | } 84 | } 85 | if (params.length > 0) { 86 | schema.params = this.parseParams(params); 87 | } 88 | if (querystring.length > 0) { 89 | schema.querystring = this.parseQueryString(querystring); 90 | } 91 | if (headers.length > 0) { 92 | schema.headers = this.parseParams(headers); 93 | } 94 | if (cookies.length > 0) { 95 | schema.cookies = this.parseParams(cookies); 96 | } 97 | } 98 | 99 | parseContent(data, maxOne = false) { 100 | if (data?.content) { 101 | const result = { content: {} }; 102 | const mimeTypes = Object.keys(data.content); 103 | if (mimeTypes.length === 0) { 104 | return undefined; 105 | } 106 | for (const mimeType of mimeTypes) { 107 | this.config.contentTypes.add(mimeType); 108 | if (data.content[mimeType].schema) { 109 | result.content[mimeType] = {}; 110 | result.content[mimeType].schema = data.content[mimeType].schema; 111 | } 112 | if (maxOne) { 113 | return data.content[mimeType].schema; 114 | } 115 | } 116 | return result; 117 | } 118 | return undefined; 119 | } 120 | 121 | parseResponses(responses) { 122 | const result = {}; 123 | for (const httpCode in responses) { 124 | const body = this.parseContent(responses[httpCode]); 125 | if (body !== undefined) { 126 | result[httpCode] = body; 127 | continue; 128 | } 129 | if (this.options.addEmptySchema) { 130 | result[httpCode] = {}; 131 | } 132 | } 133 | return result; 134 | } 135 | 136 | makeSchema(genericSchema, data) { 137 | const schema = Object.assign({}, genericSchema); 138 | const copyItems = ["tags", "summary", "description", "operationId"]; 139 | this.copyProps(data, schema, copyItems, true); 140 | if (data.parameters) { 141 | this.parseParameters(schema, data.parameters); 142 | } 143 | const body = this.parseContent(data.requestBody); 144 | if (body) { 145 | schema.body = body; 146 | } 147 | const response = this.parseResponses(data.responses); 148 | if (Object.keys(response).length > 0) { 149 | schema.response = response; 150 | } 151 | 152 | this.removeRecursion(schema); 153 | return schema; 154 | } 155 | 156 | parse(spec, options) { 157 | this.spec = spec; 158 | this.options = { 159 | addEmptySchema: options.addEmptySchema ?? false, 160 | addCookieSchema: options.addCookieSchema ?? false, 161 | }; 162 | 163 | for (const item in spec) { 164 | switch (item) { 165 | case "paths": { 166 | this.processPaths(spec.paths); 167 | break; 168 | } 169 | case "components": 170 | if (spec.components.securitySchemes) { 171 | this.config.securitySchemes = spec.components.securitySchemes; 172 | } 173 | break; 174 | default: 175 | this.config.generic[item] = spec[item]; 176 | } 177 | } 178 | return this.config; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/ParserBase.js: -------------------------------------------------------------------------------- 1 | // baseclass for the v2 and v3 parsers 2 | 3 | export const HttpOperations = new Set([ 4 | "delete", 5 | "get", 6 | "head", 7 | "patch", 8 | "post", 9 | "put", 10 | "options", 11 | ]); 12 | 13 | export class ParserBase { 14 | constructor() { 15 | this.config = { generic: {}, routes: [], contentTypes: new Set() }; 16 | } 17 | 18 | makeOperationId(operation, path) { 19 | // make a nice camelCase operationID 20 | // e.g. get /user/{name} becomes getUserByName 21 | const firstUpper = (str) => str.substr(0, 1).toUpperCase() + str.substr(1); 22 | const by = (matched, p1) => `By${firstUpper(p1)}`; 23 | const parts = path.split("/").slice(1); 24 | parts.unshift(operation); 25 | const opId = parts 26 | .map((item, i) => (i > 0 ? firstUpper(item) : item)) 27 | .join("") 28 | .replace(/{(\w+)}/g, by) 29 | .replace(/[^a-z]/gi, ""); 30 | return opId; 31 | } 32 | 33 | makeURL(path) { 34 | // fastify wants 'path/:param' instead of openapis 'path/{param}' 35 | return path.replace(/{(\w+)}/g, ":$1"); 36 | } 37 | 38 | copyProps(source, target, list, copyXprops = false) { 39 | // copy properties from source to target, if they are in the list 40 | for (const item in source) { 41 | if (list.includes(item) || (copyXprops && item.startsWith("x-"))) { 42 | target[item] = source[item]; 43 | } 44 | } 45 | } 46 | 47 | removeRecursion(schemas) { 48 | function escapeJsonPointer(str) { 49 | return str.replace(/~/g, "~0").replace(/\//g, "~1"); 50 | } 51 | 52 | function processSchema(obj) { 53 | let refAdded = false; 54 | 55 | function inspectNode(obj, path, paths) { 56 | if (typeof obj === "object" && obj !== null) { 57 | if (paths.has(obj)) { 58 | return paths.get(obj); 59 | } 60 | const newPaths = new Map(paths); 61 | newPaths.set(obj, path); 62 | for (const key in obj) { 63 | const $ref = inspectNode( 64 | obj[key], 65 | `${path}/${escapeJsonPointer(key)}`, 66 | newPaths, 67 | ); 68 | if (typeof $ref === "string") { 69 | obj[key] = { $ref }; 70 | refAdded = true; 71 | } 72 | } 73 | } 74 | return undefined; 75 | } 76 | 77 | const paths = new Map(); 78 | inspectNode(obj, "#", paths); 79 | // AJV requires an $id attribute for references to work 80 | if (refAdded && typeof obj["$id"] === "undefined") { 81 | obj["$id"] = "http://example.com/fastifySchema"; 82 | } 83 | } 84 | 85 | for (const item in schemas) { 86 | const schema = schemas[item]; 87 | // the response schema in fastify is in the form of "response->200->schema" 88 | // it needs to be dereffed per HTTP response code 89 | if (item === "response") { 90 | for (const responseCode in schema) { 91 | processSchema(schema[responseCode]); 92 | } 93 | } else { 94 | // some schemas are in the form of "body->content->mimeType->schema" 95 | if (schema.content) { 96 | for (const contentType in schema.content) { 97 | processSchema(schema.content[contentType].schema); 98 | } 99 | } 100 | // all others are in the form of "query->schema" etc 101 | else { 102 | processSchema(schema); 103 | } 104 | } 105 | } 106 | } 107 | 108 | processOperation(path, operation, operationSpec, genericSchema) { 109 | if (operationSpec["x-no-fastify-config"]) { 110 | return; 111 | } 112 | const route = { 113 | method: operation.toUpperCase(), 114 | url: this.makeURL(path), 115 | schema: this.makeSchema(genericSchema, operationSpec), 116 | openapiPath: path, 117 | operationId: 118 | operationSpec.operationId || this.makeOperationId(operation, path), 119 | openapiSource: operationSpec, 120 | security: operationSpec.security || this.spec.security, 121 | }; 122 | 123 | if (operationSpec["x-fastify-config"]) { 124 | route.config = operationSpec["x-fastify-config"]; 125 | } 126 | 127 | this.config.routes.push(route); 128 | } 129 | 130 | processPaths(paths) { 131 | const copyItems = ["summary", "description"]; 132 | for (const path in paths) { 133 | const genericSchema = {}; 134 | const pathSpec = paths[path]; 135 | 136 | this.copyProps(pathSpec, genericSchema, copyItems, true); 137 | if (typeof pathSpec.parameters === "object") { 138 | this.parseParameters(genericSchema, pathSpec.parameters); 139 | } 140 | for (const operation in pathSpec) { 141 | if (HttpOperations.has(operation)) { 142 | this.processOperation( 143 | path, 144 | operation, 145 | pathSpec[operation], 146 | genericSchema, 147 | ); 148 | } 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /lib/generator.js: -------------------------------------------------------------------------------- 1 | // generator.js 2 | 3 | import { createHash } from "node:crypto"; 4 | import { mkdirSync, writeFileSync } from "node:fs"; 5 | import { createRequire } from "node:module"; 6 | import { join } from "node:path"; 7 | import { Parser } from "./Parser.js"; 8 | import { templateInfo } from "./templates/templateTypes.js"; 9 | 10 | const importJSON = createRequire(import.meta.url); 11 | const pluginPackage = await importJSON("../package.json"); 12 | 13 | const contents = (dir, fileName, data) => { 14 | return { 15 | path: join(dir, fileName), 16 | data, 17 | fileName, 18 | }; 19 | }; 20 | 21 | function makeDir(dir, dirMode) { 22 | try { 23 | mkdirSync(dir, dirMode); 24 | } catch (error) { 25 | if (error.code !== "EEXIST") { 26 | throw error; 27 | } 28 | } 29 | } 30 | 31 | function calcHash(data) { 32 | const hash = createHash("sha256"); 33 | hash.update(data); 34 | return hash.digest("hex"); 35 | } 36 | 37 | async function getDataGenerator(type) { 38 | const path = templateInfo[type].path; 39 | const projectGenerator = `./templates/${path}/projectData.js`; 40 | const { generateProjectData } = await import(projectGenerator); 41 | return generateProjectData; 42 | } 43 | 44 | export class Generator { 45 | constructor(checksumOnly, localPlugin) { 46 | this.parser = new Parser(); 47 | if (localPlugin) { 48 | this.localPlugin = true; 49 | } 50 | if (checksumOnly) { 51 | this.checksumOnly = true; 52 | } 53 | } 54 | 55 | async parse(specification) { 56 | this.config = await this.parser.parse(specification); 57 | this.specification = this.parser.specification(); 58 | return this; 59 | } 60 | 61 | generatePackage(newPkg) { 62 | function copyProps(target, source, key) { 63 | for (const item in source[key]) { 64 | target[key][item] = source[key][item]; 65 | } 66 | } 67 | 68 | const info = this.config.generic.info; 69 | // name and desciption are mandatory in the spec 70 | newPkg.name = info.title 71 | .toLowerCase() 72 | .replace(/[^a-z0-9_]/, "") 73 | .substr(0, 214); //npm package name has maxlength of 214 74 | 75 | newPkg.description = info.description; 76 | 77 | // only include dependencies when not calculating checksums 78 | // to avoid issues with dependabot and the likes 79 | if (!this.checksumOnly) { 80 | copyProps(newPkg, pluginPackage, "dependencies"); 81 | copyProps(newPkg, pluginPackage, "devDependencies"); 82 | } 83 | 84 | if (!(this.localPlugin || this.checksumOnly)) { 85 | // add openapi-glue as dependency for the generated code 86 | newPkg.dependencies[pluginPackage.name] = `^${pluginPackage.version}`; 87 | } 88 | return JSON.stringify(newPkg, null, 2); 89 | } 90 | 91 | async generateProject(dir, project, type = "javascript") { 92 | const projectDir = join(dir, project); 93 | const testDir = join(projectDir, "test"); 94 | const pluginPackageName = pluginPackage.name; 95 | 96 | const generateProjectData = await getDataGenerator(type); 97 | const { files, pkgTemplate, instructions } = generateProjectData({ 98 | project, 99 | projectDir, 100 | testDir, 101 | config: this.config, 102 | specification: this.specification, 103 | pluginPackageName, 104 | localPlugin: this.localPlugin, 105 | }); 106 | files.package = contents( 107 | projectDir, 108 | "package.json", 109 | this.generatePackage(pkgTemplate), 110 | ); 111 | 112 | const dirMode = 0o755; 113 | const fileOpts = { 114 | mode: 0o644, 115 | }; 116 | 117 | if (this.checksumOnly) { 118 | const results = {}; 119 | for (const key in files) { 120 | const file = files[key]; 121 | results[key] = { 122 | fileName: file.fileName, 123 | checksum: calcHash(file.data), 124 | }; 125 | } 126 | return { files: results }; 127 | } 128 | 129 | makeDir(projectDir, dirMode); 130 | makeDir(testDir, dirMode); 131 | 132 | for (const key in files) { 133 | const file = files[key]; 134 | writeFileSync(file.path, file.data, fileOpts); 135 | } 136 | return `Your project has been generated in "${project}" 137 | ${instructions}`; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/securityHandlers.js: -------------------------------------------------------------------------------- 1 | const emptyScheme = Symbol("emptyScheme"); 2 | 3 | export class SecurityError extends Error { 4 | constructor(message, statusCode, name, errors) { 5 | super(message); 6 | this.statusCode = statusCode; 7 | this.name = name; 8 | this.errors = errors; 9 | } 10 | } 11 | 12 | export class SecurityHandlers { 13 | /** constructor */ 14 | constructor(handlers) { 15 | this.handlers = handlers; 16 | this.handlerMap = new Map(); 17 | this.missingHandlers = []; 18 | } 19 | 20 | add(schemes) { 21 | if (!(schemes?.length > 0)) { 22 | return false; 23 | } 24 | const mapKey = JSON.stringify(schemes); 25 | if (!this.handlerMap.has(mapKey)) { 26 | for (const schemeList of schemes) { 27 | for (const name in schemeList) { 28 | if (!(name in this.handlers)) { 29 | this.handlers[name] = () => { 30 | throw `Missing handler for "${name}" validation`; 31 | }; 32 | this.missingHandlers.push(name); 33 | } 34 | } 35 | } 36 | this.handlerMap.set(mapKey, this._buildHandler(schemes)); 37 | } 38 | return this.handlerMap.has(mapKey); 39 | } 40 | 41 | get(schemes) { 42 | const mapKey = JSON.stringify(schemes); 43 | return this.handlerMap.get(mapKey); 44 | } 45 | 46 | has(schemes) { 47 | const mapKey = JSON.stringify(schemes); 48 | return this.handlerMap.has(mapKey); 49 | } 50 | 51 | getMissingHandlers() { 52 | return this.missingHandlers; 53 | } 54 | 55 | _buildHandler(schemes) { 56 | const securityHandlers = this.handlers; 57 | return async (req, reply) => { 58 | const handlerErrors = []; 59 | const schemeListDone = []; 60 | let statusCode = 401; 61 | for (const schemeList of schemes) { 62 | let name; 63 | const andList = []; 64 | try { 65 | for (name in schemeList) { 66 | const parameters = schemeList[name]; 67 | andList.push(name); 68 | // all the handlers in a scheme list must succeed 69 | await securityHandlers[name](req, reply, parameters); 70 | } 71 | return; // If one list of schemes passes, no need to try any others 72 | } catch (err) { 73 | req.log.debug(`Security handler '${name}' failed: '${err}'`); 74 | handlerErrors.push(err); 75 | if (err.statusCode !== undefined) { 76 | statusCode = err.statusCode; 77 | } 78 | } 79 | schemeListDone.push(andList.toString()); 80 | } 81 | // if we get this far no security handlers validated this request 82 | throw new SecurityError( 83 | `None of the security schemes (${schemeListDone.join( 84 | ", ", 85 | )}) successfully authenticated this request.`, 86 | statusCode, 87 | "Unauthorized", 88 | handlerErrors, 89 | ); 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/templates/js/README.md.js: -------------------------------------------------------------------------------- 1 | export default (projectName, instructions) => `# ${projectName} 2 | 3 | This directory contains a fastify plugin that was autogenerated using 4 | [fastify-openapi-glue](https://github.com/seriousme/fastify-openapi-glue) and 5 | the OpenApi specifation in [openApi.json](openApi.json) 6 | ${instructions} 7 | `; 8 | -------------------------------------------------------------------------------- /lib/templates/js/index.js: -------------------------------------------------------------------------------- 1 | export default (data) => 2 | `// Fastify plugin autogenerated by fastify-openapi-glue 3 | import openapiGlue from "${data.pluginPackageName}"; 4 | import { Security } from "./${data.securityFile}"; 5 | import { Service } from "./${data.serviceFile}"; 6 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 7 | 8 | const pluginOptions = { 9 | specification: localFile("./${data.specFile}"), 10 | serviceHandlers: new Service(), 11 | securityHandlers: new Security(), 12 | }; 13 | 14 | export default async function (fastify, opts) { 15 | fastify.register(openapiGlue, { ...pluginOptions, ...opts }); 16 | } 17 | 18 | export const options = { 19 | ajv: { 20 | customOptions: { 21 | strict: false, 22 | }, 23 | }, 24 | }; 25 | `; 26 | -------------------------------------------------------------------------------- /lib/templates/js/instructions.js: -------------------------------------------------------------------------------- 1 | export default () => ` 2 | In this directory use: 3 | + "npm install" to install its dependencies 4 | + "npm start" to start fastify using fastify-cli 5 | + "npm run dev" to start fastify using fastify-cli with logging to the console 6 | + "npm test" to run tests 7 | 8 | note: the auto generated test scaffolding does not contain any data yet ! 9 | `; 10 | -------------------------------------------------------------------------------- /lib/templates/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mypackage", 3 | "description": "no description for this package yet", 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "fastify start --options index.js", 8 | "test": "c8 node --test test/test-*.js", 9 | "dev": "fastify start -l info -P --options index.js" 10 | }, 11 | "directories": { 12 | "test": "test" 13 | }, 14 | "dependencies": { 15 | "fastify-plugin": "" 16 | }, 17 | "devDependencies": { 18 | "fastify": "", 19 | "fastify-cli": "", 20 | "c8": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/templates/js/projectData.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | import { join, relative } from "node:path"; 3 | import generateReadme from "./README.md.js"; 4 | import generatePlugin from "./index.js"; 5 | import generateInstructions from "./instructions.js"; 6 | import generateSecurity from "./security.js"; 7 | // manifest.js for javascript generation 8 | import generateService from "./service.js"; 9 | import generateTest from "./test-plugin.js"; 10 | 11 | const importJSON = createRequire(import.meta.url); 12 | const pkgTemplate = await importJSON("./package.json"); 13 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 14 | 15 | const contents = (dir, fileName, data) => { 16 | return { 17 | path: join(dir, fileName), 18 | data, 19 | fileName, 20 | }; 21 | }; 22 | 23 | function pathLocal(from, file) { 24 | // on windows the double backslash needs escaping to ensure it correctly shows up in printing 25 | return join(relative(from, localFile("..")), file).replace(/\\/g, "/"); 26 | } 27 | 28 | export function generateProjectData(data) { 29 | const { project, projectDir, testDir, config, specification, localPlugin } = 30 | data; 31 | const specFile = "openApi.json"; 32 | const serviceFile = "service.js"; 33 | const securityFile = "security.js"; 34 | const plugin = "index.js"; 35 | const pluginPackageName = localPlugin 36 | ? pathLocal(projectDir, plugin) 37 | : data.pluginPackageName; 38 | const instructions = generateInstructions(); 39 | const files = { 40 | spec: contents( 41 | projectDir, 42 | specFile, 43 | JSON.stringify(specification, null, "\t"), 44 | ), 45 | service: contents(projectDir, serviceFile, generateService(config)), 46 | security: contents(projectDir, securityFile, generateSecurity(config)), 47 | plugin: contents( 48 | projectDir, 49 | plugin, 50 | generatePlugin({ 51 | specFile, 52 | serviceFile, 53 | securityFile, 54 | pluginPackageName, 55 | }), 56 | ), 57 | readme: contents( 58 | projectDir, 59 | "README.md", 60 | generateReadme(project, instructions), 61 | ), 62 | testPlugin: contents( 63 | testDir, 64 | "test-plugin.js", 65 | generateTest(Object.assign(config, { specFile, serviceFile, plugin })), 66 | ), 67 | }; 68 | return { files, pkgTemplate, instructions }; 69 | } 70 | -------------------------------------------------------------------------------- /lib/templates/js/security.js: -------------------------------------------------------------------------------- 1 | export default (data) => { 2 | const securitySchemes = data.securitySchemes 3 | ? Object.entries(data.securitySchemes) 4 | : []; 5 | 6 | return `// implementation of the security schemes in the openapi specification 7 | 8 | export class Security { 9 | async initialize(schemes) { 10 | // schemes will contain securitySchemes as found in the openapi specification 11 | console.log("Initialize:", JSON.stringify(schemes)); 12 | } 13 | ${securitySchemes 14 | .map( 15 | ([schemeKey, schemeVal]) => ` 16 | // Security scheme: ${schemeKey} 17 | // Type: ${schemeVal.type} 18 | async ${schemeKey}(req, reply, params) { 19 | console.log("${schemeKey}: Authenticating request"); 20 | // If validation fails: throw new Error('Could not authenticate request') 21 | // Else, simply return. 22 | 23 | // The request object can also be mutated here (e.g. to set 'req.user') 24 | }`, 25 | ) 26 | .join("\n")} 27 | } 28 | `; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/templates/js/service.js: -------------------------------------------------------------------------------- 1 | import { comments } from "../templateUtils.js"; 2 | 3 | export default ( 4 | data, 5 | ) => `// implementation of the operations in the openapi specification 6 | 7 | export class Service { 8 | ${data.routes 9 | .map( 10 | (route) => ` 11 | // Operation: ${route.operationId} 12 | // URL: ${route.url} 13 | // summary: ${route.schema.summary} 14 | ${comments(route, 1)} 15 | async ${route.operationId}(req, reply) { 16 | console.log("${route.operationId}", req.params); 17 | return { key: "value" }; 18 | }`, 19 | ) 20 | .join("\n")} 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /lib/templates/js/test-plugin.js: -------------------------------------------------------------------------------- 1 | import { comments } from "../templateUtils.js"; 2 | 3 | export default (data) => 4 | `// this file contains a test harness that was auto-generated by fastify-openapi-glue 5 | // running the tests directly after generation will probably fail as the parameters 6 | // need to be manually added. 7 | 8 | import { strict as assert } from "node:assert/strict"; 9 | import { test } from "node:test"; 10 | import Fastify from "fastify"; 11 | import fastifyPlugin from "../${data.plugin}"; 12 | import service from "../${data.serviceFile}"; 13 | 14 | const specification = "../${data.specFile}"; 15 | 16 | const opts = { 17 | specification, 18 | service, 19 | }; 20 | //${data.routes 21 | .map( 22 | (route) => ` 23 | // Operation: ${route.operationId} 24 | // URL: ${route.url} 25 | // summary: ${route.schema.summary} 26 | ${comments(route, 0)} 27 | test("testing ${route.operationId}", async (t) => { 28 | const fastify = Fastify(); 29 | fastify.register(fastifyPlugin, opts); 30 | 31 | const res = await fastify.inject({ 32 | method: "${route.method}", 33 | url: "${data.prefix ? data.prefix : ""}${route.url}", 34 | payload: undefined,${route.schema.body ? " //insert body data here!!" : ""} 35 | headers: undefined,${route.schema.headers ? " //insert headers here!!" : ""} 36 | }); 37 | assert.equal(res.statusCode, 200); 38 | });`, 39 | ) 40 | .join("\n")} 41 | `; 42 | -------------------------------------------------------------------------------- /lib/templates/standaloneJS/README.md.js: -------------------------------------------------------------------------------- 1 | export default (projectName, instructions) => `# ${projectName} 2 | 3 | This directory contains a fastify plugin that was autogenerated using 4 | [fastify-openapi-glue](https://github.com/seriousme/fastify-openapi-glue) and 5 | the OpenApi specifation in [openApi.json](openApi.json) 6 | ${instructions} 7 | `; 8 | -------------------------------------------------------------------------------- /lib/templates/standaloneJS/index.js: -------------------------------------------------------------------------------- 1 | export default ( 2 | data, 3 | ) => `// Fastify plugin autogenerated by fastify-openapi-glue 4 | 5 | import fastifyPlugin from "fastify-plugin"; 6 | import { Security } from "./${data.securityFile}"; 7 | import { SecurityError } from "./${data.securityHandlers}"; 8 | import { Service } from "./${data.serviceFile}"; 9 | 10 | function notImplemented(operationId) { 11 | return async () => { 12 | throw new Error(\`Operation \${operationId} not implemented\`); 13 | }; 14 | } 15 | 16 | function buildHandler(serviceHandlers, operationId) { 17 | if (operationId in serviceHandlers) { 18 | return serviceHandlers[operationId]; 19 | } 20 | return notImplemented(operationId); 21 | } 22 | ${ 23 | hasSecuritySchemes(data.config.routes) 24 | ? ` 25 | function buildPreHandler(securityHandlers, schemes) { 26 | if (schemes.length === 0) { 27 | return async () => { }; 28 | } 29 | return async (req, reply) => { 30 | const handlerErrors = []; 31 | const schemeList = []; 32 | let statusCode = 401; 33 | for (const scheme of schemes) { 34 | try { 35 | await securityHandlers[scheme.name](req, reply, scheme.parameters); 36 | return; // If one security check passes, no need to try any others 37 | } catch (err) { 38 | req.log.debug(\`Security handler '\${scheme.name}' failed: '\${err}'\`); 39 | handlerErrors.push(err); 40 | if (err.statusCode !== undefined) { 41 | statusCode = err.statusCode; 42 | } 43 | } 44 | schemeList.push(scheme.name); 45 | } 46 | // if we get this far no security handlers validated this request 47 | throw new SecurityError( 48 | \`None of the security schemes (\${schemeList.join(", ")}) successfully authenticated this request.\`, 49 | statusCode, 50 | "Unauthorized", 51 | handlerErrors, 52 | ); 53 | }; 54 | }` 55 | : "" 56 | } 57 | 58 | export default fastifyPlugin( 59 | async (instance, opts) => { 60 | instance.register(generateRoutes, { prefix: "${data.config.prefix || ""}" }) 61 | }, { fastify: '^4.x' }) 62 | 63 | async function generateRoutes(fastify, opts) { 64 | const service = new Service(); 65 | const security = new Security(); 66 | ${data.config.routes 67 | .map( 68 | (item) => 69 | ` fastify.route({ 70 | method: "${item.method}", 71 | url: "${item.url}", 72 | schema: ${optimizeSchema(item.schema)}, 73 | ${ 74 | item.config 75 | ? `config: ${item.config}, 76 | ` 77 | : "" 78 | }handler: buildHandler(service, "${item.operationId}").bind(Service)${ 79 | item.security 80 | ? `, 81 | prehandler: buildPreHandler(security, ${JSON.stringify(item.security)}).bind(Security) 82 | ` 83 | : "" 84 | } 85 | }); 86 | `, 87 | ) 88 | .join("\n")} 89 | } 90 | 91 | export const options = { 92 | ajv: { 93 | customOptions: { 94 | strict: false, 95 | }, 96 | }, 97 | }; 98 | `; 99 | 100 | function hasSecuritySchemes(routes) { 101 | return routes.filter((route) => route.security !== undefined).length > 0; 102 | } 103 | 104 | function optimizeSchema(schema, indent = 3) { 105 | const prefix = `${"\t".repeat(indent)}`; 106 | const schemaKeys = ["body", "query", "querystring", "params", "reponse"]; 107 | const newSchema = {}; 108 | for (const key of schemaKeys) { 109 | if (schema[key] !== undefined) { 110 | newSchema[key] = schema[key]; 111 | } 112 | } 113 | return JSON.stringify(newSchema, null, "\t"); 114 | } 115 | -------------------------------------------------------------------------------- /lib/templates/standaloneJS/instructions.js: -------------------------------------------------------------------------------- 1 | export default () => ` 2 | In this directory use: 3 | + "npm install" to install its dependencies 4 | + "npm start" to start fastify using fastify-cli 5 | + "npm run dev" to start fastify using fastify-cli with logging to the console 6 | + "npm test" to run tests 7 | 8 | note: the auto generated test scaffolding does not contain any data yet ! 9 | `; 10 | -------------------------------------------------------------------------------- /lib/templates/standaloneJS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mypackage", 3 | "description": "no description for this package yet", 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "fastify start --options index.js", 8 | "test": "c8 node --test test/test-*.js", 9 | "dev": "fastify start -l info -P --options index.js" 10 | }, 11 | "directories": { 12 | "test": "test" 13 | }, 14 | "dependencies": { 15 | "fastify-plugin": "" 16 | }, 17 | "devDependencies": { 18 | "fastify": "", 19 | "fastify-cli": "", 20 | "c8": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/templates/standaloneJS/projectData.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { createRequire } from "node:module"; 3 | import { join, relative } from "node:path"; 4 | import generateReadme from "./README.md.js"; 5 | import generatePlugin from "./index.js"; 6 | import generateInstructions from "./instructions.js"; 7 | import generateSecurity from "./security.js"; 8 | // manifest.js for javascript generation 9 | import generateService from "./service.js"; 10 | import generateTest from "./test-plugin.js"; 11 | 12 | const importJSON = createRequire(import.meta.url); 13 | const pkgTemplate = await importJSON("./package.json"); 14 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 15 | 16 | const contents = (dir, fileName, data) => { 17 | return { 18 | path: join(dir, fileName), 19 | data, 20 | fileName, 21 | }; 22 | }; 23 | 24 | function pathLocal(from, file) { 25 | // on windows the double backslash needs escaping to ensure it correctly shows up in printing 26 | return join(relative(from, localFile("..")), file).replace(/\\/g, "/"); 27 | } 28 | 29 | export function generateProjectData(data) { 30 | const { project, projectDir, testDir, config, specification, localPlugin } = 31 | data; 32 | const specFile = "openApi.json"; 33 | const serviceFile = "service.js"; 34 | const securityFile = "security.js"; 35 | const plugin = "index.js"; 36 | const securityHandlers = "securityHandlers.js"; 37 | const securityHandlersTemplate = readFileSync( 38 | localFile(`../../${securityHandlers}`), 39 | ); 40 | const pluginPackageName = localPlugin 41 | ? pathLocal(projectDir, plugin) 42 | : data.pluginPackageName; 43 | const instructions = generateInstructions(); 44 | const files = { 45 | spec: contents( 46 | projectDir, 47 | specFile, 48 | JSON.stringify(specification, null, 2), 49 | ), 50 | service: contents(projectDir, serviceFile, generateService(config)), 51 | security: contents(projectDir, securityFile, generateSecurity(config)), 52 | plugin: contents( 53 | projectDir, 54 | plugin, 55 | generatePlugin({ 56 | specFile, 57 | serviceFile, 58 | securityFile, 59 | securityHandlers, 60 | pluginPackageName, 61 | config, 62 | }), 63 | ), 64 | readme: contents( 65 | projectDir, 66 | "README.md", 67 | generateReadme(project, instructions), 68 | ), 69 | securityHandlers: contents( 70 | projectDir, 71 | securityHandlers, 72 | securityHandlersTemplate, 73 | ), 74 | testPlugin: contents( 75 | testDir, 76 | "test-plugin.js", 77 | generateTest(Object.assign(config, { specFile, serviceFile, plugin })), 78 | ), 79 | }; 80 | return { files, pkgTemplate, instructions }; 81 | } 82 | -------------------------------------------------------------------------------- /lib/templates/standaloneJS/security.js: -------------------------------------------------------------------------------- 1 | export default (data) => { 2 | const securitySchemes = data.securitySchemes 3 | ? Object.entries(data.securitySchemes) 4 | : []; 5 | 6 | return `// implementation of the security schemes in the openapi specification 7 | 8 | export class Security { 9 | async initialize(schemes) { 10 | // schemes will contain securitySchemes as found in the openapi specification 11 | console.log("Initialize:", JSON.stringify(schemes)); 12 | } 13 | ${securitySchemes 14 | .map( 15 | ([schemeKey, schemeVal]) => ` 16 | // Security scheme: ${schemeKey} 17 | // Type: ${schemeVal.type} 18 | async ${schemeKey}(req, reply, params) { 19 | console.log("${schemeKey}: Authenticating request"); 20 | // If validation fails: throw new Error('Could not authenticate request') 21 | // Else, simply return. 22 | 23 | // The request object can also be mutated here (e.g. to set 'req.user') 24 | }`, 25 | ) 26 | .join("\n")} 27 | } 28 | `; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/templates/standaloneJS/service.js: -------------------------------------------------------------------------------- 1 | import { comments } from "../templateUtils.js"; 2 | 3 | export default ( 4 | data, 5 | ) => `// implementation of the operations in the openapi specification 6 | 7 | export class Service { 8 | ${data.routes 9 | .map( 10 | (route) => ` 11 | // Operation: ${route.operationId} 12 | // URL: ${route.url} 13 | // summary: ${route.schema.summary} 14 | ${comments(route, 1)} 15 | async ${route.operationId}(req, reply) { 16 | console.log("${route.operationId}", req.params); 17 | return { key: "value" }; 18 | }`, 19 | ) 20 | .join("\n")} 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /lib/templates/standaloneJS/test-plugin.js: -------------------------------------------------------------------------------- 1 | import { comments } from "../templateUtils.js"; 2 | 3 | export default (data) => 4 | `// this file contains a test harness that was auto-generated by fastify-openapi-glue 5 | // running the tests directly after generation will probably fail as the parameters 6 | // need to be manually added. 7 | 8 | import { strict as assert } from "node:assert/strict"; 9 | import { test } from "node:test"; 10 | import Fastify from "fastify"; 11 | import fastifyPlugin from "../${data.plugin}"; 12 | import { options } from "../${data.plugin}"; 13 | 14 | const opts = {}; 15 | //${data.routes 16 | .map( 17 | (route) => ` 18 | // Operation: ${route.operationId} 19 | // URL: ${route.url} 20 | // summary: ${route.schema.summary} 21 | ${comments(route, 0)} 22 | test("testing ${route.operationId}", async (t) => { 23 | const fastify = Fastify(options); 24 | fastify.register(fastifyPlugin, opts); 25 | 26 | const res = await fastify.inject({ 27 | method: "${route.method}", 28 | url: "${data.prefix ? data.prefix : ""}${route.url}", 29 | payload: undefined,${route.schema.body ? " //insert body data here!!" : ""} 30 | headers: undefined,${route.schema.headers ? " //insert headers here!!" : ""} 31 | }); 32 | assert.equal(res.statusCode, 200); 33 | });`, 34 | ) 35 | .join("\n")} 36 | `; 37 | -------------------------------------------------------------------------------- /lib/templates/templateTypes.js: -------------------------------------------------------------------------------- 1 | const templateInfo = { 2 | javascript: { 3 | name: "javascript", 4 | path: "js", 5 | }, 6 | standaloneJS: { 7 | name: "standaloneJS", 8 | path: "standaloneJS", 9 | }, 10 | }; 11 | 12 | const templateTypes = Object.keys(templateInfo); 13 | 14 | export { templateInfo, templateTypes }; 15 | -------------------------------------------------------------------------------- /lib/templates/templateUtils.js: -------------------------------------------------------------------------------- 1 | import { dump } from "js-yaml"; 2 | 3 | export function comments(route, indent = 0) { 4 | const prefix = `${"\t".repeat(indent)}//`; 5 | const items = { 6 | "req.headers": route.schema.headers, 7 | "req.params": route.schema.params, 8 | "req.query": route.schema.querystring, 9 | "req.body": route.schema.body, 10 | "valid responses": route.openapiSource.responses, 11 | }; 12 | 13 | const commentize = (label) => { 14 | const data = items[label]; 15 | if (!data) { 16 | return ""; 17 | } 18 | const dataStrings = dump(data).split("\n"); 19 | return `${prefix} ${label} 20 | ${dataStrings 21 | .map((item) => (item.length > 0 ? `${prefix} ${item}` : prefix)) 22 | .join("\n")} 23 | `; 24 | }; 25 | 26 | return Object.keys(items).reduce((acc, label) => acc + commentize(label), ""); 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-openapi-glue", 3 | "version": "4.9.1", 4 | "description": "generate a fastify configuration from an openapi specification", 5 | "main": "index.js", 6 | "type": "module", 7 | "engines": { 8 | "node": ">=14.0.0" 9 | }, 10 | "types": "index.d.ts", 11 | "scripts": { 12 | "start": "fastify start --options examples/petstore/index.js", 13 | "format": "biome format --write .", 14 | "test": "c8 node --test test/test-*.js && biome format --write . && biome ci .", 15 | "posttest": "c8 check-coverage --lines 100 --functions 100 --branches 100", 16 | "covtest": "c8 --reporter=lcov npm test", 17 | "lint": "biome ci .", 18 | "dev": "fastify start -l info -P examples/petstore/index.js", 19 | "updateChecksums": "node test/update-checksums.js", 20 | "preversion": "npm test && git add examples/generated-*-project/package.json", 21 | "postversion": "git push && git push --tags" 22 | }, 23 | "author": "Hans Klunder", 24 | "license": "MIT", 25 | "bin": { 26 | "openapi-glue": "./bin/openapi-glue-cli.js" 27 | }, 28 | "dependencies": { 29 | "@seriousme/openapi-schema-validator": "^2.4.1", 30 | "fastify-plugin": "^5.0.1", 31 | "js-yaml": "^4.1.0" 32 | }, 33 | "directories": { 34 | "example": "./examples", 35 | "test": "./test", 36 | "lib": "./examples", 37 | "bin": "./bin" 38 | }, 39 | "devDependencies": { 40 | "@biomejs/biome": "^1.9.4", 41 | "c8": "^10.1.3", 42 | "fastify": "^5.3.2", 43 | "fastify-cli": "^7.4.0" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/seriousme/fastify-openapi-glue.git" 48 | }, 49 | "keywords": [ 50 | "fastify", 51 | "swagger", 52 | "openapi", 53 | "generator" 54 | ], 55 | "bugs": { 56 | "url": "https://github.com/seriousme/fastify-openapi-glue/issues" 57 | }, 58 | "homepage": "https://github.com/seriousme/fastify-openapi-glue#readme", 59 | "exports": { 60 | "import": "./index.js", 61 | "default": "./index.js" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/security.js: -------------------------------------------------------------------------------- 1 | class Security { 2 | async goodAuthCheck() { 3 | // Do nothing--auth check succeeds! 4 | } 5 | 6 | async failingAuthCheck() { 7 | throw "API key was invalid or not found"; 8 | } 9 | 10 | async failingAuthCheckCustomStatusCode() { 11 | const err = new Error("API key was invalid or not found"); 12 | err.statusCode = 451; 13 | throw err; 14 | } 15 | } 16 | 17 | export default new Security(); 18 | -------------------------------------------------------------------------------- /test/service.js: -------------------------------------------------------------------------------- 1 | // ES6 Module implementation of the operations in the openapi specification 2 | 3 | export class Service { 4 | // Operation: getPathParam 5 | // summary: Test path parameters 6 | // req.params: 7 | // type: object 8 | // properties: 9 | // id: 10 | // type: integer 11 | // required: 12 | // - id 13 | // 14 | // valid responses: 15 | // '200': 16 | // description: ok 17 | // 18 | 19 | async getPathParam(req) { 20 | if (typeof req.params.id !== "number") { 21 | throw new Error("req.params.id is not a number"); 22 | } 23 | return ""; 24 | } 25 | 26 | // Operation: getQueryParam 27 | // summary: Test query parameters 28 | // req.query: 29 | // type: object 30 | // properties: 31 | // int1: 32 | // type: integer 33 | // int2: 34 | // type: integer 35 | // 36 | // valid responses: 37 | // '200': 38 | // description: ok 39 | // 40 | 41 | async getQueryParam(req) { 42 | if ( 43 | typeof req.query.int1 !== "number" || 44 | typeof req.query.int2 !== "number" 45 | ) { 46 | throw new Error("req.params.int1 or req.params.int2 is not a number"); 47 | } 48 | return ""; 49 | } 50 | 51 | // Operation: getQueryParamObject 52 | // summary: Test query parameters 53 | // req.query: 54 | // type: object 55 | // properties: 56 | // int1: 57 | // type: integer 58 | // int2: 59 | // type: integer 60 | // 61 | // valid responses: 62 | // '200': 63 | // description: ok 64 | // 65 | 66 | async getQueryParamObject(req) { 67 | if ( 68 | typeof req.query.int1 !== "number" || 69 | typeof req.query.int2 !== "number" 70 | ) { 71 | throw new Error("req.params.int1 or req.params.int2 is not a number"); 72 | } 73 | return ""; 74 | } 75 | 76 | // Operation: getQueryParamArray 77 | // summary: Test query parameters 78 | // req.query: 79 | // type: object 80 | // properties: 81 | // arr: 82 | // type: array 83 | // items: 84 | // type: integer 85 | // 86 | // valid responses: 87 | // '200': 88 | // description: ok 89 | // 90 | 91 | async getQueryParamArray(req) { 92 | if ( 93 | typeof req.query.arr[0] !== "number" || 94 | typeof req.query.arr[1] !== "number" 95 | ) { 96 | throw new Error("req.params[0] or req.params[1] is not a number"); 97 | } 98 | return ""; 99 | } 100 | 101 | // Operation: getHeaderParam 102 | // summary: Test header parameters 103 | // req.headers: 104 | // type: object 105 | // properties: 106 | // X-Request-ID: 107 | // type: string 108 | // 109 | // valid responses: 110 | // '200': 111 | // description: ok 112 | // 113 | 114 | async getHeaderParam(req) { 115 | if (typeof req.headers["x-request-id"] !== "string") { 116 | throw new Error("req.header['x-request-id'] is not a string"); 117 | } 118 | return ""; 119 | } 120 | 121 | // Operation: getAuthHeaderParam 122 | // summary: Test authorization header parameters 123 | // req.headers: 124 | // type: object 125 | // properties: 126 | // authorization: 127 | // type: string 128 | // 129 | // valid responses: 130 | // '200': 131 | // description: ok 132 | // 133 | 134 | async getAuthHeaderParam(req) { 135 | if (typeof req.headers["authorization"] !== "string") { 136 | throw new Error("req.header['authorization'] is not a string"); 137 | } 138 | return ""; 139 | } 140 | 141 | // Operation: getNoParam 142 | // summary: Test no parameters 143 | // 144 | // valid responses: 145 | // '200': 146 | // description: ok 147 | // 148 | 149 | async getNoParam() { 150 | return ""; 151 | } 152 | 153 | // Operation: postBodyParam 154 | // summary: Test body parameters 155 | // req.body: 156 | // type: string 157 | // 158 | // valid responses: 159 | // '200': 160 | // description: ok 161 | // 162 | 163 | async postBodyParam(req) { 164 | if (typeof req.body.str1 !== "string") { 165 | throw new Error("req.body.str1 is not a string"); 166 | } 167 | return ""; 168 | } 169 | 170 | // Operation: getResponse 171 | // summary: Test response serialization 172 | // req.query: 173 | // type: object 174 | // properties: 175 | // respType: 176 | // type: string 177 | // 178 | // valid responses: 179 | // '200': 180 | // description: ok 181 | // schema: 182 | // type: object 183 | // properties: 184 | // response: 185 | // type: string 186 | // required: 187 | // - response 188 | // 189 | 190 | async getResponse(req) { 191 | if (req.query.replyType === "valid") { 192 | return { response: "test data" }; 193 | } 194 | return { invalid: 1 }; 195 | } 196 | 197 | // Operation: testOperationSecurity 198 | // summary: Test response serialization 199 | // req.query: 200 | // type: object 201 | // properties: 202 | // respType: 203 | // type: string 204 | // 205 | // valid responses: 206 | // '200': 207 | // description: ok 208 | // schema: 209 | // type: object 210 | // properties: 211 | // response: 212 | // type: string 213 | // required: 214 | // - response 215 | // 216 | 217 | async testOperationSecurity(req) { 218 | return { 219 | response: req.scope || "authentication succeeded!", 220 | }; 221 | } 222 | 223 | // Operation: testOperationSecurity 224 | // summary: Test response serialization 225 | // req.query: 226 | // type: object 227 | // properties: 228 | // respType: 229 | // type: string 230 | // 231 | // valid responses: 232 | // '200': 233 | // description: ok 234 | // schema: 235 | // type: object 236 | // properties: 237 | // response: 238 | // type: string 239 | // required: 240 | // - response 241 | // 242 | 243 | async testOperationSecurityUsingAnd(req) { 244 | return { 245 | response: "Authentication succeeded!", 246 | }; 247 | } 248 | 249 | // Operation: testOperationSecurityWithParameter 250 | // summary: Test response serialization 251 | // req.query: 252 | // type: object 253 | // properties: 254 | // respType: 255 | // type: string 256 | // 257 | // valid responses: 258 | // '200': 259 | // description: ok 260 | // schema: 261 | // type: object 262 | // properties: 263 | // response: 264 | // type: string 265 | // required: 266 | // - response 267 | // 268 | 269 | async testOperationSecurityWithParameter(req) { 270 | return { 271 | response: req.scope || "authentication succeeded!", 272 | }; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /test/service.multipleMimeTypes.js: -------------------------------------------------------------------------------- 1 | // ES6 Module implementation of the operations in the openapi specification 2 | 3 | export class Service { 4 | async postMultipleBodyMimeTypes(req) { 5 | if (req.body.str1 !== "string" && req.body.int1 !== 2) { 6 | throw new Error( 7 | "req.body.str1 is not a string or req.body.int1 is not 2", 8 | ); 9 | } 10 | return ""; 11 | } 12 | async getMultipleResponseMimeTypes(req, reply) { 13 | if (req.query.responseType === "application/json") { 14 | reply.type("application/json"); 15 | return { str1: "test data" }; 16 | } 17 | reply.type("text/json"); 18 | return { int1: 2 }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/test-cli.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { execSync } from "node:child_process"; 3 | import { createRequire } from "node:module"; 4 | import { test } from "node:test"; 5 | import { URL, fileURLToPath } from "node:url"; 6 | import { templateTypes } from "../lib/templates/templateTypes.js"; 7 | const importJSON = createRequire(import.meta.url); 8 | const spec = "./test-swagger.v2"; 9 | const cli = localFile("../bin/openapi-glue-cli.js"); 10 | 11 | // if you need new checksums (e.g. because you changed template or spec file) 12 | // run `npm run updateChecksums` 13 | 14 | function localFile(fileName) { 15 | return fileURLToPath(new URL(fileName, import.meta.url)); 16 | } 17 | 18 | for (const type of templateTypes) { 19 | const specPath = localFile(`${spec}.json`); 20 | const checksumFile = localFile(`${spec}.${type}.checksums.json`); 21 | const project = `generated-${type}-project`; 22 | const testChecksums = await importJSON(checksumFile); 23 | await test(`cli ${type} does not error`, (t) => { 24 | const checksums = JSON.parse( 25 | execSync(`node ${cli} -c -p ${project} -t ${type} ${specPath}`), 26 | ); 27 | assert.deepEqual(checksums, testChecksums, "checksums match"); 28 | }); 29 | 30 | await test("cli with local plugin", (t) => { 31 | const result = execSync( 32 | `node ${cli} -c -l -p ${project} -t ${type} ${specPath}`, 33 | ); 34 | assert.ok(result); 35 | }); 36 | } 37 | 38 | test("cli fails on no spec", (t) => { 39 | assert.throws(() => execSync(`node ${cli}`)); 40 | }); 41 | 42 | test("cli fails on invalid projectType", (t) => { 43 | assert.throws(() => 44 | execSync(`node ${cli} -c -l ${spec}.json -t nonExistent`), 45 | ); 46 | }); 47 | 48 | test("cli fails on invalid spec", (t) => { 49 | assert.throws(() => execSync(`node ${cli} -c nonExistingSpec.json`)); 50 | }); 51 | -------------------------------------------------------------------------------- /test/test-cookie-param.v3.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import fastifyOpenapiGlue from "../index.js"; 6 | const importJSON = createRequire(import.meta.url); 7 | 8 | const testSpec = await importJSON("./test-openapi-v3-cookie-param.json"); 9 | 10 | import { Service } from "./service.js"; 11 | const serviceHandlers = new Service(); 12 | 13 | const noStrict = { 14 | ajv: { 15 | customOptions: { 16 | strict: false, 17 | }, 18 | }, 19 | }; 20 | 21 | test("route registration succeeds with cookie param", (t, done) => { 22 | const opts = { 23 | specification: testSpec, 24 | serviceHandlers, 25 | }; 26 | 27 | const fastify = Fastify(noStrict); 28 | fastify.register(fastifyOpenapiGlue, opts); 29 | fastify.ready((err) => { 30 | if (err) { 31 | assert.fail("got unexpected error"); 32 | } else { 33 | assert.ok(true, "no unexpected error"); 34 | done(); 35 | } 36 | }); 37 | }); 38 | 39 | test("route registration inserts cookie schema", (t, done) => { 40 | const opts = { 41 | specification: testSpec, 42 | serviceHandlers, 43 | addCookieSchema: true, 44 | }; 45 | 46 | const fastify = Fastify(noStrict); 47 | // Register onRoute handler which will be called when the plugin registers routes in the specification. 48 | let hadCookieSchema = false; 49 | fastify.addHook("onRoute", (routeOptions) => { 50 | const schema = routeOptions.schema; 51 | if (schema.operationId === "getCookieParam") { 52 | hadCookieSchema = 53 | schema?.cookies && 54 | typeof schema?.cookies?.properties?.cookieValue === "object"; 55 | } 56 | }); 57 | fastify.register(fastifyOpenapiGlue, opts); 58 | fastify.ready((err) => { 59 | // Our onRoute handler above should have been invoked already and should have found the cookie schema we asked for (with 'addCookieSchema' option). 60 | if (err) { 61 | assert.fail("got unexpected error"); 62 | } else if (hadCookieSchema) { 63 | assert.ok(true, "no unexpected error"); 64 | done(); 65 | } else { 66 | assert.fail("cookie schema not found"); 67 | } 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/test-custom-route-options.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import fastifyOpenapiGlue from "../index.js"; 6 | 7 | const importJSON = createRequire(import.meta.url); 8 | 9 | const testSpec = await importJSON("./test-openapi.v3.json"); 10 | 11 | test("return route params from operationResolver", async (t) => { 12 | const fastify = Fastify(); 13 | fastify.register(fastifyOpenapiGlue, { 14 | specification: testSpec, 15 | operationResolver: () => { 16 | return { 17 | onSend: async (req, res) => { 18 | res.code(304); 19 | return null; 20 | }, 21 | handler: async () => { 22 | return { hello: "world" }; 23 | }, 24 | }; 25 | }, 26 | }); 27 | 28 | const res = await fastify.inject({ 29 | method: "GET", 30 | url: "/queryParamObject?int1=1&int2=2", 31 | }); 32 | assert.equal(res.statusCode, 304); 33 | }); 34 | 35 | test("operationResolver route params overwrite default params", async (t) => { 36 | const fastify = Fastify(); 37 | fastify.register(fastifyOpenapiGlue, { 38 | specification: testSpec, 39 | operationResolver: () => { 40 | return { 41 | config: { foo: "bar" }, 42 | handler: async (req) => { 43 | return req.routeOptions.config; 44 | }, 45 | }; 46 | }, 47 | }); 48 | 49 | const res = await fastify.inject({ 50 | method: "GET", 51 | url: "/queryParamObject?int1=1&int2=2", 52 | }); 53 | assert.equal(res.statusCode, 200); 54 | assert.equal(JSON.parse(res.body)?.foo, "bar"); 55 | }); 56 | 57 | test("throw an error if handler is not specified", async (t) => { 58 | const fastify = Fastify(); 59 | fastify.register(fastifyOpenapiGlue, { 60 | specification: testSpec, 61 | operationResolver: () => ({}), 62 | }); 63 | 64 | const res = await fastify.inject({ 65 | method: "GET", 66 | url: "/queryParamObject?int1=1&int2=2", 67 | }); 68 | assert.equal(res.statusCode, 500); 69 | const parsedBody = JSON.parse(res.body); 70 | assert.equal(parsedBody?.statusCode, 500); 71 | assert.equal(parsedBody?.error, "Internal Server Error"); 72 | assert.equal( 73 | parsedBody?.message, 74 | "Operation getQueryParamObject not implemented", 75 | ); 76 | }); 77 | -------------------------------------------------------------------------------- /test/test-debuglogging.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | // just test the basics to aid debugging 4 | import { test } from "node:test"; 5 | import Fastify from "fastify"; 6 | import fastifyOpenapiGlue from "../index.js"; 7 | const importJSON = createRequire(import.meta.url); 8 | import { Writable } from "node:stream"; 9 | 10 | const testSpec = await importJSON("./test-openapi.v3.json"); 11 | import { Service } from "./service.js"; 12 | const serviceHandlers = new Service(); 13 | import securityHandlers from "./security.js"; 14 | 15 | class DebugCatcher { 16 | constructor() { 17 | this.data = []; 18 | } 19 | stream() { 20 | const that = this; 21 | return new Writable({ 22 | write(chunk, encoding, callback) { 23 | that.data.push(chunk.toString("utf8")); 24 | callback(); 25 | }, 26 | }); 27 | } 28 | } 29 | 30 | const missingMethods = (service, methodSet) => { 31 | const proto = Object.getPrototypeOf(service); 32 | const notPresent = (item) => 33 | typeof service[item] === "function" && 34 | item.match(/^(get|post|test)/) && 35 | !methodSet.has(item); 36 | return Object.getOwnPropertyNames(proto).some(notPresent); 37 | }; 38 | 39 | test("Service registration is logged at level 'debug'", async (t) => { 40 | const catcher = new DebugCatcher(); 41 | const opts = { 42 | specification: testSpec, 43 | serviceHandlers, 44 | }; 45 | const fastify = Fastify({ 46 | logger: { 47 | level: "debug", 48 | stream: catcher.stream(), 49 | }, 50 | }); 51 | fastify.register(fastifyOpenapiGlue, opts); 52 | const res = await fastify.inject({ 53 | method: "get", 54 | url: "/noParam", 55 | }); 56 | assert.equal(res.statusCode, 200, "result is ok"); 57 | const operations = new Set(); 58 | for await (const data of catcher.data) { 59 | const match = data.match(/"msg":"serviceHandlers has '(\w+)'"/); 60 | if (match !== null) { 61 | operations.add(match[1]); 62 | } 63 | } 64 | assert.equal( 65 | missingMethods(serviceHandlers, operations), 66 | false, 67 | "all operations are present in the debug log", 68 | ); 69 | }); 70 | 71 | test("Error from invalid securityHandler is logged at level 'debug' ", async (t) => { 72 | const catcher = new DebugCatcher(); 73 | const opts = { 74 | specification: testSpec, 75 | serviceHandlers, 76 | securityHandlers: { 77 | api_key: securityHandlers.failingAuthCheck, 78 | skipped: securityHandlers.goodAuthCheck, 79 | failing: securityHandlers.failingAuthCheck, 80 | }, 81 | }; 82 | const fastify = Fastify({ 83 | logger: { 84 | level: "debug", 85 | stream: catcher.stream(), 86 | }, 87 | }); 88 | fastify.register(fastifyOpenapiGlue, opts); 89 | const res = await fastify.inject({ 90 | method: "GET", 91 | url: "/operationSecurity", 92 | }); 93 | assert.equal(res.statusCode, 200, "request succeeded"); 94 | const handlers = new Set(); 95 | for await (const data of catcher.data) { 96 | const match = data.match( 97 | /Security handler 'api_key' failed: 'API key was invalid or not found'/, 98 | ); 99 | if (match !== null) { 100 | handlers.add(match[0]); 101 | } 102 | } 103 | assert.equal( 104 | handlers.has( 105 | "Security handler 'api_key' failed: 'API key was invalid or not found'", 106 | ), 107 | true, 108 | "securityHandler error is present in the debug log", 109 | ); 110 | }); 111 | -------------------------------------------------------------------------------- /test/test-fastify-cli.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { after, test } from "node:test"; 3 | // test fastify-cli as used by the npm start script 4 | import { URL, fileURLToPath } from "node:url"; 5 | import { build } from "fastify-cli/helper.js"; 6 | 7 | function localFile(fileName) { 8 | return fileURLToPath(new URL(fileName, import.meta.url)); 9 | } 10 | 11 | test("test fastify-cli with petstore example", async (t) => { 12 | const fastifyCli = await build([ 13 | "--options", 14 | localFile("../examples/petstore/index.js"), 15 | ]); 16 | after(() => fastifyCli.close()); 17 | const res = await fastifyCli.inject({ 18 | method: "GET", 19 | url: "v2/pet/24", 20 | }); 21 | assert.equal(res.statusCode, 200); 22 | assert.deepEqual(JSON.parse(res.body), { 23 | id: 24, 24 | name: "Kitty the cat", 25 | photoUrls: [ 26 | "https://en.wikipedia.org/wiki/Cat#/media/File:Kittyply_edit1.jpg", 27 | ], 28 | status: "available", 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/test-fastify-recursive.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { test } from "node:test"; 3 | import Fastify from "fastify"; 4 | 5 | const opts = { 6 | schema: { 7 | body: { 8 | $id: "https://example.com/tree", 9 | type: "object", 10 | additionalProperties: false, 11 | properties: { 12 | str1: { 13 | type: "string", 14 | }, 15 | obj1: { 16 | $ref: "#", 17 | }, 18 | }, 19 | required: ["str1"], 20 | }, 21 | }, 22 | }; 23 | 24 | test("fastify validation works", async (t) => { 25 | const fastify = Fastify(); 26 | 27 | async function routes(fastify) { 28 | fastify.post("/", opts, async (request) => { 29 | assert.deepEqual( 30 | request.body, 31 | { str1: "test data", obj1: { str1: "test data" } }, 32 | "expected value", 33 | ); 34 | return; 35 | }); 36 | } 37 | fastify.register(routes); 38 | { 39 | const res = await fastify.inject({ 40 | method: "POST", 41 | url: "/", 42 | payload: { 43 | str1: "test data", 44 | obj1: { 45 | str1: "test data", 46 | }, 47 | }, 48 | }); 49 | assert.equal(res.statusCode, 200, "expected HTTP code"); 50 | } 51 | { 52 | const res = await fastify.inject({ 53 | method: "GET", 54 | url: "/blah", 55 | }); 56 | assert.equal(res.statusCode, 404, "expected HTTP code"); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /test/test-fastify.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | // just test the basics to aid debugging 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | 6 | const opts = { 7 | schema: { 8 | querystring: { 9 | type: "object", 10 | properties: { 11 | hello: { type: "string" }, 12 | }, 13 | required: ["hello"], 14 | }, 15 | }, 16 | }; 17 | 18 | test("basic fastify works", async (t) => { 19 | const fastify = Fastify(); 20 | 21 | async function routes(fastify) { 22 | fastify.get("/", async () => { 23 | return { hello: "world" }; 24 | }); 25 | } 26 | fastify.register(routes); 27 | const res = await fastify.inject({ 28 | method: "GET", 29 | url: "/", 30 | }); 31 | assert.equal(res.statusCode, 200); 32 | }); 33 | 34 | test("fastify validation works", async (t) => { 35 | const fastify = Fastify(); 36 | 37 | async function routes(fastify) { 38 | fastify.get("/", opts, async (request) => { 39 | return { hello: request.query.hello }; 40 | }); 41 | } 42 | fastify.register(routes); 43 | { 44 | const res = await fastify.inject({ 45 | method: "GET", 46 | url: "/?hello=world", 47 | }); 48 | assert.equal(res.body, '{"hello":"world"}', "expected value"); 49 | assert.equal(res.statusCode, 200, "expected HTTP code"); 50 | } 51 | { 52 | const res = await fastify.inject({ 53 | method: "GET", 54 | url: "/?ello=world", 55 | }); 56 | assert.equal(res.statusCode, 400, "expected HTTP code"); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /test/test-generate-project.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import { Generator } from "../lib/generator.js"; 5 | import { templateTypes } from "../lib/templates/templateTypes.js"; 6 | const importJSON = createRequire(import.meta.url); 7 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 8 | 9 | const specPath = localFile("./petstore-swagger.v2.json"); 10 | const specPath3 = localFile("./petstore-openapi.v3.json"); 11 | const projectName = `generated-${templateTypes[0]}-project`; 12 | const dir = localFile("../examples"); 13 | const nonExistentDir = localFile("./non-existent-directory"); 14 | const spec301 = await importJSON(specPath3); 15 | 16 | const checksumOnly = false; 17 | const localPlugin = true; 18 | const noLocalPlugin = false; 19 | 20 | const localGenerator = new Generator(checksumOnly, localPlugin); 21 | const generator = new Generator(checksumOnly, noLocalPlugin); 22 | 23 | test("generator generates V3.0.0 project without error", async (t) => { 24 | try { 25 | await generator.parse(specPath3); 26 | await generator.generateProject(dir, projectName); 27 | assert.ok(true, "no error occurred"); 28 | } catch (e) { 29 | assert.fail(e.message); 30 | } 31 | }); 32 | 33 | test("generator generates V3.0.1 project without error", async (t) => { 34 | spec301["openapi"] = "3.0.1"; 35 | 36 | try { 37 | await generator.parse(spec301); 38 | await generator.generateProject(dir, projectName); 39 | assert.ok(true, "no error occurred"); 40 | } catch (e) { 41 | assert.fail(e.message); 42 | } 43 | }); 44 | 45 | test("generator generates project with local plugin without error", async (t) => { 46 | try { 47 | await localGenerator.parse(specPath); 48 | await localGenerator.generateProject(dir, projectName); 49 | assert.ok(true, "no error occurred"); 50 | } catch (e) { 51 | assert.fail(e.message); 52 | } 53 | }); 54 | 55 | test("generator throws error on non-existent basedir", async (t) => { 56 | try { 57 | await generator.parse(specPath); 58 | await generator.generateProject(nonExistentDir, projectName); 59 | assert.fail("no error occurred"); 60 | } catch (e) { 61 | assert.equal(e.code, "ENOENT", "got expected error"); 62 | } 63 | }); 64 | 65 | // this one needs to be last 66 | 67 | for (const type of templateTypes) { 68 | const project = `generated-${type}-project`; 69 | const generator = new Generator(checksumOnly, localPlugin); 70 | await test(`generator generates ${type} project without error`, async (t) => { 71 | try { 72 | await generator.parse(specPath); 73 | await generator.generateProject(dir, project, type); 74 | assert.ok(true, "no error occurred"); 75 | } catch (e) { 76 | assert.fail(e.message); 77 | } 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/test-generator.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import { Generator } from "../lib/generator.js"; 5 | import { templateTypes } from "../lib/templates/templateTypes.js"; 6 | const importJSON = createRequire(import.meta.url); 7 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 8 | const dir = localFile("."); 9 | const checksumOnly = true; 10 | const localPlugin = false; 11 | 12 | // if you need new checksums (e.g. because you changed template or spec file) 13 | // run `npm run updateChecksums` 14 | const specs = new Set(["./test-swagger.v2", "./test-swagger-noBasePath.v2"]); 15 | for (const type of templateTypes) { 16 | for (const spec of specs) { 17 | const specFile = localFile(`${spec}.json`); 18 | const checksumFile = localFile(`${spec}.${type}.checksums.json`); 19 | const testChecksums = await importJSON(checksumFile); 20 | const project = `generated-${type}-project`; 21 | const generator = new Generator(checksumOnly, localPlugin); 22 | await test(`generator generates ${type} project data for ${spec}`, async (t) => { 23 | try { 24 | await generator.parse(specFile); 25 | const checksums = await generator.generateProject(dir, project, type); 26 | assert.deepEqual(checksums, testChecksums, "checksums match"); 27 | } catch (e) { 28 | assert.fail(e.message); 29 | } 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/test-import-plugin.cjs: -------------------------------------------------------------------------------- 1 | const { test } = require("node:test"); 2 | 3 | test("import in CommonJS works", async (t) => { 4 | const openapiGlue = await import("../index.js"); 5 | assert.equal(openapiGlue.fastifyOpenapiGlue !== undefined, true); 6 | }); 7 | -------------------------------------------------------------------------------- /test/test-import-plugin.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { test } from "node:test"; 3 | import { fastifyOpenapiGlue } from "../index.js"; 4 | import openapiGlue from "../index.js"; 5 | 6 | test("named import in ESM works", async (t) => { 7 | assert.equal(fastifyOpenapiGlue.fastifyOpenapiGlue !== undefined, true); 8 | }); 9 | 10 | test("default import in ESM works", async (t) => { 11 | assert.equal(openapiGlue.fastifyOpenapiGlue !== undefined, true); 12 | }); 13 | -------------------------------------------------------------------------------- /test/test-openapi-v3-cookie-param.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "servers": [ 4 | { 5 | "url": "http://localhost/v2" 6 | } 7 | ], 8 | "info": { 9 | "title": "Test specification", 10 | "description": "testing the fastify openapi glue", 11 | "version": "0.1.0" 12 | }, 13 | "paths": { 14 | "/cookieParam": { 15 | "get": { 16 | "operationId": "getCookieParam", 17 | "summary": "Test cookie parameters", 18 | "parameters": [ 19 | { 20 | "in": "cookie", 21 | "name": "cookieValue", 22 | "schema": { 23 | "type": "string" 24 | } 25 | } 26 | ], 27 | "responses": { 28 | "200": { 29 | "description": "ok" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/test-openapi-v3-generic-path-items.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "servers": [ 4 | { 5 | "url": "http://localhost/v2" 6 | } 7 | ], 8 | "info": { 9 | "title": "Test specification", 10 | "description": "testing the fastify openapi glue", 11 | "version": "0.1.0" 12 | }, 13 | "paths": { 14 | "/pathParam/{id}": { 15 | "parameters": [ 16 | { 17 | "name": "id", 18 | "in": "path", 19 | "required": true, 20 | "schema": { 21 | "type": "integer" 22 | } 23 | } 24 | ], 25 | "get": { 26 | "operationId": "getPathParam", 27 | "summary": "Test path parameters", 28 | "responses": { 29 | "200": { 30 | "description": "ok" 31 | } 32 | } 33 | } 34 | }, 35 | "/noParam": { 36 | "parameters": [ 37 | { 38 | "name": "id", 39 | "in": "query", 40 | "required": true, 41 | "schema": { 42 | "type": "integer" 43 | } 44 | } 45 | ], 46 | "get": { 47 | "operationId": "getNoParam", 48 | "summary": "Test path parameters", 49 | "parameters": [ 50 | { 51 | "name": "id", 52 | "in": "query", 53 | "schema": { 54 | "type": "integer" 55 | } 56 | } 57 | ], 58 | "responses": { 59 | "200": { 60 | "description": "ok" 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/test-openapi-v3-recursive.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "servers": [ 4 | { 5 | "url": "http://localhost/v2" 6 | } 7 | ], 8 | "info": { 9 | "title": "Recursive Test specification", 10 | "description": "testing the fastify openapi glue using recursive schema", 11 | "version": "0.1.0" 12 | }, 13 | "paths": { 14 | "/recursive": { 15 | "post": { 16 | "operationId": "postRecursive", 17 | "summary": "Test recursive parameters", 18 | "responses": { 19 | "200": { 20 | "description": "ok", 21 | "content": { 22 | "application/json": { 23 | "schema": { 24 | "$ref": "#/components/schemas/bodyObject" 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "requestBody": { 31 | "content": { 32 | "application/json": { 33 | "schema": { 34 | "$ref": "#/components/schemas/bodyObject" 35 | } 36 | } 37 | }, 38 | "required": true 39 | } 40 | } 41 | } 42 | }, 43 | 44 | "components": { 45 | "schemas": { 46 | "bodyObject": { 47 | "type": "object", 48 | "additionalProperties": false, 49 | "properties": { 50 | "str1": { 51 | "type": "string" 52 | }, 53 | "arr": { 54 | "type": "array", 55 | "items": { 56 | "type": "string" 57 | } 58 | }, 59 | "objRef": { 60 | "$ref": "#/components/schemas/bodyObject" 61 | }, 62 | "arrRef": { 63 | "type": "array", 64 | "items": { 65 | "$ref": "#/components/schemas/bodyObject" 66 | } 67 | }, 68 | "refStr": { 69 | "$ref": "#/components/schemas/bodyObject/properties/arrRef/items/properties/arr/items" 70 | } 71 | }, 72 | "required": ["str1"] 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/test-openapi.v3.content.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "servers": [ 4 | { 5 | "url": "http://localhost/v2" 6 | } 7 | ], 8 | "info": { 9 | "title": "Test specification", 10 | "description": "testing the fastify openapi glue", 11 | "version": "0.1.0" 12 | }, 13 | "security": [ 14 | { 15 | "skipped": [] 16 | } 17 | ], 18 | "paths": { 19 | "/queryParamObjectInContent": { 20 | "get": { 21 | "operationId": "getQueryParamObject", 22 | "summary": "Test query parameters in an object", 23 | "parameters": [ 24 | { 25 | "in": "query", 26 | "name": "obj", 27 | "content": { 28 | "application/json": { 29 | "schema": { 30 | "type": "object", 31 | "properties": { 32 | "int1": { 33 | "type": "integer" 34 | }, 35 | "int2": { 36 | "type": "integer" 37 | } 38 | }, 39 | "required": ["int1", "int2"] 40 | } 41 | } 42 | } 43 | } 44 | ], 45 | "responses": { 46 | "200": { 47 | "description": "ok" 48 | } 49 | }, 50 | "x-tap-ok": true 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/test-openapi.v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "servers": [ 4 | { 5 | "url": "http://localhost/v2" 6 | } 7 | ], 8 | "info": { 9 | "title": "Test specification", 10 | "description": "testing the fastify openapi glue", 11 | "version": "0.1.0" 12 | }, 13 | "security": [ 14 | { 15 | "skipped": [] 16 | } 17 | ], 18 | "paths": { 19 | "/pathParam/{id}": { 20 | "get": { 21 | "operationId": "getPathParam", 22 | "summary": "Test path parameters", 23 | "parameters": [ 24 | { 25 | "name": "id", 26 | "in": "path", 27 | "required": true, 28 | "schema": { 29 | "type": "integer" 30 | } 31 | } 32 | ], 33 | "responses": { 34 | "200": { 35 | "description": "ok" 36 | } 37 | } 38 | } 39 | }, 40 | "/queryParam": { 41 | "get": { 42 | "operationId": "getQueryParam", 43 | "summary": "Test query parameters", 44 | "parameters": [ 45 | { 46 | "in": "query", 47 | "name": "int1", 48 | "schema": { 49 | "type": "integer" 50 | }, 51 | "required": true 52 | }, 53 | { 54 | "in": "query", 55 | "name": "int2", 56 | "schema": { 57 | "type": "integer" 58 | }, 59 | "required": true 60 | } 61 | ], 62 | "responses": { 63 | "200": { 64 | "description": "ok" 65 | } 66 | }, 67 | "x-tap-ok": true 68 | } 69 | }, 70 | "/queryParamObject": { 71 | "get": { 72 | "operationId": "getQueryParamObject", 73 | "summary": "Test query parameters in an object", 74 | "parameters": [ 75 | { 76 | "in": "query", 77 | "name": "obj", 78 | "schema": { 79 | "type": "object", 80 | "properties": { 81 | "int1": { 82 | "type": "integer" 83 | }, 84 | "int2": { 85 | "type": "integer" 86 | } 87 | }, 88 | "required": ["int1", "int2"] 89 | }, 90 | "required": true 91 | } 92 | ], 93 | "responses": { 94 | "200": { 95 | "description": "ok" 96 | } 97 | }, 98 | "x-tap-ok": true 99 | } 100 | }, 101 | "/queryParamArray": { 102 | "get": { 103 | "operationId": "getQueryParamArray", 104 | "summary": "Test query parameters in an array", 105 | "parameters": [ 106 | { 107 | "in": "query", 108 | "name": "arr", 109 | "schema": { 110 | "type": "array", 111 | "items": { 112 | "type": "integer" 113 | } 114 | }, 115 | "required": true 116 | } 117 | ], 118 | "responses": { 119 | "200": { 120 | "description": "ok" 121 | } 122 | }, 123 | "x-tap-ok": true 124 | } 125 | }, 126 | "/headerParam": { 127 | "get": { 128 | "operationId": "getHeaderParam", 129 | "summary": "Test header parameters", 130 | "parameters": [ 131 | { 132 | "in": "header", 133 | "name": "X-Request-ID", 134 | "schema": { 135 | "type": "string" 136 | } 137 | } 138 | ], 139 | "responses": { 140 | "200": { 141 | "description": "ok" 142 | } 143 | } 144 | } 145 | }, 146 | "/authHeaderParam": { 147 | "get": { 148 | "operationId": "getAuthHeaderParam", 149 | "summary": "Test authorization header parameters", 150 | "parameters": [ 151 | { 152 | "in": "header", 153 | "name": "authorization", 154 | "schema": { 155 | "type": "string" 156 | } 157 | } 158 | ], 159 | "responses": { 160 | "200": { 161 | "description": "ok" 162 | } 163 | } 164 | } 165 | }, 166 | "/bodyParam": { 167 | "post": { 168 | "operationId": "postBodyParam", 169 | "summary": "Test body parameters", 170 | "responses": { 171 | "200": { 172 | "description": "ok" 173 | } 174 | }, 175 | "requestBody": { 176 | "content": { 177 | "application/json": { 178 | "schema": { 179 | "$ref": "#/components/schemas/bodyObject" 180 | } 181 | } 182 | }, 183 | "required": true 184 | } 185 | } 186 | }, 187 | "/bodyParamWithoutContent": { 188 | "post": { 189 | "operationId": "postBodyParam", 190 | "summary": "Test body parameters", 191 | "responses": { 192 | "200": { 193 | "description": "ok" 194 | } 195 | }, 196 | "requestBody": { 197 | "content": {}, 198 | "required": true 199 | } 200 | } 201 | }, 202 | "/noParam": { 203 | "get": { 204 | "operationId": "getNoParam", 205 | "summary": "Test without parameters", 206 | "responses": { 207 | "200": { 208 | "description": "ok" 209 | } 210 | } 211 | } 212 | }, 213 | "/noOperationId/{id}": { 214 | "get": { 215 | "summary": "Test missing operationid", 216 | "parameters": [ 217 | { 218 | "name": "id", 219 | "in": "path", 220 | "required": true, 221 | "schema": { 222 | "type": "integer" 223 | } 224 | } 225 | ], 226 | "responses": { 227 | "200": { 228 | "description": "ok", 229 | "content": { 230 | "application/json": { 231 | "schema": { 232 | "$ref": "#/components/schemas/responseObject" 233 | } 234 | } 235 | } 236 | } 237 | } 238 | } 239 | }, 240 | "/responses": { 241 | "get": { 242 | "operationId": "getResponse", 243 | "summary": "Test response serialization", 244 | "parameters": [ 245 | { 246 | "in": "query", 247 | "name": "replyType", 248 | "schema": { 249 | "type": "string" 250 | } 251 | } 252 | ], 253 | "responses": { 254 | "200": { 255 | "description": "ok", 256 | "content": { 257 | "application/json": { 258 | "schema": { 259 | "$ref": "#/components/schemas/responseObject" 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | }, 267 | "/operationSecurity": { 268 | "get": { 269 | "operationId": "testOperationSecurity", 270 | "summary": "Test security handling OR functionality", 271 | "security": [ 272 | { 273 | "api_key": [] 274 | }, 275 | { 276 | "skipped": [] 277 | }, 278 | { 279 | "failing": [] 280 | } 281 | ], 282 | "responses": { 283 | "200": { 284 | "description": "ok", 285 | "content": { 286 | "application/json": { 287 | "schema": { 288 | "$ref": "#/components/schemas/responseObject" 289 | } 290 | } 291 | } 292 | }, 293 | "401": { 294 | "description": "unauthorized", 295 | "content": { 296 | "application/json": { 297 | "schema": { 298 | "$ref": "#/components/schemas/errorObject" 299 | } 300 | } 301 | } 302 | } 303 | } 304 | } 305 | }, 306 | "/operationSecurityUsingAnd": { 307 | "get": { 308 | "operationId": "testOperationSecurityUsingAnd", 309 | "summary": "Test security handling AND functionality", 310 | "security": [ 311 | { 312 | "api_key": ["and"], 313 | "skipped": ["works"] 314 | } 315 | ], 316 | "responses": { 317 | "200": { 318 | "description": "ok", 319 | "content": { 320 | "application/json": { 321 | "schema": { 322 | "$ref": "#/components/schemas/responseObject" 323 | } 324 | } 325 | } 326 | }, 327 | "401": { 328 | "description": "unauthorized", 329 | "content": { 330 | "application/json": { 331 | "schema": { 332 | "$ref": "#/components/schemas/errorObject" 333 | } 334 | } 335 | } 336 | } 337 | } 338 | } 339 | }, 340 | "/operationSecurityWithParameter": { 341 | "get": { 342 | "operationId": "testOperationSecurityWithParameter", 343 | "summary": "Test security handling", 344 | "security": [ 345 | { 346 | "api_key": [] 347 | }, 348 | { 349 | "skipped": ["skipped"] 350 | }, 351 | { 352 | "failing": [] 353 | } 354 | ], 355 | "responses": { 356 | "200": { 357 | "description": "ok", 358 | "content": { 359 | "application/json": { 360 | "schema": { 361 | "$ref": "#/components/schemas/responseObject" 362 | } 363 | } 364 | } 365 | }, 366 | "401": { 367 | "description": "unauthorized", 368 | "content": { 369 | "application/json": { 370 | "schema": { 371 | "$ref": "#/components/schemas/errorObject" 372 | } 373 | } 374 | } 375 | } 376 | } 377 | } 378 | }, 379 | "/operationSecurityEmptyHandler": { 380 | "get": { 381 | "operationId": "testOperationSecurity", 382 | "summary": "Test security handling", 383 | "security": [ 384 | {}, 385 | { 386 | "failing": [] 387 | } 388 | ], 389 | "responses": { 390 | "200": { 391 | "description": "ok", 392 | "content": { 393 | "application/json": { 394 | "schema": { 395 | "$ref": "#/components/schemas/responseObject" 396 | } 397 | } 398 | } 399 | }, 400 | "401": { 401 | "description": "unauthorized", 402 | "content": { 403 | "application/json": { 404 | "schema": { 405 | "$ref": "#/components/schemas/errorObject" 406 | } 407 | } 408 | } 409 | } 410 | } 411 | } 412 | }, 413 | "/operationSecurityOverrideWithNoSecurity": { 414 | "get": { 415 | "operationId": "testOperationSecurity", 416 | "summary": "Test security handling", 417 | "security": [], 418 | "responses": { 419 | "200": { 420 | "description": "ok", 421 | "content": { 422 | "application/json": { 423 | "schema": { 424 | "$ref": "#/components/schemas/responseObject" 425 | } 426 | } 427 | } 428 | } 429 | } 430 | } 431 | }, 432 | "/operationWithFastifyConfigExtension": { 433 | "get": { 434 | "operationId": "operationWithFastifyConfigExtension", 435 | "summary": "Test fastify config extension", 436 | "x-fastify-config": { 437 | "rawBody": true 438 | }, 439 | "responses": { 440 | "200": { 441 | "description": "ok", 442 | "content": { 443 | "application/json": { 444 | "schema": { 445 | "$ref": "#/components/schemas/responseObject" 446 | } 447 | } 448 | } 449 | } 450 | } 451 | } 452 | }, 453 | "/ignoreRoute": { 454 | "get": { 455 | "operationId": "ignoreRoute", 456 | "summary": "Test route correclty being ignored", 457 | "x-no-fastify-config": true, 458 | "responses": { 459 | "200": { 460 | "description": "ok" 461 | } 462 | } 463 | } 464 | }, 465 | "/emptyBodySchema": { 466 | "get": { 467 | "operationId": "emptyBodySchema", 468 | "summary": "Empty body schema", 469 | "responses": { 470 | "204": { 471 | "description": "Empty" 472 | }, 473 | "302": { 474 | "description": "Empty" 475 | } 476 | } 477 | } 478 | } 479 | }, 480 | "components": { 481 | "schemas": { 482 | "bodyObject": { 483 | "type": "object", 484 | "additionalProperties": false, 485 | "properties": { 486 | "str1": { 487 | "type": "string" 488 | } 489 | }, 490 | "required": ["str1"] 491 | }, 492 | "responseObject": { 493 | "type": "object", 494 | "properties": { 495 | "response": { 496 | "type": "string" 497 | } 498 | }, 499 | "required": ["response"] 500 | }, 501 | "errorObject": { 502 | "type": "object", 503 | "properties": { 504 | "error": { 505 | "type": "string" 506 | }, 507 | "statusCode": { 508 | "type": "integer" 509 | } 510 | } 511 | } 512 | }, 513 | "securitySchemes": { 514 | "api_key": { 515 | "type": "apiKey", 516 | "in": "header", 517 | "name": "X-API-Key" 518 | }, 519 | "skipped": { 520 | "type": "http", 521 | "scheme": "basic" 522 | } 523 | } 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /test/test-openapi.v3.multipleMimeTypes.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.1", 3 | "servers": [ 4 | { 5 | "url": "http://localhost/v2" 6 | } 7 | ], 8 | "info": { 9 | "title": "Test specification", 10 | "description": "testing the fastify openapi glue", 11 | "version": "0.1.0" 12 | }, 13 | "security": [ 14 | { 15 | "skipped": [] 16 | } 17 | ], 18 | "paths": { 19 | "/postMultipleBodyMimeTypes": { 20 | "post": { 21 | "operationId": "postMultipleBodyMimeTypes", 22 | "summary": "Test multiple Mime types in requestBody", 23 | "requestBody": { 24 | "content": { 25 | "application/json": { 26 | "schema": { "$ref": "#/components/schemas/bodyObjectString" } 27 | }, 28 | "text/json": { 29 | "schema": { "$ref": "#/components/schemas/bodyObjectInt" } 30 | } 31 | } 32 | }, 33 | "responses": { 34 | "200": { 35 | "description": "ok" 36 | } 37 | }, 38 | "x-tap-ok": true 39 | } 40 | }, 41 | "/getMultipleResponseMimeTypes": { 42 | "get": { 43 | "operationId": "getMultipleResponseMimeTypes", 44 | "summary": "Test multiple Mime types in responses", 45 | "parameters": [ 46 | { 47 | "in": "query", 48 | "name": "responseType", 49 | "schema": { 50 | "type": "string" 51 | }, 52 | "required": true 53 | } 54 | ], 55 | "responses": { 56 | "200": { 57 | "description": "ok", 58 | "content": { 59 | "application/json": { 60 | "schema": { "$ref": "#/components/schemas/bodyObjectString" } 61 | }, 62 | "text/json": { 63 | "schema": { "$ref": "#/components/schemas/bodyObjectInt" } 64 | } 65 | } 66 | } 67 | }, 68 | "x-tap-ok": true 69 | } 70 | } 71 | }, 72 | "components": { 73 | "schemas": { 74 | "bodyObjectString": { 75 | "type": "object", 76 | "additionalProperties": false, 77 | "properties": { 78 | "str1": { 79 | "type": "string" 80 | } 81 | }, 82 | "required": ["str1"] 83 | }, 84 | "bodyObjectInt": { 85 | "type": "object", 86 | "additionalProperties": false, 87 | "properties": { 88 | "int1": { 89 | "type": "integer" 90 | } 91 | }, 92 | "required": ["int1"] 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/test-openapi.v3.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | servers: 3 | - url: 'http://localhost/v2' 4 | info: 5 | title: Test specification 6 | description: testing the fastify openapi glue 7 | version: 0.1.0 8 | paths: 9 | '/pathParam/{id}': 10 | get: 11 | operationId: getPathParam 12 | summary: Test path parameters 13 | parameters: 14 | - name: id 15 | in: path 16 | required: true 17 | schema: 18 | type: integer 19 | responses: 20 | '200': 21 | description: ok 22 | /queryParam: 23 | get: 24 | operationId: getQueryParam 25 | summary: Test query parameters 26 | parameters: 27 | - in: query 28 | name: int1 29 | schema: 30 | type: integer 31 | - in: query 32 | name: int2 33 | schema: 34 | type: integer 35 | responses: 36 | '200': 37 | description: ok 38 | /headerParam: 39 | get: 40 | operationId: getHeaderParam 41 | summary: Test header parameters 42 | parameters: 43 | - in: header 44 | name: X-Request-ID 45 | schema: 46 | type: string 47 | responses: 48 | '200': 49 | description: ok 50 | /bodyParam: 51 | post: 52 | operationId: postBodyParam 53 | summary: Test body parameters 54 | responses: 55 | '200': 56 | description: ok 57 | requestBody: 58 | content: 59 | application/json: 60 | schema: 61 | $ref: '#/components/schemas/bodyObject' 62 | required: true 63 | '/noOperationId/{id}': 64 | get: 65 | summary: Test missing operationid 66 | parameters: 67 | - name: id 68 | in: path 69 | required: true 70 | schema: 71 | type: integer 72 | responses: 73 | '200': 74 | description: ok 75 | content: 76 | application/json: 77 | schema: 78 | $ref: '#/components/schemas/responseObject' 79 | /responses: 80 | get: 81 | operationId: getResponse 82 | summary: Test response serialization 83 | parameters: 84 | - in: query 85 | name: replyType 86 | schema: 87 | type: string 88 | responses: 89 | '200': 90 | description: ok 91 | content: 92 | application/json: 93 | schema: 94 | $ref: '#/components/schemas/responseObject' 95 | components: 96 | schemas: 97 | bodyObject: 98 | type: object 99 | properties: 100 | str1: 101 | type: string 102 | format: custom-format 103 | required: 104 | - str1 105 | responseObject: 106 | type: object 107 | properties: 108 | response: 109 | type: string 110 | required: 111 | - response -------------------------------------------------------------------------------- /test/test-parser-base.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | // just test the basics to aid debugging 3 | import { test } from "node:test"; 4 | import { ParserBase } from "../lib/ParserBase.js"; 5 | 6 | test("generation of operationId works", (t) => { 7 | const pb = new ParserBase(); 8 | assert.equal( 9 | pb.makeOperationId("get", "/user/{name}"), 10 | "getUserByName", 11 | "get /user/{name} becomes getUserByName", 12 | ); 13 | }); 14 | -------------------------------------------------------------------------------- /test/test-petstore-example.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | // this suite tests the examples shown in README.md 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import petstoreExample, { options } from "../examples/petstore/index.js"; 6 | 7 | test("/v2/pet/24 works", async (t) => { 8 | const fastify = Fastify(options); 9 | fastify.register(petstoreExample, {}); 10 | const res = await fastify.inject({ 11 | method: "GET", 12 | url: "v2/pet/24", 13 | }); 14 | assert.equal(res.statusCode, 200); 15 | assert.deepEqual(JSON.parse(res.body), { 16 | id: 24, 17 | name: "Kitty the cat", 18 | photoUrls: [ 19 | "https://en.wikipedia.org/wiki/Cat#/media/File:Kittyply_edit1.jpg", 20 | ], 21 | status: "available", 22 | }); 23 | }); 24 | 25 | test("/v2/pet/myPet returns Fastify validation error", async (t) => { 26 | const fastify = Fastify(options); 27 | fastify.register(petstoreExample, {}); 28 | const res = await fastify.inject({ 29 | method: "GET", 30 | url: "v2/pet/myPet", 31 | }); 32 | assert.equal(res.statusCode, 400); 33 | const parsedBody = JSON.parse(res.body); 34 | assert.equal(parsedBody.statusCode, 400); 35 | assert.equal(parsedBody.error, "Bad Request"); 36 | assert.equal(parsedBody.message, "params/petId must be integer"); 37 | }); 38 | 39 | test("v2/pet/findByStatus?status=available&status=pending returns 'not implemented'", async (t) => { 40 | const fastify = Fastify(options); 41 | fastify.register(petstoreExample, {}); 42 | const res = await fastify.inject({ 43 | method: "GET", 44 | url: "v2/pet/findByStatus?status=available&status=pending", 45 | }); 46 | assert.equal(res.statusCode, 500); 47 | const parsedBody = JSON.parse(res.body); 48 | assert.equal(parsedBody.statusCode, 500); 49 | assert.equal(parsedBody.error, "Internal Server Error"); 50 | assert.equal( 51 | parsedBody.message, 52 | "Operation findPetsByStatus not implemented", 53 | ); 54 | }); 55 | 56 | test("v2/pet/0 returns serialization error", async (t) => { 57 | const fastify = Fastify(options); 58 | fastify.register(petstoreExample, {}); 59 | const res = await fastify.inject({ 60 | method: "GET", 61 | url: "v2/pet/0", 62 | }); 63 | assert.equal(res.statusCode, 500); 64 | const parsedBody = JSON.parse(res.body); 65 | assert.equal(parsedBody.statusCode, 500); 66 | assert.equal(parsedBody.error, "Internal Server Error"); 67 | assert.equal(parsedBody.message, '"name" is required!'); 68 | }); 69 | -------------------------------------------------------------------------------- /test/test-plugin.v2.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import fastifyOpenapiGlue from "../index.js"; 6 | const importJSON = createRequire(import.meta.url); 7 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 8 | 9 | const testSpec = await importJSON("./test-swagger.v2.json"); 10 | const petStoreSpec = await importJSON("./petstore-swagger.v2.json"); 11 | const testSpecYAML = localFile("./test-swagger.v2.yaml"); 12 | const genericPathItemsSpec = await importJSON( 13 | "./test-swagger-v2-generic-path-items.json", 14 | ); 15 | import { Service } from "./service.js"; 16 | const serviceHandlers = new Service(); 17 | 18 | const noStrict = { 19 | ajv: { 20 | customOptions: { 21 | strict: false, 22 | }, 23 | }, 24 | }; 25 | 26 | const opts = { 27 | specification: testSpec, 28 | serviceHandlers, 29 | }; 30 | 31 | const yamlOpts = { 32 | specification: testSpecYAML, 33 | serviceHandlers, 34 | }; 35 | 36 | const invalidSwaggerOpts = { 37 | specification: { valid: false }, 38 | serviceHandlers, 39 | }; 40 | 41 | const invalidServiceOpts = { 42 | specification: testSpecYAML, 43 | serviceHandlers: "wrong", 44 | }; 45 | 46 | const missingServiceOpts = { 47 | specification: testSpecYAML, 48 | serviceHandlers: localFile("./not-a-valid-service.js"), 49 | }; 50 | 51 | const genericPathItemsOpts = { 52 | specification: genericPathItemsSpec, 53 | serviceHandlers, 54 | }; 55 | 56 | process.on("warning", (warning) => { 57 | if (warning.name === "FastifyWarning") { 58 | throw new Error(`Fastify generated a warning: ${warning}`); 59 | } 60 | }); 61 | 62 | test("path parameters work", async (t) => { 63 | const fastify = Fastify(); 64 | fastify.register(fastifyOpenapiGlue, opts); 65 | 66 | const res = await fastify.inject({ 67 | method: "GET", 68 | url: "/v2/pathParam/2", 69 | }); 70 | assert.equal(res.statusCode, 200); 71 | }); 72 | 73 | test("query parameters work", async (t) => { 74 | const fastify = Fastify(); 75 | fastify.register(fastifyOpenapiGlue, opts); 76 | 77 | const res = await fastify.inject({ 78 | method: "GET", 79 | url: "/v2/queryParam?int1=1&int2=2", 80 | }); 81 | assert.equal(res.statusCode, 200); 82 | }); 83 | 84 | test("header parameters work", async (t) => { 85 | const fastify = Fastify(); 86 | fastify.register(fastifyOpenapiGlue, opts); 87 | 88 | const res = await fastify.inject({ 89 | method: "GET", 90 | url: "/v2/headerParam", 91 | headers: { 92 | "X-Request-ID": "test data", 93 | }, 94 | }); 95 | assert.equal(res.statusCode, 200); 96 | }); 97 | 98 | test("body parameters work", async (t) => { 99 | const fastify = Fastify(); 100 | fastify.register(fastifyOpenapiGlue, opts); 101 | 102 | const res = await fastify.inject({ 103 | method: "post", 104 | url: "/v2/bodyParam", 105 | payload: { str1: "test data" }, 106 | }); 107 | assert.equal(res.statusCode, 200); 108 | }); 109 | 110 | test("no parameters work", async (t) => { 111 | const fastify = Fastify(); 112 | fastify.register(fastifyOpenapiGlue, opts); 113 | 114 | const res = await fastify.inject({ 115 | method: "get", 116 | url: "/v2/noParam", 117 | }); 118 | assert.equal(res.statusCode, 200); 119 | }); 120 | 121 | test("missing operation from service returns error 500", async (t) => { 122 | const fastify = Fastify(); 123 | fastify.register(fastifyOpenapiGlue, opts); 124 | 125 | const res = await fastify.inject({ 126 | method: "get", 127 | url: "/v2/noOperationId/1", 128 | }); 129 | assert.equal(res.statusCode, 500); 130 | }); 131 | 132 | test("response schema works with valid response", async (t) => { 133 | const fastify = Fastify(); 134 | fastify.register(fastifyOpenapiGlue, opts); 135 | 136 | const res = await fastify.inject({ 137 | method: "get", 138 | url: "/v2/responses?replyType=valid", 139 | }); 140 | assert.equal(res.statusCode, 200); 141 | }); 142 | 143 | test("response schema works with invalid response", async (t) => { 144 | const fastify = Fastify(); 145 | fastify.register(fastifyOpenapiGlue, opts); 146 | 147 | const res = await fastify.inject({ 148 | method: "get", 149 | url: "/v2/responses?replyType=invalid", 150 | }); 151 | assert.equal(res.statusCode, 500); 152 | }); 153 | 154 | test("yaml spec works", async (t) => { 155 | const fastify = Fastify(); 156 | fastify.register(fastifyOpenapiGlue, yamlOpts); 157 | 158 | const res = await fastify.inject({ 159 | method: "GET", 160 | url: "/v2/pathParam/2", 161 | }); 162 | assert.equal(res.statusCode, 200); 163 | }); 164 | 165 | test("invalid openapi v2 specification throws error ", (t, done) => { 166 | const fastify = Fastify(); 167 | fastify.register(fastifyOpenapiGlue, invalidSwaggerOpts); 168 | fastify.ready((err) => { 169 | if (err) { 170 | assert.equal( 171 | err.message, 172 | "'specification' parameter must contain a valid version 2.0 or 3.0.x or 3.1.x specification", 173 | "got expected error", 174 | ); 175 | done(); 176 | } else { 177 | assert.fail("missed expected error"); 178 | } 179 | }); 180 | }); 181 | 182 | test("missing service definition throws error ", (t, done) => { 183 | const fastify = Fastify(); 184 | fastify.register(fastifyOpenapiGlue, invalidServiceOpts); 185 | fastify.ready((err) => { 186 | if (err) { 187 | assert.equal( 188 | err.message, 189 | "'serviceHandlers' parameter must refer to an object", 190 | "got expected error", 191 | ); 192 | done(); 193 | } else { 194 | assert.fail("missed expected error"); 195 | } 196 | }); 197 | }); 198 | 199 | test("invalid service definition throws error ", (t, done) => { 200 | const fastify = Fastify(); 201 | fastify.register(fastifyOpenapiGlue, missingServiceOpts); 202 | fastify.ready((err) => { 203 | if (err) { 204 | assert.equal( 205 | err.message, 206 | "'serviceHandlers' parameter must refer to an object", 207 | "got expected error", 208 | ); 209 | done(); 210 | } else { 211 | assert.fail("missed expected error"); 212 | } 213 | }); 214 | }); 215 | 216 | test("full pet store V2 definition does not throw error ", (t, done) => { 217 | const fastify = Fastify(noStrict); 218 | fastify.register(fastifyOpenapiGlue, { 219 | specification: structuredClone(petStoreSpec), 220 | serviceHandlers, 221 | }); 222 | fastify.ready((err) => { 223 | if (err) { 224 | assert.fail("got unexpected error"); 225 | } else { 226 | assert.ok(true, "no unexpected error"); 227 | done(); 228 | } 229 | }); 230 | }); 231 | 232 | test("x- props are copied", async (t) => { 233 | const fastify = Fastify(); 234 | fastify.addHook("preHandler", async (request, reply) => { 235 | if (request.routeOptions.schema["x-tap-ok"]) { 236 | assert.ok(true, "found x- prop"); 237 | } else { 238 | assert.fail("missing x- prop"); 239 | } 240 | }); 241 | fastify.register(fastifyOpenapiGlue, opts); 242 | 243 | const res = await fastify.inject({ 244 | method: "GET", 245 | url: "/v2/queryParam?int1=1&int2=2", 246 | }); 247 | assert.equal(res.statusCode, 200); 248 | }); 249 | 250 | test("x-fastify-config is applied", async (t) => { 251 | const fastify = Fastify(); 252 | fastify.register(fastifyOpenapiGlue, { 253 | ...opts, 254 | serviceHandlers: { 255 | operationWithFastifyConfigExtension: async (req, reply) => { 256 | assert.equal( 257 | req.routeOptions.config.rawBody, 258 | true, 259 | "config.rawBody is true", 260 | ); 261 | return; 262 | }, 263 | }, 264 | }); 265 | 266 | await fastify.inject({ 267 | method: "GET", 268 | url: "/v2/operationWithFastifyConfigExtension", 269 | }); 270 | }); 271 | 272 | test("generic path parameters work", async (t) => { 273 | const fastify = Fastify(); 274 | fastify.register(fastifyOpenapiGlue, genericPathItemsOpts); 275 | 276 | const res = await fastify.inject({ 277 | method: "GET", 278 | url: "/pathParam/2", 279 | }); 280 | assert.equal(res.statusCode, 200); 281 | }); 282 | 283 | test("generic path parameters override works", async (t) => { 284 | const fastify = Fastify(); 285 | fastify.register(fastifyOpenapiGlue, genericPathItemsOpts); 286 | 287 | const res = await fastify.inject({ 288 | method: "GET", 289 | url: "/noParam", 290 | }); 291 | assert.equal(res.statusCode, 200); 292 | }); 293 | 294 | test("schema attributes for non-body parameters work", async (t) => { 295 | const fastify = Fastify(noStrict); 296 | fastify.register(fastifyOpenapiGlue, { 297 | specification: structuredClone(petStoreSpec), 298 | serviceHandlers, 299 | }); 300 | const res = await fastify.inject({ 301 | method: "GET", 302 | url: "v2/store/order/11", 303 | }); 304 | assert.equal(res.statusCode, 400); 305 | const parsedBody = JSON.parse(res.body); 306 | assert.equal(parsedBody.statusCode, 400); 307 | assert.equal(parsedBody.error, "Bad Request"); 308 | assert.equal(parsedBody.message, "params/orderId must be <= 10"); 309 | }); 310 | 311 | test("create an empty body with addEmptySchema option", async (t) => { 312 | const fastify = Fastify(); 313 | 314 | let emptyBodySchemaFound = false; 315 | fastify.addHook("onRoute", (routeOptions) => { 316 | if (routeOptions.url === "/v2/emptyBodySchema") { 317 | assert.deepStrictEqual(routeOptions.schema.response?.["204"], {}); 318 | assert.deepStrictEqual(routeOptions.schema.response?.["302"], {}); 319 | emptyBodySchemaFound = true; 320 | } 321 | }); 322 | 323 | await fastify.register(fastifyOpenapiGlue, { 324 | specification: testSpec, 325 | serviceHandlers: new Set(), 326 | addEmptySchema: true, 327 | }); 328 | assert.ok(emptyBodySchemaFound); 329 | }); 330 | -------------------------------------------------------------------------------- /test/test-plugin.v3.content.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import fastifyOpenapiGlue from "../index.js"; 6 | 7 | const importJSON = createRequire(import.meta.url); 8 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 9 | 10 | const testSpec = await importJSON("./test-openapi.v3.content.json"); 11 | import { Service } from "./service.js"; 12 | const serviceHandlers = new Service(); 13 | 14 | const noStrict = { 15 | ajv: { 16 | customOptions: { 17 | strict: false, 18 | }, 19 | }, 20 | }; 21 | 22 | const opts = { 23 | specification: testSpec, 24 | serviceHandlers, 25 | }; 26 | 27 | process.on("warning", (warning) => { 28 | if (warning.name === "FastifyWarning") { 29 | throw new Error(`Fastify generated a warning: ${warning}`); 30 | } 31 | }); 32 | 33 | test("query parameters with object schema in content work", async (t) => { 34 | const fastify = Fastify(); 35 | fastify.register(fastifyOpenapiGlue, opts); 36 | 37 | const res = await fastify.inject({ 38 | method: "GET", 39 | url: "/queryParamObjectInContent?int1=1&int2=2", 40 | }); 41 | assert.equal(res.statusCode, 200); 42 | }); 43 | -------------------------------------------------------------------------------- /test/test-plugin.v3.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import fastifyOpenapiGlue from "../index.js"; 6 | 7 | const importJSON = createRequire(import.meta.url); 8 | const localFile = (fileName) => new URL(fileName, import.meta.url).pathname; 9 | 10 | const testSpec = await importJSON("./test-openapi.v3.json"); 11 | const petStoreSpec = await importJSON("./petstore-openapi.v3.json"); 12 | const testSpecYAML = localFile("./test-openapi.v3.yaml"); 13 | const genericPathItemsSpec = await importJSON( 14 | "./test-openapi-v3-generic-path-items.json", 15 | ); 16 | import { Service } from "./service.js"; 17 | const serviceHandlers = new Service(); 18 | 19 | const noStrict = { 20 | ajv: { 21 | customOptions: { 22 | strict: false, 23 | }, 24 | }, 25 | }; 26 | 27 | const opts = { 28 | specification: testSpec, 29 | serviceHandlers, 30 | }; 31 | 32 | const prefixOpts = { 33 | specification: testSpec, 34 | serviceHandlers, 35 | prefix: "prefix", 36 | }; 37 | 38 | const yamlOpts = { 39 | specification: testSpecYAML, 40 | serviceHandlers, 41 | }; 42 | 43 | const invalidSwaggerOpts = { 44 | specification: { valid: false }, 45 | serviceHandlers, 46 | }; 47 | 48 | const invalidServiceOpts = { 49 | specification: testSpecYAML, 50 | serviceHandlers: "wrong", 51 | }; 52 | 53 | const petStoreOpts = { 54 | specification: petStoreSpec, 55 | serviceHandlers, 56 | }; 57 | 58 | const genericPathItemsOpts = { 59 | specification: genericPathItemsSpec, 60 | serviceHandlers, 61 | }; 62 | 63 | const serviceAndOperationResolver = { 64 | specification: testSpec, 65 | serviceHandlers: localFile("./not-a-valid-service.js"), 66 | operationResolver() { 67 | return; 68 | }, 69 | }; 70 | 71 | const noServiceNoResolver = { 72 | specification: testSpec, 73 | }; 74 | 75 | const withOperationResolver = { 76 | specification: testSpec, 77 | operationResolver() { 78 | return async (req, reply) => { 79 | reply.send("ok"); 80 | }; 81 | }, 82 | }; 83 | 84 | const withOperationResolverUsingMethodPath = { 85 | specification: testSpec, 86 | operationResolver(_operationId, method) { 87 | const result = method === "GET" ? "ok" : "notOk"; 88 | return async (req, reply) => { 89 | reply.send(result); 90 | }; 91 | }, 92 | }; 93 | 94 | process.on("warning", (warning) => { 95 | if (warning.name === "FastifyWarning") { 96 | throw new Error(`Fastify generated a warning: ${warning}`); 97 | } 98 | }); 99 | 100 | test("path parameters work", async (t) => { 101 | const fastify = Fastify(); 102 | fastify.register(fastifyOpenapiGlue, opts); 103 | 104 | const res = await fastify.inject({ 105 | method: "GET", 106 | url: "/pathParam/2", 107 | }); 108 | assert.equal(res.statusCode, 200); 109 | }); 110 | 111 | test("query parameters work", async (t) => { 112 | const fastify = Fastify(); 113 | fastify.register(fastifyOpenapiGlue, opts); 114 | 115 | const res = await fastify.inject({ 116 | method: "GET", 117 | url: "/queryParam?int1=1&int2=2", 118 | }); 119 | assert.equal(res.statusCode, 200); 120 | }); 121 | 122 | test("query parameters with object schema work", async (t) => { 123 | const fastify = Fastify(); 124 | fastify.register(fastifyOpenapiGlue, opts); 125 | 126 | const res = await fastify.inject({ 127 | method: "GET", 128 | url: "/queryParamObject?int1=1&int2=2", 129 | }); 130 | assert.equal(res.statusCode, 200); 131 | }); 132 | 133 | test("query parameters with array schema work", async (t) => { 134 | const fastify = Fastify(); 135 | fastify.register(fastifyOpenapiGlue, opts); 136 | 137 | const res = await fastify.inject({ 138 | method: "GET", 139 | url: "/queryParamArray?arr=1&arr=2", 140 | }); 141 | assert.equal(res.statusCode, 200); 142 | }); 143 | 144 | test("header parameters work", async (t) => { 145 | const fastify = Fastify(); 146 | fastify.register(fastifyOpenapiGlue, opts); 147 | 148 | const res = await fastify.inject({ 149 | url: "/headerParam", 150 | method: "GET", 151 | headers: { 152 | "X-Request-ID": "test data", 153 | }, 154 | }); 155 | assert.equal(res.statusCode, 200); 156 | }); 157 | 158 | test("missing header parameters returns error 500", async (t) => { 159 | const fastify = Fastify(); 160 | fastify.register(fastifyOpenapiGlue, opts); 161 | 162 | const res = await fastify.inject({ 163 | method: "GET", 164 | url: "/headerParam", 165 | }); 166 | assert.equal(res.statusCode, 500); 167 | }); 168 | 169 | test("missing authorization header returns error 500", async (t) => { 170 | const fastify = Fastify(); 171 | fastify.register(fastifyOpenapiGlue, opts); 172 | 173 | const res = await fastify.inject({ 174 | method: "GET", 175 | url: "/authHeaderParam", 176 | }); 177 | assert.equal(res.statusCode, 500); 178 | }); 179 | 180 | test("body parameters work", async (t) => { 181 | const fastify = Fastify(); 182 | fastify.register(fastifyOpenapiGlue, opts); 183 | 184 | const res = await fastify.inject({ 185 | method: "post", 186 | url: "/bodyParam", 187 | payload: { 188 | str1: "test data", 189 | str2: "test data", 190 | }, 191 | }); 192 | assert.equal(res.statusCode, 200); 193 | }); 194 | 195 | test("no parameters work", async (t) => { 196 | const fastify = Fastify(); 197 | fastify.register(fastifyOpenapiGlue, opts); 198 | 199 | const res = await fastify.inject({ 200 | method: "get", 201 | url: "/noParam", 202 | }); 203 | assert.equal(res.statusCode, 200); 204 | }); 205 | 206 | test("prefix in opts works", async (t) => { 207 | const fastify = Fastify(); 208 | fastify.register(fastifyOpenapiGlue, prefixOpts); 209 | 210 | const res = await fastify.inject({ 211 | method: "get", 212 | url: "/prefix/noParam", 213 | }); 214 | assert.equal(res.statusCode, 200); 215 | }); 216 | 217 | test("missing operation from service returns error 500", async (t) => { 218 | const fastify = Fastify(); 219 | fastify.register(fastifyOpenapiGlue, opts); 220 | 221 | const res = await fastify.inject({ 222 | method: "get", 223 | url: "/noOperationId/1", 224 | }); 225 | assert.equal(res.statusCode, 500); 226 | }); 227 | 228 | test("response schema works with valid response", async (t) => { 229 | const fastify = Fastify(); 230 | fastify.register(fastifyOpenapiGlue, opts); 231 | 232 | const res = await fastify.inject({ 233 | method: "get", 234 | url: "/responses?replyType=valid", 235 | }); 236 | assert.equal(res.statusCode, 200); 237 | }); 238 | 239 | test("response schema works with invalid response", async (t) => { 240 | const fastify = Fastify(); 241 | fastify.register(fastifyOpenapiGlue, opts); 242 | 243 | const res = await fastify.inject({ 244 | method: "get", 245 | url: "/responses?replyType=invalid", 246 | }); 247 | assert.equal(res.statusCode, 500); 248 | }); 249 | 250 | test("yaml spec works", async (t) => { 251 | const fastify = Fastify(noStrict); 252 | fastify.register(fastifyOpenapiGlue, yamlOpts); 253 | 254 | const res = await fastify.inject({ 255 | method: "GET", 256 | url: "/pathParam/2", 257 | }); 258 | assert.equal(res.statusCode, 200); 259 | }); 260 | 261 | test("generic path parameters work", async (t) => { 262 | const fastify = Fastify(); 263 | fastify.register(fastifyOpenapiGlue, genericPathItemsOpts); 264 | 265 | const res = await fastify.inject({ 266 | method: "GET", 267 | url: "/pathParam/2", 268 | }); 269 | assert.equal(res.statusCode, 200); 270 | }); 271 | 272 | test("generic path parameters override works", async (t) => { 273 | const fastify = Fastify(); 274 | fastify.register(fastifyOpenapiGlue, genericPathItemsOpts); 275 | 276 | const res = await fastify.inject({ 277 | method: "GET", 278 | url: "/noParam", 279 | }); 280 | assert.equal(res.statusCode, 200); 281 | }); 282 | 283 | test("invalid openapi v3 specification throws error ", (t, done) => { 284 | const fastify = Fastify(); 285 | fastify.register(fastifyOpenapiGlue, invalidSwaggerOpts); 286 | fastify.ready((err) => { 287 | if (err) { 288 | assert.equal( 289 | err.message, 290 | "'specification' parameter must contain a valid version 2.0 or 3.0.x or 3.1.x specification", 291 | "got expected error", 292 | ); 293 | done(); 294 | } else { 295 | assert.fail("missed expected error"); 296 | } 297 | }); 298 | }); 299 | 300 | test("missing service definition throws error ", (t, done) => { 301 | const fastify = Fastify(); 302 | fastify.register(fastifyOpenapiGlue, invalidServiceOpts); 303 | fastify.ready((err) => { 304 | if (err) { 305 | assert.equal( 306 | err.message, 307 | "'serviceHandlers' parameter must refer to an object", 308 | "got expected error", 309 | ); 310 | done(); 311 | } else { 312 | assert.fail("missed expected error"); 313 | } 314 | }); 315 | }); 316 | 317 | test("full pet store V3 definition does not throw error ", (t, done) => { 318 | const fastify = Fastify(noStrict); 319 | // dummy parser to fix coverage testing 320 | fastify.addContentTypeParser( 321 | "application/xml", 322 | { parseAs: "string" }, 323 | (req, body) => body, 324 | ); 325 | fastify.register(fastifyOpenapiGlue, petStoreOpts); 326 | fastify.ready((err) => { 327 | if (err) { 328 | assert.fail("got unexpected error"); 329 | } else { 330 | assert.ok(true, "no unexpected error"); 331 | done(); 332 | } 333 | }); 334 | }); 335 | 336 | test("V3.0.1 definition does not throw error", (t, done) => { 337 | const spec301 = structuredClone(petStoreSpec); 338 | spec301["openapi"] = "3.0.1"; 339 | const opts301 = { 340 | specification: spec301, 341 | serviceHandlers, 342 | }; 343 | 344 | const fastify = Fastify(noStrict); 345 | fastify.register(fastifyOpenapiGlue, opts301); 346 | fastify.ready((err) => { 347 | if (err) { 348 | assert.fail("got unexpected error"); 349 | } else { 350 | assert.ok(true, "no unexpected error"); 351 | done(); 352 | } 353 | }); 354 | }); 355 | 356 | test("V3.0.2 definition does not throw error", (t, done) => { 357 | const spec302 = structuredClone(petStoreSpec); 358 | spec302["openapi"] = "3.0.2"; 359 | const opts302 = { 360 | specification: spec302, 361 | serviceHandlers, 362 | }; 363 | 364 | const fastify = Fastify(noStrict); 365 | fastify.register(fastifyOpenapiGlue, opts302); 366 | fastify.ready((err) => { 367 | if (err) { 368 | assert.fail("got unexpected error"); 369 | } else { 370 | assert.ok(true, "no unexpected error"); 371 | done(); 372 | } 373 | }); 374 | }); 375 | 376 | test("V3.0.3 definition does not throw error", (t, done) => { 377 | const spec303 = structuredClone(petStoreSpec); 378 | spec303["openapi"] = "3.0.3"; 379 | const opts303 = { 380 | specification: spec303, 381 | serviceHandlers, 382 | }; 383 | 384 | const fastify = Fastify(noStrict); 385 | fastify.register(fastifyOpenapiGlue, opts303); 386 | fastify.ready((err) => { 387 | if (err) { 388 | assert.fail("got unexpected error"); 389 | } else { 390 | assert.ok(true, "no unexpected error"); 391 | done(); 392 | } 393 | }); 394 | }); 395 | 396 | test("x- props are copied", async (t) => { 397 | const fastify = Fastify(); 398 | fastify.addHook("preHandler", async (request, reply) => { 399 | if (request.routeOptions.schema["x-tap-ok"]) { 400 | assert.ok(true, "found x- prop"); 401 | } else { 402 | assert.fail("missing x- prop"); 403 | } 404 | }); 405 | fastify.register(fastifyOpenapiGlue, opts); 406 | 407 | const res = await fastify.inject({ 408 | method: "GET", 409 | url: "/queryParam?int1=1&int2=2", 410 | }); 411 | assert.equal(res.statusCode, 200); 412 | }); 413 | 414 | test("x-fastify-config is applied", async (t) => { 415 | const fastify = Fastify(); 416 | fastify.register(fastifyOpenapiGlue, { 417 | ...opts, 418 | serviceHandlers: { 419 | operationWithFastifyConfigExtension: async (req, reply) => { 420 | assert.equal( 421 | req.routeOptions.config.rawBody, 422 | true, 423 | "config.rawBody is true", 424 | ); 425 | return; 426 | }, 427 | }, 428 | }); 429 | 430 | await fastify.inject({ 431 | method: "GET", 432 | url: "/operationWithFastifyConfigExtension", 433 | }); 434 | }); 435 | 436 | test("x-no-fastify-config is applied", async (t) => { 437 | const fastify = Fastify(); 438 | fastify.register(fastifyOpenapiGlue, { 439 | ...opts, 440 | serviceHandlers: { 441 | ignoreRoute: async (req, reply) => {}, 442 | }, 443 | }); 444 | 445 | const res = await fastify.inject({ 446 | method: "GET", 447 | url: "/ignoreRoute", 448 | }); 449 | assert.equal(res.statusCode, 404); 450 | }); 451 | 452 | test("service and operationResolver together throw error", (t, done) => { 453 | const fastify = Fastify(); 454 | fastify.register(fastifyOpenapiGlue, serviceAndOperationResolver); 455 | fastify.ready((err) => { 456 | if (err) { 457 | assert.equal( 458 | err.message, 459 | "'serviceHandlers' and 'operationResolver' are mutually exclusive", 460 | "got expected error", 461 | ); 462 | done(); 463 | } else { 464 | assert.fail("missed expected error"); 465 | } 466 | }); 467 | }); 468 | 469 | test("no service and no operationResolver throw error", (t, done) => { 470 | const fastify = Fastify(); 471 | fastify.register(fastifyOpenapiGlue, noServiceNoResolver); 472 | fastify.ready((err) => { 473 | if (err) { 474 | assert.equal( 475 | err.message, 476 | "either 'serviceHandlers' or 'operationResolver' are required", 477 | "got expected error", 478 | ); 479 | done(); 480 | } else { 481 | assert.fail("missed expected error"); 482 | } 483 | }); 484 | }); 485 | 486 | test("operation resolver works", async (t) => { 487 | const fastify = Fastify(); 488 | fastify.register(fastifyOpenapiGlue, withOperationResolver); 489 | 490 | const res = await fastify.inject({ 491 | method: "get", 492 | url: "/noParam", 493 | }); 494 | assert.equal(res.body, "ok"); 495 | }); 496 | 497 | test("operation resolver with method and url works", async (t) => { 498 | const fastify = Fastify(); 499 | fastify.register(fastifyOpenapiGlue, withOperationResolverUsingMethodPath); 500 | 501 | const res = await fastify.inject({ 502 | method: "get", 503 | url: "/noParam", 504 | }); 505 | assert.equal(res.body, "ok"); 506 | }); 507 | 508 | test("create an empty body with addEmptySchema option", async (t) => { 509 | const fastify = Fastify(); 510 | 511 | let emptyBodySchemaFound = false; 512 | fastify.addHook("onRoute", (routeOptions) => { 513 | if (routeOptions.url === "/emptyBodySchema") { 514 | assert.deepStrictEqual(routeOptions.schema.response?.["204"], {}); 515 | assert.deepStrictEqual(routeOptions.schema.response?.["302"], {}); 516 | emptyBodySchemaFound = true; 517 | } 518 | }); 519 | 520 | await fastify.register(fastifyOpenapiGlue, { 521 | specification: testSpec, 522 | serviceHandlers: new Set(), 523 | addEmptySchema: true, 524 | }); 525 | assert.ok(emptyBodySchemaFound); 526 | }); 527 | -------------------------------------------------------------------------------- /test/test-plugin.v3.multipleMimeTypes.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import fastifyOpenapiGlue from "../index.js"; 6 | 7 | const importJSON = createRequire(import.meta.url); 8 | 9 | const testSpec = await importJSON("./test-openapi.v3.multipleMimeTypes.json"); 10 | import { Service } from "./service.multipleMimeTypes.js"; 11 | const serviceHandlers = new Service(); 12 | 13 | const noStrict = { 14 | ajv: { 15 | customOptions: { 16 | strict: false, 17 | }, 18 | }, 19 | }; 20 | 21 | const opts = { 22 | specification: testSpec, 23 | serviceHandlers, 24 | }; 25 | 26 | test("multiple MIME types in request body work", async (t) => { 27 | const fastify = Fastify(); 28 | fastify.addContentTypeParser( 29 | "text/json", 30 | { parseAs: "string" }, 31 | fastify.getDefaultJsonParser(), 32 | ); 33 | fastify.register(fastifyOpenapiGlue, opts); 34 | 35 | const method = "POST"; 36 | const url = "/postMultipleBodyMimeTypes"; 37 | const res1 = await fastify.inject({ 38 | method, 39 | url, 40 | payload: { 41 | str1: "string", 42 | }, 43 | }); 44 | assert.equal(res1.statusCode, 200); 45 | const res2 = await fastify.inject({ 46 | headers: { 47 | "content-type": "text/json", 48 | }, 49 | method, 50 | url, 51 | payload: { 52 | int1: 2, 53 | }, 54 | }); 55 | assert.equal(res2.statusCode, 200); 56 | 57 | // just to be sure 58 | const res3 = await fastify.inject({ 59 | headers: { 60 | "content-type": "application/vnd.v2-json", 61 | }, 62 | method, 63 | url, 64 | payload: { 65 | str1: "string", 66 | int1: 2, 67 | }, 68 | }); 69 | assert.equal(res3.statusCode, 415); // 415 = unsupported media type 70 | }); 71 | 72 | test("multiple MIME types in response work", async (t) => { 73 | const fastify = Fastify(); 74 | fastify.register(fastifyOpenapiGlue, opts); 75 | 76 | const method = "GET"; 77 | const url = "/getMultipleResponseMimeTypes"; 78 | const res1 = await fastify.inject({ 79 | method, 80 | url, 81 | query: { 82 | responseType: "application/json", 83 | }, 84 | }); 85 | assert.equal(res1.statusCode, 200); 86 | const res1Body = JSON.parse(res1.body); 87 | assert.equal(res1Body.str1, "test data"); 88 | assert.ok(res1.headers["content-type"].includes("application/json")); 89 | const res2 = await fastify.inject({ 90 | method, 91 | url, 92 | query: { 93 | responseType: "text/json", 94 | }, 95 | }); 96 | assert.equal(res2.statusCode, 200); 97 | const res2Body = JSON.parse(res2.body); 98 | assert.equal(res2Body.int1, 2); 99 | assert.ok(res2.headers["content-type"].includes("text/json")); 100 | }); 101 | -------------------------------------------------------------------------------- /test/test-recursive.v3.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import fastifyOpenapiGlue from "../index.js"; 6 | const importJSON = createRequire(import.meta.url); 7 | 8 | const testSpec = await importJSON("./test-openapi-v3-recursive.json"); 9 | 10 | import { Service } from "./service.js"; 11 | const serviceHandlers = new Service(); 12 | 13 | const noStrict = { 14 | ajv: { 15 | customOptions: { 16 | strict: false, 17 | }, 18 | }, 19 | }; 20 | 21 | test("route registration succeeds with recursion", (t, done) => { 22 | const opts = { 23 | specification: testSpec, 24 | serviceHandlers, 25 | }; 26 | 27 | const fastify = Fastify(noStrict); 28 | fastify.register(fastifyOpenapiGlue, opts); 29 | fastify.ready((err) => { 30 | if (err) { 31 | assert.fail("got unexpected error"); 32 | } else { 33 | assert.ok(true, "no unexpected error"); 34 | done(); 35 | } 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/test-securityHandlers.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import fastifyOpenapiGlue from "../index.js"; 6 | const importJSON = createRequire(import.meta.url); 7 | 8 | import securityHandlers from "./security.js"; 9 | import { Service } from "./service.js"; 10 | const serviceHandlers = new Service(); 11 | 12 | const noStrict = { 13 | ajv: { 14 | customOptions: { 15 | strict: false, 16 | }, 17 | }, 18 | }; 19 | 20 | await doTest( 21 | "v2", 22 | "./test-swagger.v2.json", 23 | "./petstore-swagger.v2.json", 24 | "/v2", 25 | ); 26 | await doTest("v3", "./test-openapi.v3.json", "./petstore-openapi.v3.json", ""); 27 | 28 | async function doTest(version, testSpecName, petStoreSpecName, prefix) { 29 | const testSpec = await importJSON(testSpecName); 30 | const petStoreSpec = await importJSON(petStoreSpecName); 31 | 32 | const invalidSecurityOpts = { 33 | specification: testSpec, 34 | serviceHandlers, 35 | securityHandlers: () => {}, 36 | }; 37 | 38 | test(`${version} security handler registration succeeds`, (t, done) => { 39 | const opts = { 40 | specification: petStoreSpec, 41 | serviceHandlers, 42 | securityHandlers, 43 | }; 44 | 45 | const fastify = Fastify(noStrict); 46 | fastify.register(fastifyOpenapiGlue, opts); 47 | fastify.ready((err) => { 48 | if (err) { 49 | assert.fail("got unexpected error"); 50 | } else { 51 | assert.ok(true, "no unexpected error"); 52 | done(); 53 | } 54 | }); 55 | }); 56 | 57 | test(`${version} security preHandler throws error`, async (t) => { 58 | const opts = { 59 | specification: testSpec, 60 | serviceHandlers, 61 | securityHandlers: { 62 | api_key: securityHandlers.failingAuthCheck, 63 | }, 64 | }; 65 | 66 | const fastify = Fastify(); 67 | fastify.setErrorHandler((err, req, reply) => { 68 | assert.equal(err.errors.length, 3); 69 | reply.code(err.statusCode).send(err); 70 | }); 71 | fastify.register(fastifyOpenapiGlue, opts); 72 | 73 | const res = await fastify.inject({ 74 | method: "GET", 75 | url: `${prefix}/operationSecurity`, 76 | }); 77 | assert.equal(res.statusCode, 401); 78 | assert.equal(res.statusMessage, "Unauthorized"); 79 | }); 80 | 81 | test(`${version} security preHandler passes on succes using OR`, async (t) => { 82 | const opts = { 83 | specification: testSpec, 84 | serviceHandlers, 85 | securityHandlers: { 86 | api_key: securityHandlers.failingAuthCheck, 87 | skipped: securityHandlers.goodAuthCheck, 88 | failing: securityHandlers.failingAuthCheck, 89 | }, 90 | }; 91 | 92 | const fastify = Fastify(); 93 | fastify.register(fastifyOpenapiGlue, opts); 94 | 95 | const res = await fastify.inject({ 96 | method: "GET", 97 | url: `${prefix}/operationSecurity`, 98 | }); 99 | assert.equal(res.statusCode, 200); 100 | assert.equal(res.statusMessage, "OK"); 101 | }); 102 | 103 | test(`${version} security preHandler passes on succes using AND`, async (t) => { 104 | const result = {}; 105 | const opts = { 106 | specification: testSpec, 107 | serviceHandlers, 108 | securityHandlers: { 109 | api_key: securityHandlers.goodAuthCheck, 110 | skipped: securityHandlers.goodAuthCheck, 111 | failing: securityHandlers.failingAuthCheck, 112 | }, 113 | }; 114 | 115 | const fastify = Fastify(); 116 | fastify.register(fastifyOpenapiGlue, opts); 117 | 118 | const res = await fastify.inject({ 119 | method: "GET", 120 | url: `${prefix}/operationSecurityUsingAnd`, 121 | }); 122 | assert.equal(res.statusCode, 200); 123 | assert.equal(res.statusMessage, "OK"); 124 | assert.equal(res.body, '{"response":"Authentication succeeded!"}'); 125 | }); 126 | 127 | test(`${version} security preHandler fails correctly on failure using AND`, async (t) => { 128 | const opts = { 129 | specification: testSpec, 130 | serviceHandlers, 131 | securityHandlers: { 132 | api_key: securityHandlers.failingAuthCheck, 133 | skipped: securityHandlers.goodAuthCheck, 134 | failing: securityHandlers.failingAuthCheck, 135 | }, 136 | }; 137 | 138 | const fastify = Fastify(); 139 | fastify.register(fastifyOpenapiGlue, opts); 140 | 141 | const res = await fastify.inject({ 142 | method: "GET", 143 | url: `${prefix}/operationSecurityUsingAnd`, 144 | }); 145 | assert.equal(res.statusCode, 401); 146 | assert.equal(res.statusMessage, "Unauthorized"); 147 | }); 148 | 149 | test(`${version} security preHandler passes with empty handler`, async (t) => { 150 | const opts = { 151 | specification: testSpec, 152 | serviceHandlers, 153 | securityHandlers, 154 | }; 155 | 156 | const fastify = Fastify(); 157 | fastify.register(fastifyOpenapiGlue, opts); 158 | 159 | const res = await fastify.inject({ 160 | method: "GET", 161 | url: `${prefix}/operationSecurityEmptyHandler`, 162 | }); 163 | assert.equal(res.statusCode, 200); 164 | assert.equal(res.statusMessage, "OK"); 165 | }); 166 | 167 | test(`${version} security preHandler handles missing handlers`, async (t) => { 168 | const opts = { 169 | specification: testSpec, 170 | serviceHandlers, 171 | securityHandlers: { 172 | ipa_key: securityHandlers.failingAuthCheck, 173 | }, 174 | }; 175 | 176 | const fastify = Fastify(); 177 | fastify.setErrorHandler((err, req, reply) => { 178 | assert.equal(err.errors.length, 3); 179 | reply.code(err.statusCode).send(err); 180 | }); 181 | fastify.register(fastifyOpenapiGlue, opts); 182 | 183 | const res = await fastify.inject({ 184 | method: "GET", 185 | url: `${prefix}/operationSecurity`, 186 | }); 187 | assert.equal(res.statusCode, 401); 188 | assert.equal(res.statusMessage, "Unauthorized"); 189 | }); 190 | 191 | test(`${version} invalid securityHandler definition throws error `, (t, done) => { 192 | const fastify = Fastify(); 193 | fastify.register(fastifyOpenapiGlue, invalidSecurityOpts); 194 | fastify.ready((err) => { 195 | if (err) { 196 | assert.equal( 197 | err.message, 198 | "'securityHandlers' parameter must refer to an object", 199 | "got expected error", 200 | ); 201 | done(); 202 | } else { 203 | assert.fail("missed expected error"); 204 | } 205 | }); 206 | }); 207 | 208 | if (version !== "v2") { 209 | test(`${version} initalization of securityHandlers succeeds`, (t, done) => { 210 | const opts = { 211 | specification: testSpec, 212 | serviceHandlers, 213 | securityHandlers: { 214 | initialize: async (securitySchemes) => { 215 | const securitySchemeFromSpec = JSON.stringify( 216 | testSpec.components.securitySchemes, 217 | ); 218 | assert.equal( 219 | JSON.stringify(securitySchemes), 220 | securitySchemeFromSpec, 221 | ); 222 | }, 223 | }, 224 | }; 225 | 226 | const fastify = Fastify(); 227 | fastify.register(fastifyOpenapiGlue, opts); 228 | fastify.ready((err) => { 229 | if (err) { 230 | assert.fail("got unexpected error"); 231 | } else { 232 | assert.ok(true, "no unexpected error"); 233 | done(); 234 | } 235 | }); 236 | }); 237 | } 238 | 239 | test(`${version} security preHandler gets parameters passed`, async (t) => { 240 | const opts = { 241 | specification: testSpec, 242 | serviceHandlers, 243 | securityHandlers: { 244 | api_key: securityHandlers.failingAuthCheck, 245 | skipped: (req, repl, param) => { 246 | req.scope = param[0]; 247 | }, 248 | failing: securityHandlers.failingAuthCheck, 249 | }, 250 | }; 251 | 252 | const fastify = Fastify(); 253 | fastify.register(fastifyOpenapiGlue, opts); 254 | 255 | { 256 | const res = await fastify.inject({ 257 | method: "GET", 258 | url: `${prefix}/operationSecurity`, 259 | }); 260 | assert.equal(res.statusCode, 200); 261 | assert.equal(res.statusMessage, "OK"); 262 | assert.equal(res.body, '{"response":"authentication succeeded!"}'); 263 | } 264 | 265 | { 266 | const res = await fastify.inject({ 267 | method: "GET", 268 | url: `${prefix}/operationSecurityWithParameter`, 269 | }); 270 | assert.equal(res.statusCode, 200); 271 | assert.equal(res.statusMessage, "OK"); 272 | assert.equal(res.body, '{"response":"skipped"}'); 273 | } 274 | }); 275 | 276 | test(`${version} security preHandler throws error with custom StatusCode`, async (t) => { 277 | const opts = { 278 | specification: testSpec, 279 | serviceHandlers, 280 | securityHandlers: { 281 | api_key: securityHandlers.failingAuthCheckCustomStatusCode, 282 | }, 283 | }; 284 | 285 | const fastify = Fastify(); 286 | fastify.setErrorHandler((err, req, reply) => { 287 | assert.equal(err.errors.length, 3); 288 | reply.code(err.statusCode).send(err); 289 | }); 290 | fastify.register(fastifyOpenapiGlue, opts); 291 | 292 | const res = await fastify.inject({ 293 | method: "GET", 294 | url: `${prefix}/operationSecurity`, 295 | }); 296 | assert.equal(res.statusCode, 451); 297 | assert.equal(res.statusMessage, "Unavailable For Legal Reasons"); 298 | }); 299 | 300 | test(`${version} security preHandler does not throw error when global security handler is overwritten with local empty security`, async (t) => { 301 | const opts = { 302 | specification: testSpec, 303 | serviceHandlers, 304 | securityHandlers: { 305 | api_key: securityHandlers.failingAuthCheck, 306 | }, 307 | }; 308 | 309 | const fastify = Fastify(); 310 | fastify.setErrorHandler((err, _req, reply) => { 311 | assert.equal(err.errors.length, 3); 312 | reply.code(err.statusCode).send(err); 313 | }); 314 | fastify.register(fastifyOpenapiGlue, opts); 315 | 316 | const res = await fastify.inject({ 317 | method: "GET", 318 | url: `${prefix}/operationSecurityOverrideWithNoSecurity`, 319 | }); 320 | assert.equal(res.statusCode, 200); 321 | assert.equal(res.statusMessage, "OK"); 322 | assert.equal(res.body, '{"response":"authentication succeeded!"}'); 323 | }); 324 | } 325 | -------------------------------------------------------------------------------- /test/test-swagger-noBasePath.v2.javascript.checksums.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "spec": { 4 | "fileName": "openApi.json", 5 | "checksum": "9cb912f1f51fbcc9098b3f76320b25c150a807083de4fe9c1faa08dd61de84ea" 6 | }, 7 | "service": { 8 | "fileName": "service.js", 9 | "checksum": "07c998d7a86e723be918d38e54230a392f5886ee3fce6d1eb9fe154234f2f8fe" 10 | }, 11 | "security": { 12 | "fileName": "security.js", 13 | "checksum": "e75dac6f2199a27e5517fac04d677ee7711a6c7d99374fef9cd84269e493b4db" 14 | }, 15 | "plugin": { 16 | "fileName": "index.js", 17 | "checksum": "aaf6dbb034952530335e39a4a48f1b0ac065d00072d0d6156489391de8721451" 18 | }, 19 | "readme": { 20 | "fileName": "README.md", 21 | "checksum": "bc6157b1800d9ae797426faf2f1150adb8839fc939ff5ca40ddb15db6427be80" 22 | }, 23 | "testPlugin": { 24 | "fileName": "test-plugin.js", 25 | "checksum": "e54fabded49f5b8c13210038e03e432242936ff2790ad29e66084208eed45f9e" 26 | }, 27 | "package": { 28 | "fileName": "package.json", 29 | "checksum": "af9aa73cbb8842aad2ab65ed44ef6e65d04055608c5c3ff82675675f707349f7" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/test-swagger-noBasePath.v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Test swagger", 5 | "description": "testing the fastify swagger api", 6 | "version": "0.1.0" 7 | }, 8 | "host": "localhost", 9 | "schemes": ["http"], 10 | "consumes": ["application/json"], 11 | "produces": ["application/json"], 12 | "paths": { 13 | "/pathParam/{id}": { 14 | "get": { 15 | "operationId": "getPathParam", 16 | "summary": "Test path parameters", 17 | "parameters": [ 18 | { 19 | "name": "id", 20 | "in": "path", 21 | "required": true, 22 | "type": "integer" 23 | } 24 | ], 25 | "responses": { 26 | "200": { 27 | "description": "ok" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/test-swagger-noBasePath.v2.standaloneJS.checksums.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "spec": { 4 | "fileName": "openApi.json", 5 | "checksum": "3d6eea0625207d859fed989f45ee6ff43fe968a459e58b07556b8cf4cf58d2c4" 6 | }, 7 | "service": { 8 | "fileName": "service.js", 9 | "checksum": "07c998d7a86e723be918d38e54230a392f5886ee3fce6d1eb9fe154234f2f8fe" 10 | }, 11 | "security": { 12 | "fileName": "security.js", 13 | "checksum": "e75dac6f2199a27e5517fac04d677ee7711a6c7d99374fef9cd84269e493b4db" 14 | }, 15 | "plugin": { 16 | "fileName": "index.js", 17 | "checksum": "15d5418114932848972e8fe701144341ec8d6e41314a4b87274d7de0c0931106" 18 | }, 19 | "readme": { 20 | "fileName": "README.md", 21 | "checksum": "525645f6c7f8244fd91a768c331ef08165d62210fcdd4f80e3f7fa77c272acba" 22 | }, 23 | "securityHandlers": { 24 | "fileName": "securityHandlers.js", 25 | "checksum": "69e06db50bc73042bcea1fa0deaeb1f677dbfc009459dd55ad921b7eeb2184dd" 26 | }, 27 | "testPlugin": { 28 | "fileName": "test-plugin.js", 29 | "checksum": "2b055432a970c11bf3f09dc6b89cce0c8f842518d207ff733ebe01a9ad69a1e8" 30 | }, 31 | "package": { 32 | "fileName": "package.json", 33 | "checksum": "af9aa73cbb8842aad2ab65ed44ef6e65d04055608c5c3ff82675675f707349f7" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/test-swagger-v2-generic-path-items.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Test swagger", 5 | "description": "testing the fastify swagger api", 6 | "version": "0.1.0" 7 | }, 8 | "host": "localhost", 9 | "paths": { 10 | "/pathParam/{id}": { 11 | "parameters": [ 12 | { 13 | "name": "id", 14 | "in": "path", 15 | "required": true, 16 | "type": "string" 17 | } 18 | ], 19 | "get": { 20 | "operationId": "getPathParam", 21 | "summary": "Test path parameters", 22 | "parameters": [ 23 | { 24 | "name": "id", 25 | "in": "path", 26 | "required": true, 27 | "type": "integer" 28 | } 29 | ], 30 | "responses": { 31 | "200": { 32 | "description": "ok" 33 | } 34 | } 35 | } 36 | }, 37 | "/noParam": { 38 | "parameters": [ 39 | { 40 | "name": "id", 41 | "in": "query", 42 | "required": true, 43 | "type": "string" 44 | } 45 | ], 46 | "get": { 47 | "operationId": "getNoParam", 48 | "summary": "Test path parameters", 49 | "parameters": [ 50 | { 51 | "name": "id", 52 | "in": "query", 53 | "type": "integer" 54 | } 55 | ], 56 | "responses": { 57 | "200": { 58 | "description": "ok" 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/test-swagger.v2.javascript.checksums.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "spec": { 4 | "fileName": "openApi.json", 5 | "checksum": "2be97ce753a77f1dec89de5a36012498368de21abdb10b7b273c3b1caaffbfe9" 6 | }, 7 | "service": { 8 | "fileName": "service.js", 9 | "checksum": "5aa5591dda61861b97b8f3231038eed69cd2d0c47b2cfaa727dea89708a91161" 10 | }, 11 | "security": { 12 | "fileName": "security.js", 13 | "checksum": "78442cf521da3a908d4ec2b3842938972a0220d82bf078fd6eb57ade2cdb330d" 14 | }, 15 | "plugin": { 16 | "fileName": "index.js", 17 | "checksum": "aaf6dbb034952530335e39a4a48f1b0ac065d00072d0d6156489391de8721451" 18 | }, 19 | "readme": { 20 | "fileName": "README.md", 21 | "checksum": "bc6157b1800d9ae797426faf2f1150adb8839fc939ff5ca40ddb15db6427be80" 22 | }, 23 | "testPlugin": { 24 | "fileName": "test-plugin.js", 25 | "checksum": "b352050ec4c2e35dce64218257ea653388f8a50aa7a45d9cf8c20e818deac0e1" 26 | }, 27 | "package": { 28 | "fileName": "package.json", 29 | "checksum": "af9aa73cbb8842aad2ab65ed44ef6e65d04055608c5c3ff82675675f707349f7" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/test-swagger.v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Test swagger", 5 | "description": "testing the fastify swagger api", 6 | "version": "0.1.0" 7 | }, 8 | "host": "localhost", 9 | "basePath": "/v2", 10 | "schemes": ["http"], 11 | "consumes": ["application/json"], 12 | "produces": ["application/json"], 13 | "security": [ 14 | { 15 | "skipped": [] 16 | } 17 | ], 18 | "paths": { 19 | "/pathParam/{id}": { 20 | "get": { 21 | "operationId": "getPathParam", 22 | "summary": "Test path parameters", 23 | "parameters": [ 24 | { 25 | "name": "id", 26 | "in": "path", 27 | "required": true, 28 | "type": "integer" 29 | } 30 | ], 31 | "responses": { 32 | "200": { 33 | "description": "ok" 34 | } 35 | } 36 | } 37 | }, 38 | "/queryParam": { 39 | "get": { 40 | "operationId": "getQueryParam", 41 | "summary": "Test query parameters", 42 | "parameters": [ 43 | { 44 | "in": "query", 45 | "name": "int1", 46 | "type": "integer" 47 | }, 48 | { 49 | "in": "query", 50 | "name": "int2", 51 | "type": "integer" 52 | } 53 | ], 54 | "responses": { 55 | "200": { 56 | "description": "ok" 57 | } 58 | }, 59 | "x-tap-ok": true 60 | } 61 | }, 62 | "/headerParam": { 63 | "get": { 64 | "operationId": "getHeaderParam", 65 | "summary": "Test header parameters", 66 | "parameters": [ 67 | { 68 | "in": "header", 69 | "name": "X-Request-ID", 70 | "type": "string" 71 | } 72 | ], 73 | "responses": { 74 | "200": { 75 | "description": "ok" 76 | } 77 | } 78 | } 79 | }, 80 | "/bodyParam": { 81 | "post": { 82 | "operationId": "postBodyParam", 83 | "summary": "Test body parameters", 84 | "parameters": [ 85 | { 86 | "name": "str1", 87 | "in": "body", 88 | "required": true, 89 | "schema": { 90 | "$ref": "#/definitions/bodyObject" 91 | } 92 | } 93 | ], 94 | "responses": { 95 | "200": { 96 | "description": "ok" 97 | } 98 | } 99 | } 100 | }, 101 | "/noParam": { 102 | "get": { 103 | "operationId": "getNoParam", 104 | "summary": "Test without parameters", 105 | "responses": { 106 | "200": { 107 | "description": "ok" 108 | } 109 | } 110 | } 111 | }, 112 | "/noOperationId/{id}": { 113 | "get": { 114 | "summary": "Test missing operationid", 115 | "parameters": [ 116 | { 117 | "name": "id", 118 | "in": "path", 119 | "required": true, 120 | "type": "integer" 121 | } 122 | ], 123 | "responses": { 124 | "200": { 125 | "description": "ok", 126 | "schema": { 127 | "$ref": "#/definitions/responseObject" 128 | } 129 | } 130 | } 131 | } 132 | }, 133 | "/responses": { 134 | "get": { 135 | "operationId": "getResponse", 136 | "summary": "Test response serialization", 137 | "parameters": [ 138 | { 139 | "in": "query", 140 | "name": "replyType", 141 | "type": "string" 142 | } 143 | ], 144 | "responses": { 145 | "200": { 146 | "description": "ok", 147 | "schema": { 148 | "$ref": "#/definitions/responseObject" 149 | } 150 | } 151 | } 152 | } 153 | }, 154 | "/operationSecurity": { 155 | "get": { 156 | "operationId": "testOperationSecurity", 157 | "summary": "Test security handling", 158 | "security": [ 159 | { 160 | "api_key": [] 161 | }, 162 | { 163 | "skipped": [] 164 | }, 165 | { 166 | "failing": [] 167 | } 168 | ], 169 | "responses": { 170 | "200": { 171 | "description": "ok", 172 | "schema": { 173 | "$ref": "#/definitions/responseObject" 174 | } 175 | }, 176 | "401": { 177 | "description": "unauthorized", 178 | "schema": { 179 | "$ref": "#/definitions/errorObject" 180 | } 181 | } 182 | } 183 | } 184 | }, 185 | "/operationSecurityUsingAnd": { 186 | "get": { 187 | "operationId": "testOperationSecurityUsingAnd", 188 | "summary": "Test security handling AND functionality", 189 | "security": [ 190 | { 191 | "api_key": ["and"], 192 | "skipped": ["works"] 193 | } 194 | ], 195 | "responses": { 196 | "200": { 197 | "description": "ok", 198 | "schema": { 199 | "$ref": "#/definitions/responseObject" 200 | } 201 | }, 202 | "401": { 203 | "description": "unauthorized", 204 | "schema": { 205 | "$ref": "#/definitions/errorObject" 206 | } 207 | } 208 | } 209 | } 210 | }, 211 | "/operationSecurityWithParameter": { 212 | "get": { 213 | "operationId": "testOperationSecurityWithParameter", 214 | "summary": "Test security handling", 215 | "security": [ 216 | { 217 | "api_key": [] 218 | }, 219 | { 220 | "skipped": ["skipped"] 221 | }, 222 | { 223 | "failing": [] 224 | } 225 | ], 226 | "responses": { 227 | "200": { 228 | "description": "ok", 229 | "schema": { 230 | "$ref": "#/definitions/responseObject" 231 | } 232 | }, 233 | "401": { 234 | "description": "unauthorized", 235 | "schema": { 236 | "$ref": "#/definitions/errorObject" 237 | } 238 | } 239 | } 240 | } 241 | }, 242 | "/operationSecurityEmptyHandler": { 243 | "get": { 244 | "operationId": "testOperationSecurity", 245 | "summary": "Test security handling", 246 | "security": [ 247 | {}, 248 | { 249 | "failing": [] 250 | } 251 | ], 252 | "responses": { 253 | "200": { 254 | "description": "ok", 255 | "schema": { 256 | "$ref": "#/definitions/responseObject" 257 | } 258 | }, 259 | "401": { 260 | "description": "unauthorized", 261 | "schema": { 262 | "$ref": "#/definitions/errorObject" 263 | } 264 | } 265 | } 266 | } 267 | }, 268 | "/operationSecurityOverrideWithNoSecurity": { 269 | "get": { 270 | "operationId": "testOperationSecurity", 271 | "summary": "Test security handling", 272 | "security": [], 273 | "responses": { 274 | "200": { 275 | "description": "ok", 276 | "schema": { 277 | "$ref": "#/definitions/responseObject" 278 | } 279 | }, 280 | "401": { 281 | "description": "unauthorized", 282 | "schema": { 283 | "$ref": "#/definitions/errorObject" 284 | } 285 | } 286 | } 287 | } 288 | }, 289 | "/operationWithFastifyConfigExtension": { 290 | "get": { 291 | "operationId": "operationWithFastifyConfigExtension", 292 | "summary": "Test fastify config extension", 293 | "x-fastify-config": { 294 | "rawBody": true 295 | }, 296 | "responses": { 297 | "200": { 298 | "description": "ok", 299 | "schema": { 300 | "$ref": "#/definitions/responseObject" 301 | } 302 | } 303 | } 304 | } 305 | }, 306 | "/emptyBodySchema": { 307 | "get": { 308 | "operationId": "emptyBodySchema", 309 | "summary": "Empty body schema", 310 | "responses": { 311 | "204": { 312 | "description": "Empty" 313 | }, 314 | "302": { 315 | "description": "Empty" 316 | } 317 | } 318 | } 319 | } 320 | }, 321 | "definitions": { 322 | "bodyObject": { 323 | "type": "object", 324 | "properties": { 325 | "str1": { 326 | "type": "string" 327 | } 328 | }, 329 | "required": ["str1"] 330 | }, 331 | "responseObject": { 332 | "type": "object", 333 | "properties": { 334 | "response": { 335 | "type": "string" 336 | } 337 | }, 338 | "required": ["response"] 339 | }, 340 | "errorObject": { 341 | "type": "object", 342 | "properties": { 343 | "error": { 344 | "type": "string" 345 | }, 346 | "statusCode": { 347 | "type": "integer" 348 | } 349 | } 350 | } 351 | }, 352 | "securityDefinitions": { 353 | "api_key": { 354 | "type": "apiKey", 355 | "in": "header", 356 | "name": "X-API-Key" 357 | }, 358 | "skipped": { 359 | "type": "basic" 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /test/test-swagger.v2.standaloneJS.checksums.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "spec": { 4 | "fileName": "openApi.json", 5 | "checksum": "24e880dfaf0c8a27ce41da1685bfdb793091a8ea062255db80be67f75555d796" 6 | }, 7 | "service": { 8 | "fileName": "service.js", 9 | "checksum": "5aa5591dda61861b97b8f3231038eed69cd2d0c47b2cfaa727dea89708a91161" 10 | }, 11 | "security": { 12 | "fileName": "security.js", 13 | "checksum": "78442cf521da3a908d4ec2b3842938972a0220d82bf078fd6eb57ade2cdb330d" 14 | }, 15 | "plugin": { 16 | "fileName": "index.js", 17 | "checksum": "3df60ebd0311350f8c4834ab23bba9b50b26d5739281fbb19891baf26d2a0cd3" 18 | }, 19 | "readme": { 20 | "fileName": "README.md", 21 | "checksum": "525645f6c7f8244fd91a768c331ef08165d62210fcdd4f80e3f7fa77c272acba" 22 | }, 23 | "securityHandlers": { 24 | "fileName": "securityHandlers.js", 25 | "checksum": "69e06db50bc73042bcea1fa0deaeb1f677dbfc009459dd55ad921b7eeb2184dd" 26 | }, 27 | "testPlugin": { 28 | "fileName": "test-plugin.js", 29 | "checksum": "e1f61744618b1bd37091294aded89d5c7ce855d6cd17ae544aced9187251d8ab" 30 | }, 31 | "package": { 32 | "fileName": "package.json", 33 | "checksum": "af9aa73cbb8842aad2ab65ed44ef6e65d04055608c5c3ff82675675f707349f7" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/test-swagger.v2.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | title: Test swagger 4 | description: testing the fastify swagger api 5 | version: 0.1.0 6 | host: localhost 7 | basePath: /v2 8 | schemes: 9 | - http 10 | consumes: 11 | - application/json 12 | produces: 13 | - application/json 14 | paths: 15 | '/pathParam/{id}': 16 | get: 17 | operationId: getPathParam 18 | summary: Test path parameters 19 | parameters: 20 | - name: id 21 | in: path 22 | required: true 23 | type: integer 24 | responses: 25 | '200': 26 | description: ok 27 | /queryParam: 28 | get: 29 | operationId: getQueryParam 30 | summary: Test query parameters 31 | parameters: 32 | - in: query 33 | name: int1 34 | type: integer 35 | - in: query 36 | name: int2 37 | type: integer 38 | responses: 39 | '200': 40 | description: ok 41 | /headerParam: 42 | get: 43 | operationId: getHeaderParam 44 | summary: Test header parameters 45 | parameters: 46 | - in: header 47 | name: X-Request-ID 48 | type: string 49 | responses: 50 | '200': 51 | description: ok 52 | /bodyParam: 53 | post: 54 | operationId: postBodyParam 55 | summary: Test body parameters 56 | parameters: 57 | - name: str1 58 | in: body 59 | required: true 60 | schema: 61 | $ref: '#/definitions/bodyObject' 62 | responses: 63 | '200': 64 | description: ok 65 | '/noOperationId/{id}': 66 | get: 67 | summary: Test missing operationid 68 | parameters: 69 | - name: id 70 | in: path 71 | required: true 72 | type: integer 73 | responses: 74 | '200': 75 | description: ok 76 | schema: 77 | $ref: '#/definitions/responseObject' 78 | /responses: 79 | get: 80 | operationId: getResponse 81 | summary: Test response serialization 82 | parameters: 83 | - in: query 84 | name: replyType 85 | type: string 86 | responses: 87 | '200': 88 | description: ok 89 | schema: 90 | $ref: '#/definitions/responseObject' 91 | definitions: 92 | bodyObject: 93 | type: object 94 | properties: 95 | str1: 96 | type: string 97 | required: 98 | - str1 99 | responseObject: 100 | type: object 101 | properties: 102 | response: 103 | type: string 104 | required: 105 | - response 106 | 107 | -------------------------------------------------------------------------------- /test/test-warnings.js: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "node:assert/strict"; 2 | import { createRequire } from "node:module"; 3 | import { test } from "node:test"; 4 | import Fastify from "fastify"; 5 | import fastifyOpenapiGlue from "../index.js"; 6 | import { Service } from "./service.js"; 7 | 8 | const service = new Service(); 9 | 10 | const importJSON = createRequire(import.meta.url); 11 | 12 | const testSpec = await importJSON("./test-openapi.v3.json"); 13 | const calls = []; 14 | 15 | function onWarning(w) { 16 | console.log("received warning"); 17 | assert.equal( 18 | w.message, 19 | "The 'service' option is deprecated, use 'serviceHandlers' instead.", 20 | ); 21 | assert.equal(w.name, "DeprecationWarning"); 22 | assert.equal(w.code, "FSTOGDEP001"); 23 | assert.ok(true, "Got warning"); 24 | calls.push(w.code); 25 | } 26 | 27 | async function delay() { 28 | await new Promise((resolve) => setTimeout(resolve, 100)); 29 | } 30 | 31 | test("test if 'service' parameter still works", async (t) => { 32 | process.on("warning", onWarning); 33 | 34 | await t.test("test if warning", async (t) => { 35 | const fastify = Fastify(); 36 | fastify.register(fastifyOpenapiGlue, { 37 | specification: testSpec, 38 | service, 39 | }); 40 | 41 | const res = await fastify.inject({ 42 | method: "GET", 43 | url: "/queryParamObject?int1=1&int2=2", 44 | }); 45 | assert.equal(res.statusCode, 200, "status code ok"); 46 | }); 47 | await delay(); 48 | setImmediate(() => { 49 | process.removeListener("warning", onWarning); 50 | }); 51 | assert.deepEqual(calls, ["FSTOGDEP001"]); 52 | }); 53 | -------------------------------------------------------------------------------- /test/update-checksums.js: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { URL, fileURLToPath } from "node:url"; 3 | import { templateTypes } from "../lib/templates/templateTypes.js"; 4 | const specs = new Set(["./test-swagger.v2", "./test-swagger-noBasePath.v2"]); 5 | const cli = localFile("../bin/openapi-glue-cli.js"); 6 | 7 | function localFile(fileName) { 8 | return fileURLToPath(new URL(fileName, import.meta.url)); 9 | } 10 | 11 | for (const type of templateTypes) { 12 | for (const spec of specs) { 13 | const specFile = localFile(`${spec}.json`); 14 | const checksumFile = localFile(`${spec}.${type}.checksums.json`); 15 | const project = `generated-${type}-project`; 16 | execSync( 17 | `node ${cli} -c -t ${type} -p ${project} ${specFile} > ${checksumFile}`, 18 | ); 19 | } 20 | } 21 | --------------------------------------------------------------------------------