├── .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 | [](https://github.com/seriousme/fastify-openapi-glue/actions?query=workflow%3A%22Node.js+CI%22)
3 | [](https://coveralls.io/github/seriousme/fastify-openapi-glue?branch=master)
4 | [](https://www.npmjs.com/package/fastify-openapi-glue)
5 | 
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 |
--------------------------------------------------------------------------------