Projektstart

This commit is contained in:
2026-01-22 15:49:12 +01:00
parent 7212eb6f7a
commit 57e5f652f8
10637 changed files with 2598792 additions and 64 deletions

5
backend/node_modules/@fastify/swagger/.gitattributes generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Set the default behavior, in case people don't have core.autocrlf set
* text=auto
# Require Unix line endings
* text eol=lf

View File

@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10

View File

@@ -0,0 +1,21 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "discussion"
- "feature request"
- "bug"
- "help wanted"
- "plugin suggestion"
- "good first issue"
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -0,0 +1,3 @@
comment: 'Could you please add tests to make sure this change works as expected?',
fileExtensions: ['.php', '.ts', '.js', '.c', '.cs', '.cpp', '.rb', '.java']
testDir: 'test'

View File

@@ -0,0 +1,33 @@
name: CI
on:
push:
branches:
- main
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
license-check: true
lint: true

23
backend/node_modules/@fastify/swagger/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2017-present The Fastify team
The Fastify team members are listed at https://github.com/fastify/fastify#team.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

177
backend/node_modules/@fastify/swagger/MIGRATION.md generated vendored Normal file
View File

@@ -0,0 +1,177 @@
# Migration
## Migrating from version 7 to 8
As of version 8 `@fastify/swagger` is only responsible for generating valid
swagger/openapi-specifications. The new `@fastify/swagger-ui` plugin is
responsible for serving the swagger-ui frontend.
Options in version 7 of `@fastify/swagger` related to the configuration
of the swagger-ui frontend are now options of `@fastify/swagger-ui`.
The `exposeRoute` option is removed.
Following are the `@fastify/swagger-ui` options:
| Option | Default | Description |
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
| baseDir | undefined | Only relevant if `@fastify/swagger` used in static-mode and additional schema-files contain referenced schemas. Specify the directory where all spec files that are included in the main one using $ref will be located. By default, this is the directory where the main spec file is located. Provided value should be an absolute path without trailing slash. |
| initOAuth | {} | Configuration options for [Swagger UI initOAuth](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). |
| routePrefix | '/documentation' | Overwrite the default Swagger UI route prefix. |
| staticCSP | false | Enable CSP header for static resources. |
| transformStaticCSP | undefined | Synchronous function to transform CSP header for static resources if the header has been previously set. |
| uiConfig | {} | Configuration options for [Swagger UI](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). Must be literal values, see [#5710](https://github.com/swagger-api/swagger-ui/issues/5710).|
| uiHooks | {} | Additional hooks for the documentation's routes. You can provide the `onRequest` and `preHandler` hooks with the same [route's options](https://fastify.dev/docs/latest/Reference/Routes/#options) interface.|
The `baseDir` option is new and is only needed if external spec files should be
exposed. `baseDir` option of `@fastify/swagger-ui` should be set to the same
value as the `specification.baseDir` option of `@fastify/swagger`.
### Example (static-mode):
before:
```js
import Fastify from 'fastify'
import fastifySwagger from '@fastify/swagger'
const fastify = new Fastify()
await fastify.register(fastifySwagger, {
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml',
postProcessor: function(swaggerObject) {
return swaggerObject
},
baseDir: '/path/to/external/spec/files/location',
},
exposeRoute: true,
routePrefix: '/documentation',
initOAuth: { },
uiConfig: {
docExpansion: 'full',
deepLinking: false
},
uiHooks: {
onRequest: function (request, reply, next) { next() },
preHandler: function (request, reply, next) { next() }
},
staticCSP: true,
transformStaticCSP: (header) => header
})
```
after:
```js
import Fastify from 'fastify'
import fastifySwagger from '@fastify/swagger'
import fastifySwaggerUi from '@fastify/swagger-ui'
const fastify = new Fastify()
await fastify.register(fastifySwagger, {
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml',
postProcessor: function(swaggerObject) {
return swaggerObject
},
baseDir: '/path/to/external/spec/files/location'
}
})
await fastify.register(fastifySwaggerUi, {
baseDir: '/path/to/external/spec/files/location',
routePrefix: '/documentation',
initOAuth: { },
uiConfig: {
docExpansion: 'full',
deepLinking: false
},
uiHooks: {
onRequest: function (request, reply, next) { next() },
preHandler: function (request, reply, next) { next() }
},
staticCSP: true,
transformStaticCSP: (header) => header
})
```
### Example (dynamic-mode):
before:
```js
import Fastify from 'fastify'
import fastifySwagger from '@fastify/swagger'
const fastify = new Fastify()
await fastify.register(fastifySwagger, {
mode: 'dynamic',
openapi: {
info: {
title: String,
description: String,
version: String,
},
externalDocs: Object,
servers: [ Object ],
components: Object,
security: [ Object ],
tags: [ Object ]
},
exposeRoute: true,
routePrefix: '/documentation',
initOAuth: { },
uiConfig: {
docExpansion: 'full',
deepLinking: false
},
uiHooks: {
onRequest: function (request, reply, next) { next() },
preHandler: function (request, reply, next) { next() }
},
staticCSP: true,
transformStaticCSP: (header) => header
})
```
after:
```js
import Fastify from 'fastify'
import fastifySwagger from '@fastify/swagger'
import fastifySwaggerUi from '@fastify/swagger-ui'
const fastify = new Fastify()
await fastify.register(fastifySwagger, {
mode: 'dynamic',
openapi: {
info: {
title: String,
description: String,
version: String,
},
externalDocs: Object,
servers: [ Object ],
components: Object,
security: [ Object ],
tags: [ Object ]
}
})
await fastify.register(fastifySwaggerUi, {
routePrefix: '/documentation',
initOAuth: { },
uiConfig: {
docExpansion: 'full',
deepLinking: false
},
uiHooks: {
onRequest: function (request, reply, next) { next() },
preHandler: function (request, reply, next) { next() }
},
staticCSP: true,
transformStaticCSP: (header) => header
})
```

1156
backend/node_modules/@fastify/swagger/README.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
'use strict'
module.exports = require('neostandard')({
ignores: [
...require('neostandard').resolveIgnoresFromGitignore(),
'static'
],
ts: true
})

View File

@@ -0,0 +1,52 @@
'use strict'
const fastify = require('fastify')({
// Need to add a collectionFormat keyword to ajv in fastify instance
ajv: {
customOptions: {
keywords: ['collectionFormat']
}
}
})
fastify.register(require('../index'), {
exposeRoute: true
})
fastify.route({
method: 'GET',
url: '/',
schema: {
querystring: {
type: 'object',
required: ['fields'],
additionalProperties: false,
properties: {
fields: {
type: 'array',
items: {
type: 'string'
},
minItems: 1,
//
// Note that this is an Open API version 2 configuration option. The
// options changed in version 3. The plugin currently only supports
// version 2 of Open API.
//
// Put `collectionFormat` on the same property which you are defining
// as an array of values. (i.e. `collectionFormat` should be a sibling
// of the `type: "array"` specification.)
collectionFormat: 'multi'
}
}
}
},
handler (request, reply) {
reply.send(request.query.fields)
}
})
fastify.listen({ port: 3000 }, (err, addr) => {
if (err) throw err
console.log(`listening on ${addr}`)
})

View File

@@ -0,0 +1,170 @@
'use strict'
const fastify = require('fastify')()
fastify.register(require('../index'), {
openapi: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
servers: [{
url: 'http://localhost'
}],
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
}
}
},
hideUntagged: true,
exposeRoute: true
})
fastify.register(async function (fastify) {
fastify.put('/some-route/:id', {
schema: {
description: 'post some data',
tags: ['user', 'code'],
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
},
default: {
description: 'Default response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
fastify.post('/some-route/:id', {
schema: {
description: 'post some data',
summary: 'qwerty',
security: [{ apiKey: [] }],
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
})
fastify.post('/subscribe', {
schema: {
description: 'subscribe for webhooks',
summary: 'webhook example',
security: [],
response: {
201: {
description: 'Succesful response'
}
},
body: {
type: 'object',
properties: {
callbackUrl: {
type: 'string',
examples: ['https://example.com']
}
}
},
callbacks: {
myEvent: {
'{$request.body#/callbackUrl}': {
post: {
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
message: {
type: 'string',
example: 'Some event happened'
}
},
required: [
'message'
]
}
}
}
},
responses: {
200: {
description: 'Success'
}
}
}
}
}
}
}
})
fastify.listen({ port: 3000 }, err => {
if (err) throw err
})

View File

@@ -0,0 +1,63 @@
'use strict'
const fastify = require('fastify')()
fastify.register(require('../index'), {
swagger: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
host: 'localhost',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json']
},
exposeRoute: true,
routePrefix: '/swagger-docs'
})
fastify.register(async function (fastify) {
fastify.put('/some-route/:id', {
schema: {
description: 'post some data',
tags: ['user', 'code'],
summary: 'qwerty',
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
},
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
}, () => {})
})
fastify.listen({ port: 3000 }, err => {
if (err) throw err
})

View File

@@ -0,0 +1,109 @@
'use strict'
const fastify = require('fastify')()
fastify.register(require('../index'), {
swagger: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
securityDefinitions: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
},
host: 'localhost:3000',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json']
},
hideUntagged: true,
exposeRoute: true
})
fastify.addSchema({
$id: 'user',
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
})
fastify.addSchema({
$id: 'some',
type: 'object',
properties: {
some: { type: 'string' }
}
})
fastify.register(async function (fastify) {
fastify.put('/some-route/:id', {
schema: {
description: 'post some data',
tags: ['user', 'code'],
summary: 'qwerty',
security: [{ apiKey: [] }],
params: { $ref: 'user#' },
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: { $ref: 'some#' }
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
},
default: {
description: 'Default response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
fastify.post('/some-route/:id', {
schema: {
description: 'post some data',
summary: 'qwerty',
security: [{ apiKey: [] }],
params: { $ref: 'user#' },
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: { $ref: 'some#' }
}
},
response: {
201: {
description: 'Succesful response',
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
}, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
})
fastify.listen({ port: 3000 }, err => {
if (err) throw err
})

View File

@@ -0,0 +1,51 @@
{
"openapi": "3.0.0",
"info": {
"description": "Test swagger specification",
"version": "1.0.0",
"title": "Test swagger specification",
"contact": {
"email": "super.developer@gmail.com"
}
},
"servers": [
{
"url": "http://localhost:3000/",
"description": "Localhost (uses test data)"
}
],
"paths": {
"/status": {
"get": {
"description": "Status route, so we can check if server is alive",
"tags": [
"Status"
],
"responses": {
"200": {
"description": "Server is alive",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"health": {
"type": "boolean"
},
"date": {
"type": "string"
}
},
"example": {
"health": true,
"date": "2018-02-19T15:36:46.758Z"
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
openapi: 3.0.0
info:
description: Test swagger specification
version: 1.0.0
title: Test swagger specification
contact:
email: super.developer@gmail.com
servers:
- url: http://localhost:3000/
description: Localhost (uses test data)
paths:
/status:
get:
description: Status route, so we can check if server is alive
tags:
- Status
responses:
"200":
description: Server is alive
content:
application/json:
schema:
type: object
properties:
health:
type: boolean
date:
type: string
example:
health: true
date: 2018-02-19T15:36:46.758Z

View File

@@ -0,0 +1,72 @@
'use strict'
const qs = require('qs')
const Ajv = require('ajv')
const ajv = new Ajv({
removeAdditional: true,
useDefaults: true,
coerceTypes: true
})
const fastify = require('fastify')({
querystringParser: (str) => {
const result = qs.parse(str)
if (result.filter && typeof result.filter === 'string') {
result.filter = JSON.parse(result.filter)
}
return result
}
})
ajv.addKeyword({
keyword: 'x-consume',
code: () => Promise.resolve(true)
})
fastify.setValidatorCompiler(({ schema }) => ajv.compile(schema))
fastify.register(require('../index'), {
openapi: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
}
},
exposeRoute: true
})
fastify.register(async function (fastify) {
fastify.route({
method: 'GET',
url: '/',
schema: {
querystring: {
type: 'object',
required: ['filter'],
additionalProperties: false,
properties: {
filter: {
type: 'object',
required: ['foo'],
properties: {
foo: { type: 'string' },
bar: { type: 'string' }
},
'x-consume': 'application/json'
}
}
}
},
handler (request, reply) {
reply.send(request.query.filter)
}
})
})
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err
})

View File

@@ -0,0 +1,388 @@
'use strict'
const swaggerOption = {
swagger: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
host: 'localhost',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json'],
tags: [
{ name: 'tag' }
],
externalDocs: {
description: 'Find more info here',
url: 'https://swagger.io'
},
securityDefinitions: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
},
security: [{
apiKey: []
}]
}
}
const openapiOption = {
openapi: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
servers: [
{
url: 'http://localhost'
}
],
tags: [
{ name: 'tag' }
],
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
},
bearerAuth: {
type: 'http',
scheme: 'bearer'
}
}
},
security: [{
apiKey: [],
bearerAuth: []
}],
externalDocs: {
description: 'Find more info here',
url: 'https://swagger.io'
}
}
}
const openapiWebHookOption = {
openapi: {
openapi: '3.1.0',
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
servers: [
{
url: 'http://localhost'
}
],
tags: [{ name: 'tag' }],
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
},
schemas: {
Pet: {
require: ['id', 'name'],
properties: {
id: {
type: 'integer',
format: 'int64'
},
name: {
type: 'string'
},
tag: {
type: 'string'
}
}
}
}
},
security: [
{
apiKey: []
}
],
externalDocs: {
description: 'Find more info here',
url: 'https://swagger.io'
},
webhooks: {
newPet: {
post: {
requestBody: {
description: 'Information about a new pet in the system',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Pet'
}
}
}
},
responses: {
200: {
description:
'Return a 200 status to indicate that the data was received successfully'
}
}
}
}
}
}
}
const openapiRelativeOptions = {
openapi: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
servers: [
{
url: '/test'
}
],
tags: [
{ name: 'tag' }
],
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
}
},
security: [{
apiKey: []
}],
externalDocs: {
description: 'Find more info here',
url: 'https://swagger.io'
}
},
stripBasePath: false
}
const schemaQuerystring = {
schema: {
response: {
200: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
},
querystring: {
type: 'object',
properties: {
hello: { type: 'string' },
world: { type: 'string' }
}
}
}
}
const schemaBody = {
schema: {
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' },
constantProp: { const: 'my-const' }
}
}
},
required: ['hello']
}
}
}
const schemaParams = {
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
}
}
}
const schemaHeaders = {
schema: {
headers: {
type: 'object',
properties: {
authorization: {
type: 'string',
description: 'api token'
}
},
required: ['authorization']
}
}
}
const schemaHeadersParams = {
schema: {
headers: {
type: 'object',
properties: {
'x-api-token': {
type: 'string',
description: 'optional api token'
},
'x-api-version': {
type: 'string',
description: 'optional api version'
}
}
},
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'user id'
}
}
}
}
}
const schemaSecurity = {
schema: {
security: [
{
apiKey: []
}
]
}
}
const schemaConsumes = {
schema: {
consumes: ['application/x-www-form-urlencoded'],
body: {
type: 'object',
properties: {
hello: {
description: 'hello',
type: 'string'
}
},
required: ['hello']
}
}
}
const schemaProduces = {
schema: {
produces: ['*/*'],
response: {
200: {
type: 'object',
properties: {
hello: {
description: 'hello',
type: 'string'
}
},
required: ['hello']
}
}
}
}
const schemaCookies = {
schema: {
cookies: {
type: 'object',
properties: {
bar: { type: 'string' }
}
}
}
}
const schemaAllOf = {
schema: {
querystring: {
allOf: [
{
type: 'object',
properties: {
foo: { type: 'string' }
}
}
]
}
}
}
const schemaExtension = {
schema: {
'x-tension': true
}
}
const schemaOperationId = {
schema: {
operationId: 'helloWorld',
response: {
200: {
type: 'object',
properties: {
hello: {
description: 'hello',
type: 'string'
}
},
required: ['hello']
}
}
}
}
module.exports = {
openapiOption,
openapiRelativeOptions,
openapiWebHookOption,
swaggerOption,
schemaQuerystring,
schemaBody,
schemaParams,
schemaHeaders,
schemaHeadersParams,
schemaSecurity,
schemaConsumes,
schemaProduces,
schemaCookies,
schemaAllOf,
schemaExtension,
schemaOperationId
}

View File

@@ -0,0 +1,17 @@
'use strict'
const fastify = require('fastify')({ logger: true })
// const swagger = require('@fastify/swagger')
const swagger = require('..')
fastify.register(swagger, {
mode: 'static',
specification: {
path: './examples/example-static-specification.json'
},
exposeRoute: true
})
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err
})

View File

@@ -0,0 +1,17 @@
'use strict'
const fastify = require('fastify')({ logger: true })
// const swagger = require('@fastify/swagger')
const swagger = require('..')
fastify.register(swagger, {
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml'
},
exposeRoute: true
})
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err
})

View File

@@ -0,0 +1,3 @@
{
"name": "test"
}

197
backend/node_modules/@fastify/swagger/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,197 @@
import { FastifyPluginCallback, FastifySchema, RouteOptions } from 'fastify'
import {
OpenAPI,
OpenAPIV2,
OpenAPIV3,
// eslint-disable-next-line camelcase
OpenAPIV3_1
} from 'openapi-types'
/**
* Swagger-UI Vendor Extensions
* @see https://support.smartbear.com/swaggerhub/docs/apis/vendor-extensions.html#api-docs-x-tokenname
*/
declare module 'openapi-types' {
namespace OpenAPIV3 {
interface OAuth2SecurityScheme {
'x-tokenName'?: string;
}
}
namespace OpenAPIV2 {
interface SecuritySchemeOauth2Base {
'x-tokenName'?: string;
}
}
}
declare module 'fastify' {
interface FastifyInstance {
swagger:
((opts?: { yaml?: false }) => OpenAPI.Document) &
((opts: { yaml: true }) => string) &
((opts: { yaml: boolean }) => OpenAPI.Document | string);
swaggerCSP: {
script: string[];
style: string[];
}
}
interface FastifySchema {
hide?: boolean;
deprecated?: boolean;
tags?: readonly string[];
description?: string;
summary?: string;
consumes?: readonly string[];
produces?: readonly string[];
externalDocs?: OpenAPIV2.ExternalDocumentationObject | OpenAPIV3.ExternalDocumentationObject;
security?: ReadonlyArray<{ [securityLabel: string]: readonly string[] }>;
/**
* OpenAPI operation unique identifier
*/
operationId?: string;
}
interface RouteShorthandOptions {
links?: {
[statusCode: string]: OpenAPIV3.ResponseObject['links'];
}
}
interface FastifyContextConfig {
swaggerTransform?: fastifySwagger.SwaggerTransform | false;
swagger?: {
exposeHeadRoute?: boolean
}
}
}
type SwaggerDocumentObject = {
swaggerObject: Partial<OpenAPIV2.Document>;
} | {
// eslint-disable-next-line camelcase
openapiObject: Partial<OpenAPIV3.Document | OpenAPIV3_1.Document>;
}
type FastifySwagger = FastifyPluginCallback<fastifySwagger.SwaggerOptions>
declare namespace fastifySwagger {
export type SwaggerOptions = (FastifyStaticSwaggerOptions | FastifyDynamicSwaggerOptions)
export interface FastifySwaggerOptions {
mode?: 'static' | 'dynamic';
}
type JSONValue =
| string
| null
| number
| boolean
| JSONObject
| Array<JSONValue>
export interface JSONObject {
[key: string]: JSONValue;
}
export type SwaggerTransform<S extends FastifySchema = FastifySchema> = ({
schema,
url,
route,
...documentObject
}: {
schema: S;
url: string;
route: RouteOptions;
} & SwaggerDocumentObject) => { schema: FastifySchema; url: string }
// eslint-disable-next-line camelcase
export type SwaggerTransformObject = (documentObject: SwaggerDocumentObject) => Partial<OpenAPIV2.Document> | Partial<OpenAPIV3.Document | OpenAPIV3_1.Document>
export interface FastifyDynamicSwaggerOptions extends FastifySwaggerOptions {
mode?: 'dynamic';
swagger?: Partial<OpenAPIV2.Document>;
// eslint-disable-next-line camelcase
openapi?: Partial<OpenAPIV3.Document | OpenAPIV3_1.Document>
hiddenTag?: string;
hideUntagged?: boolean;
/** Include HEAD routes in the definitions */
exposeHeadRoutes?: boolean;
/**
* Strips matching base path from routes in documentation
* @default true
*/
stripBasePath?: boolean;
/**
* custom function to transform the route's schema and url
*/
transform?: SwaggerTransform;
/**
* custom function to transform the openapi or swagger object before it is rendered
*/
transformObject?: SwaggerTransformObject;
/** Overrides the Fastify decorator. */
decorator?: 'swagger' | (string & Record<never, never>);
refResolver?: {
/** Clone the input schema without changing it. Default to `false`. */
clone?: boolean;
buildLocalReference: (
/** The `json` that is being resolved. */
json: JSONObject,
/** The `baseUri` object of the schema. */
baseUri: {
scheme?: string;
userinfo?: string;
host?: string;
port?: number | string;
path?: string;
query?: string;
fragment?: string;
reference?: string;
error?: string;
},
/** `fragment` is the `$ref` string when the `$ref` is a relative reference. */
fragment: string,
/** `i` is a local counter to generate a unique key. */
i: number
) => string;
}
/**
* Whether to convert const definitions to enum definitions.
* const support was added in OpenAPI 3.1, but not all tools support it.
* This option only affects OpenAPI documents.
* @default true
*/
convertConstToEnum?: boolean;
}
export interface StaticPathSpec {
path: string;
postProcessor?: (spec: OpenAPI.Document) => OpenAPI.Document;
baseDir: string;
}
export interface StaticDocumentSpec {
document: OpenAPIV2.Document | OpenAPIV3.Document;
}
export interface FastifyStaticSwaggerOptions extends FastifySwaggerOptions {
mode: 'static';
specification: StaticPathSpec | StaticDocumentSpec;
}
export function formatParamUrl (paramUrl: string): string
export const fastifySwagger: FastifySwagger
export { fastifySwagger as default }
}
declare function fastifySwagger (...params: Parameters<FastifySwagger>): ReturnType<FastifySwagger>
export = fastifySwagger

34
backend/node_modules/@fastify/swagger/index.js generated vendored Normal file
View File

@@ -0,0 +1,34 @@
'use strict'
const fp = require('fastify-plugin')
const { formatParamUrl } = require('./lib/util/format-param-url')
function fastifySwagger (fastify, opts, next) {
// by default the mode is dynamic, as plugin initially was developed
opts.mode = opts.mode || 'dynamic'
switch (opts.mode) {
case 'static': {
const setup = require('./lib/mode/static')
setup(fastify, opts, next)
break
}
case 'dynamic': {
const setup = require('./lib/mode/dynamic')
setup(fastify, opts, next)
break
}
default: {
return next(new Error("unsupported mode, should be one of ['static', 'dynamic']"))
}
}
}
module.exports = fp(fastifySwagger, {
fastify: '5.x',
name: '@fastify/swagger'
})
module.exports.fastifySwagger = fastifySwagger
module.exports.default = fastifySwagger
module.exports.formatParamUrl = formatParamUrl

11
backend/node_modules/@fastify/swagger/lib/constants.js generated vendored Normal file
View File

@@ -0,0 +1,11 @@
'use strict'
const xConsume = 'x-consume'
const xResponseDescription = 'x-response-description'
const xExamples = 'x-examples'
module.exports = {
xConsume,
xResponseDescription,
xExamples
}

View File

@@ -0,0 +1,39 @@
'use strict'
const { addHook } = require('../util/add-hook')
const { resolveSwaggerFunction } = require('../util/resolve-swagger-function')
module.exports = function (fastify, opts, done) {
opts = Object.assign({}, {
exposeRoute: false,
hiddenTag: 'X-HIDDEN',
hideUntagged: false,
stripBasePath: true,
openapi: null,
swagger: {},
transform: null,
transformObject: null,
decorator: 'swagger',
refResolver: {
buildLocalReference (json, _baseUri, _fragment, i) {
if (!json.title && json.$id) {
json.title = json.$id
}
return `def-${i}`
}
},
convertConstToEnum: true
}, opts)
const { routes, Ref } = addHook(fastify, opts)
const cache = {
object: null,
string: null
}
const swagger = resolveSwaggerFunction(opts, cache, routes, Ref)
fastify.decorate(opts.decorator, swagger)
done()
}

View File

@@ -0,0 +1,84 @@
'use strict'
const path = require('node:path')
const fs = require('node:fs')
const yaml = require('yaml')
module.exports = function (fastify, opts, done) {
if (!opts.specification) return done(new Error('specification is missing in the module options'))
if (typeof opts.specification !== 'object') return done(new Error('specification is not an object'))
let swaggerObject = {}
if (!opts.specification.path && !opts.specification.document) {
return done(new Error('both specification.path and specification.document are missing, should be path to the file or swagger document spec'))
} else if (opts.specification.path) {
if (typeof opts.specification.path !== 'string') return done(new Error('specification.path is not a string'))
if (!fs.existsSync(path.resolve(opts.specification.path))) return done(new Error(`${opts.specification.path} does not exist`))
const extName = path.extname(opts.specification.path).toLowerCase()
if (['.yaml', '.json'].indexOf(extName) === -1) return done(new Error("specification.path extension name is not supported, should be one from ['.yaml', '.json']"))
if (opts.specification.postProcessor && typeof opts.specification.postProcessor !== 'function') return done(new Error('specification.postProcessor should be a function'))
if (opts.specification.baseDir && typeof opts.specification.baseDir !== 'string') return done(new Error('specification.baseDir should be string'))
if (!opts.specification.baseDir) {
opts.specification.baseDir = path.resolve(path.dirname(opts.specification.path))
} else {
while (opts.specification.baseDir.endsWith('/')) {
opts.specification.baseDir = opts.specification.baseDir.slice(0, -1)
}
}
// read
const source = fs.readFileSync(
path.resolve(opts.specification.path),
'utf8'
)
switch (extName) {
case '.yaml':
swaggerObject = yaml.parse(source)
break
case '.json':
swaggerObject = JSON.parse(source)
break
}
// apply postProcessor, if one was passed as an argument
if (opts.specification.postProcessor) {
swaggerObject = opts.specification.postProcessor(swaggerObject)
}
} else {
if (typeof opts.specification.document !== 'object') return done(new Error('specification.document is not an object'))
swaggerObject = opts.specification.document
}
fastify.decorate(opts.decorator || 'swagger', swagger)
const cache = {
swaggerObject: null,
swaggerString: null
}
function swagger (opts) {
if (opts?.yaml) {
if (cache.swaggerString) return cache.swaggerString
} else {
if (cache.swaggerObject) return cache.swaggerObject
}
if (opts?.yaml) {
const swaggerString = yaml.stringify(swaggerObject, { strict: false })
cache.swaggerString = swaggerString
return swaggerString
}
cache.swaggerObject = swaggerObject
return swaggerObject
}
done()
}

View File

@@ -0,0 +1,85 @@
'use strict'
const yaml = require('yaml')
const { shouldRouteHide } = require('../../util/should-route-hide')
const { prepareDefaultOptions, prepareOpenapiObject, prepareOpenapiMethod, prepareOpenapiSchemas, normalizeUrl, resolveServerUrls } = require('./utils')
module.exports = function (opts, cache, routes, Ref) {
let ref
const defOpts = prepareDefaultOptions(opts)
return function (opts) {
if (opts?.yaml) {
if (cache.string) return cache.string
} else {
if (cache.object) return cache.object
}
// Base Openapi info
const openapiObject = prepareOpenapiObject(defOpts)
ref = Ref()
openapiObject.components.schemas = prepareOpenapiSchemas(defOpts, {
...openapiObject.components.schemas,
...(ref.definitions().definitions)
}, ref)
const serverUrls = resolveServerUrls(defOpts.servers)
for (const route of routes) {
const transformResult = route.config?.swaggerTransform !== undefined
? route.config.swaggerTransform
? route.config.swaggerTransform({ schema: route.schema, url: route.url, route, openapiObject })
: {}
: defOpts.transform
? defOpts.transform({ schema: route.schema, url: route.url, route, openapiObject })
: {}
const schema = transformResult.schema || route.schema
const shouldRouteHideOpts = {
hiddenTag: defOpts.hiddenTag,
hideUntagged: defOpts.hideUntagged
}
if (shouldRouteHide(schema, shouldRouteHideOpts)) continue
let url = transformResult.url || route.url
url = normalizeUrl(url, serverUrls, defOpts.stripBasePath)
const openapiRoute = Object.assign({}, openapiObject.paths[url])
const openapiMethod = prepareOpenapiMethod(defOpts, schema, ref, openapiObject, url)
if (route.links) {
for (const statusCode of Object.keys(route.links)) {
if (!openapiMethod.responses[statusCode]) {
throw new Error(`missing status code ${statusCode} in route ${route.path}`)
}
openapiMethod.responses[statusCode].links = route.links[statusCode]
}
}
// route.method should be either a String, like 'POST', or an Array of Strings, like ['POST','PUT','PATCH']
const methods = typeof route.method === 'string' ? [route.method] : route.method
for (const method of methods) {
openapiRoute[method.toLowerCase()] = openapiMethod
}
openapiObject.paths[url] = openapiRoute
}
const transformObjectResult = defOpts.transformObject
? defOpts.transformObject({ openapiObject })
: openapiObject
if (opts?.yaml) {
cache.string = yaml.stringify(transformObjectResult, { strict: false })
return cache.string
}
cache.object = transformObjectResult
return cache.object
}
}

View File

@@ -0,0 +1,602 @@
'use strict'
const { readPackageJson } = require('../../util/read-package-json')
const { formatParamUrl } = require('../../util/format-param-url')
const { resolveLocalRef } = require('../../util/resolve-local-ref')
const { resolveSchemaReference } = require('../../util/resolve-schema-reference')
const { xResponseDescription, xConsume, xExamples } = require('../../constants')
const { rawRequired } = require('../../symbols')
const { generateParamsSchema } = require('../../util/generate-params-schema')
const { hasParams } = require('../../util/match-params')
function prepareDefaultOptions (opts) {
const openapi = opts.openapi
const info = openapi.info || null
const servers = openapi.servers || null
const components = openapi.components || null
const security = openapi.security || null
const tags = openapi.tags || null
const externalDocs = openapi.externalDocs || null
const stripBasePath = opts.stripBasePath
const transform = opts.transform
const transformObject = opts.transformObject
const hiddenTag = opts.hiddenTag
const hideUntagged = opts.hideUntagged
const extensions = []
const convertConstToEnum = opts.convertConstToEnum
for (const [key, value] of Object.entries(opts.openapi)) {
if (key.startsWith('x-')) {
extensions.push([key, value])
}
}
return {
...openapi,
info,
servers,
components,
security,
tags,
externalDocs,
stripBasePath,
transform,
transformObject,
hiddenTag,
extensions,
hideUntagged,
convertConstToEnum
}
}
function prepareOpenapiObject (opts) {
const pkg = readPackageJson()
const openapiObject = {
openapi: '3.0.3',
info: {
version: pkg.version || '1.0.0',
title: pkg.name || ''
},
components: { schemas: {} },
paths: {}
}
if (opts.openapi) openapiObject.openapi = opts.openapi
if (opts.info) openapiObject.info = opts.info
if (opts.servers) openapiObject.servers = opts.servers
if (opts.components) openapiObject.components = Object.assign({}, opts.components, { schemas: Object.assign({}, opts.components.schemas) })
if (opts.paths) openapiObject.paths = opts.paths
if (opts.webhooks) openapiObject.webhooks = opts.webhooks
if (opts.security) openapiObject.security = opts.security
if (opts.tags) openapiObject.tags = opts.tags
if (opts.externalDocs) openapiObject.externalDocs = opts.externalDocs
for (const [key, value] of opts.extensions) {
// "x-" extension can not be typed
openapiObject[key] = value
}
return openapiObject
}
function normalizeUrl (url, serverUrls, stripBasePath) {
if (!stripBasePath) return formatParamUrl(url)
serverUrls.forEach(function (serverUrl) {
const basePath = serverUrl.startsWith('/') ? serverUrl : new URL(serverUrl).pathname
if (url.startsWith(basePath) && basePath !== '/') {
url = url.replace(basePath, '')
}
})
return formatParamUrl(url)
}
function resolveServerUrls (servers) {
const resolvedUrls = []
const findVariablesRegex = /\{([^{}]+)\}/gu // As for OpenAPI v3 spec url variables are named in brackets, e.g. {foo}
servers = Array.isArray(servers) ? servers : []
for (const server of servers) {
const originalUrl = server.url
const variables = server.variables
let url = originalUrl
const matches = url.matchAll(findVariablesRegex)
for (const [nameInBrackets, name] of matches) {
const value = variables?.[name]?.default
if (value === undefined) {
throw new Error(`Server URL ${originalUrl} could not be resolved. Make sure to provide a default value for each URL variable.`)
}
url = url.replace(nameInBrackets, value)
}
resolvedUrls.push(url)
}
return resolvedUrls
}
function convertExamplesArrayToObject (examples) {
return examples.reduce((examplesObject, example, index) => {
if (typeof example === 'object') {
examplesObject['example' + (index + 1)] = { value: example }
} else {
examplesObject[example] = { value: example }
}
return examplesObject
}, {})
}
// For supported keys read:
// https://swagger.io/docs/specification/describing-parameters/
function plainJsonObjectToOpenapi3 (opts, container, jsonSchema, externalSchemas, securityIgnores = []) {
const obj = convertJsonSchemaToOpenapi3(opts, resolveLocalRef(jsonSchema, externalSchemas))
let toOpenapiProp
switch (container) {
case 'cookie':
case 'header':
case 'query':
toOpenapiProp = function (propertyName, jsonSchemaElement) {
let result = {
in: container,
name: propertyName,
required: jsonSchemaElement.required
}
const media = schemaToMedia(jsonSchemaElement)
// complex serialization in query or cookie, eg. JSON
// https://swagger.io/docs/specification/describing-parameters/#schema-vs-content
if (jsonSchemaElement[xConsume]) {
media.schema.required = jsonSchemaElement[rawRequired]
result.content = {
[jsonSchemaElement[xConsume]]: media
}
delete result.content[jsonSchemaElement[xConsume]].schema[xConsume]
} else {
result = { ...media, ...result }
}
// description should be optional
if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
// optionally add serialization format style
if (jsonSchema.style) result.style = jsonSchema.style
if (jsonSchema.explode != null) result.explode = jsonSchema.explode
if (jsonSchema.allowReserved === true && container === 'query') {
result.allowReserved = jsonSchema.allowReserved
}
return result
}
break
case 'path':
toOpenapiProp = function (propertyName, jsonSchemaElement) {
const media = schemaToMedia(jsonSchemaElement)
const result = {
...media,
in: container,
name: propertyName,
required: true
}
// description should be optional
if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
return result
}
break
}
return Object.keys(obj)
.filter((propKey) => (!securityIgnores.includes(propKey)))
.map((propKey) => {
const jsonSchema = toOpenapiProp(propKey, obj[propKey])
if (jsonSchema.schema) {
// it is needed as required in schema is invalid prop - delete only if needed
if (jsonSchema.schema.required !== undefined) delete jsonSchema.schema.required
// it is needed as description in schema is invalid prop - delete only if needed
if (jsonSchema.schema.description !== undefined) delete jsonSchema.schema.description
}
return jsonSchema
})
}
const schemaTypeToNestedSchemas = {
object: (schema) => {
return [
...Object.values(schema.properties || {}),
...Object.values(schema.patternProperties || {}),
...Object.values(schema.additionalProperties || {})
]
},
array: (schema) => {
return [
...(schema.items ? [schema.items] : []),
...(schema.contains ? [schema.contains] : [])
]
}
}
function resolveSchemaExamples (schema) {
const example = schema[xExamples] ?? schema.examples?.[0]
if (typeof example !== 'undefined') {
schema.example = example
}
delete schema[xExamples]
delete schema.examples
}
function resolveSchemaExamplesRecursive (schema) {
resolveSchemaExamples(schema)
const getNestedSchemas = schemaTypeToNestedSchemas[schema.type]
const nestedSchemas = getNestedSchemas?.(schema) ?? []
for (const nestedSchema of nestedSchemas) {
resolveSchemaExamplesRecursive(nestedSchema)
}
}
function schemaToMedia (schema) {
const media = { schema }
if (schema.examples?.length === 1) {
media.example = schema.examples[0]
delete schema.examples
} else if (schema.examples?.length > 1) {
media.examples = convertExamplesArrayToObject(schema.examples)
// examples is invalid property of media object schema
delete schema.examples
}
if (schema[xExamples]) {
media.examples = schema[xExamples]
delete schema[xExamples]
}
return media
}
function schemaToMediaRecursive (schema) {
const media = schemaToMedia(schema)
resolveSchemaExamplesRecursive(schema)
return media
}
function resolveBodyParams (opts, body, schema, consumes, ref) {
const resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(schema))
if (resolved.content?.[Object.keys(resolved.content)[0]].schema) {
for (const contentType in schema.content) {
body.content[contentType] = schemaToMediaRecursive(resolved.content[contentType].schema)
}
} else {
if ((Array.isArray(consumes) && consumes.length === 0) || consumes === undefined) {
consumes = ['application/json']
}
const media = schemaToMediaRecursive(resolved)
consumes.forEach((consume) => {
body.content[consume] = media
})
if (resolved?.required?.length) {
body.required = true
}
if (resolved?.description) {
body.description = resolved.description
}
}
}
function resolveCommonParams (opts, container, parameters, schema, ref, sharedSchemas, securityIgnores) {
const schemasPath = '#/components/schemas/'
let resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(schema))
// if the resolved definition is in global schema
if (resolved.$ref?.startsWith(schemasPath)) {
const parts = resolved.$ref.split(schemasPath)
const pathParts = parts[1].split('/')
resolved = pathParts.reduce((resolved, pathPart) => resolved[pathPart], ref.definitions().definitions)
}
const arr = plainJsonObjectToOpenapi3(opts, container, resolved, { ...sharedSchemas, ...ref.definitions().definitions }, securityIgnores)
arr.forEach(swaggerSchema => parameters.push(swaggerSchema))
}
function findReferenceDescription (rawSchema, ref) {
const resolved = resolveSchemaReference(rawSchema, ref)
return resolved?.description
}
// https://swagger.io/docs/specification/describing-responses/
function resolveResponse (opts, fastifyResponseJson, produces, ref) {
// if the user does not provided an out schema
if (!fastifyResponseJson) {
return { 200: { description: 'Default Response' } }
}
const responsesContainer = {}
const statusCodes = Object.keys(fastifyResponseJson)
statusCodes.forEach(statusCode => {
const rawJsonSchema = fastifyResponseJson[statusCode]
const resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(rawJsonSchema))
/**
* 2xx require to be all upper-case
* converts statusCode to upper case only when it is not "default"
*/
if (statusCode !== 'default') {
statusCode = statusCode.toUpperCase()
}
const response = {
description: resolved[xResponseDescription] ||
rawJsonSchema.description ||
findReferenceDescription(rawJsonSchema, ref) ||
'Default Response'
}
// add headers when there are any.
if (rawJsonSchema.headers) {
response.headers = {}
Object.keys(rawJsonSchema.headers).forEach(function (key) {
const header = {
schema: { ...rawJsonSchema.headers[key] }
}
if (rawJsonSchema.headers[key].description) {
header.description = rawJsonSchema.headers[key].description
// remove invalid field
delete header.schema.description
}
response.headers[key] = header
})
// remove invalid field
delete resolved.headers
}
// add schema when type is not 'null'
if (rawJsonSchema.type !== 'null') {
if (resolved.content?.[Object.keys(resolved.content)[0]].schema) {
response.content = resolved.content
} else {
const content = {}
if ((Array.isArray(produces) && produces.length === 0) || produces === undefined) {
produces = ['application/json']
}
delete resolved[xResponseDescription]
const media = schemaToMediaRecursive(resolved)
for (const produce of produces) {
content[produce] = media
}
response.content = content
}
}
responsesContainer[statusCode] = response
})
return responsesContainer
}
function resolveCallbacks (opts, schema, ref) {
const callbacksContainer = {}
// Iterate over each callback event
for (const eventName in schema) {
if (!schema[eventName]) {
continue
}
// Create an empty object to house the future iterations
callbacksContainer[eventName] = {}
const eventSchema = schema[eventName]
// Iterate over each callbackUrl for the event
for (const callbackUrl in eventSchema) {
if (!callbackUrl || !eventSchema[callbackUrl]) {
continue
}
// Create an empty object to house the future iterations
callbacksContainer[eventName][callbackUrl] = {}
const callbackSchema = eventSchema[callbackUrl]
// Iterate over each httpMethod for the callbackUrl
for (const httpMethodName in callbackSchema) {
if (!httpMethodName || !callbackSchema[httpMethodName]) {
continue
}
const httpMethodSchema = callbackSchema[httpMethodName]
const httpMethodContainer = {}
if (httpMethodSchema.requestBody) {
httpMethodContainer.requestBody = convertJsonSchemaToOpenapi3(
opts,
ref.resolve(httpMethodSchema.requestBody)
)
}
// If a response is not provided, set a 2XX default response
httpMethodContainer.responses = httpMethodSchema.responses
? convertJsonSchemaToOpenapi3(opts, ref.resolve(httpMethodSchema.responses))
: { '2XX': { description: 'Default Response' } }
// Set the schema at the appropriate location in the response object
callbacksContainer[eventName][callbackUrl][httpMethodName] = httpMethodContainer
}
}
}
return callbacksContainer
}
function prepareOpenapiMethod (opts, schema, ref, openapiObject, url) {
const openapiMethod = {}
const parameters = []
// Parse out the security prop keys to ignore
const securityIgnores = [
...(openapiObject?.security || []),
...(schema?.security || [])
]
.reduce((acc, securitySchemeGroup) => {
Object.keys(securitySchemeGroup).forEach((securitySchemeLabel) => {
const scheme = openapiObject.components.securitySchemes[securitySchemeLabel]
const isBearer = scheme.type === 'http' && scheme.scheme === 'bearer'
const category = isBearer ? 'header' : scheme.in
const name = isBearer ? 'authorization' : scheme.name
if (!acc[category]) {
acc[category] = []
}
acc[category].push(name)
})
return acc
}, {})
// All the data the user can give us, is via the schema object
if (schema) {
if (schema.operationId) openapiMethod.operationId = schema.operationId
if (schema.summary) openapiMethod.summary = schema.summary
if (schema.tags) openapiMethod.tags = schema.tags
if (schema.description) openapiMethod.description = schema.description
if (schema.externalDocs) openapiMethod.externalDocs = schema.externalDocs
if (schema.querystring) resolveCommonParams(opts, 'query', parameters, schema.querystring, ref, openapiObject.definitions, securityIgnores.query)
if (schema.body) {
openapiMethod.requestBody = { content: {} }
resolveBodyParams(opts, openapiMethod.requestBody, schema.body, schema.consumes, ref)
}
if (schema.params) resolveCommonParams(opts, 'path', parameters, schema.params, ref, openapiObject.definitions)
if (schema.headers) resolveCommonParams(opts, 'header', parameters, schema.headers, ref, openapiObject.definitions, securityIgnores.header)
// TODO: need to documentation, we treat it same as the querystring
// fastify do not support cookies schema in first place
if (schema.cookies) resolveCommonParams(opts, 'cookie', parameters, schema.cookies, ref, openapiObject.definitions, securityIgnores.cookie)
if (parameters.length > 0) openapiMethod.parameters = parameters
if (schema.deprecated) openapiMethod.deprecated = schema.deprecated
if (schema.security) openapiMethod.security = schema.security
if (schema.servers) openapiMethod.servers = schema.servers
if (schema.callbacks) openapiMethod.callbacks = resolveCallbacks(opts, schema.callbacks, ref)
for (const key of Object.keys(schema)) {
if (key.startsWith('x-')) {
openapiMethod[key] = schema[key]
}
}
}
// If there is no schema or schema.params, we need to generate them
if ((!schema || !schema.params) && hasParams(url)) {
const schemaGenerated = generateParamsSchema(url)
resolveCommonParams(opts, 'path', parameters, schemaGenerated.params, ref, openapiObject.definitions)
openapiMethod.parameters = parameters
}
openapiMethod.responses = resolveResponse(opts, schema ? schema.response : null, schema ? schema.produces : null, ref)
return openapiMethod
}
function convertJsonSchemaToOpenapi3 (opts, jsonSchema) {
if (typeof jsonSchema !== 'object' || jsonSchema === null) {
return jsonSchema
}
if (Array.isArray(jsonSchema)) {
return jsonSchema.map((s) => convertJsonSchemaToOpenapi3(opts, s))
}
const openapiSchema = { ...jsonSchema }
if (Object.hasOwn(openapiSchema, '$ref') && Object.keys(openapiSchema).length !== 1) {
for (const key of Object.keys(openapiSchema).filter(k => k !== '$ref')) {
delete openapiSchema[key]
continue
}
}
for (const key of Object.keys(openapiSchema)) {
const value = openapiSchema[key]
if (key === '$id' || key === '$schema' || key === 'definitions') {
// TODO: this breaks references to the definition properties
delete openapiSchema[key]
continue
}
if (key === '$ref') {
openapiSchema.$ref = value.replace('definitions', 'components/schemas')
continue
}
if (opts.convertConstToEnum && key === 'const') {
// OAS 3.1 supports `const` but it is not supported by `swagger-ui`
// https://swagger.io/docs/specification/data-models/keywords/
// TODO: check if enum property already exists
// TODO: this breaks references to the const property
openapiSchema.enum = [openapiSchema.const]
delete openapiSchema.const
continue
}
if (key === 'patternProperties') {
// TODO: check if additionalProperties property already exists
// TODO: this breaks references to the additionalProperties properties
// TODO: patternProperties actually allowed in the openapi schema, but should
// always start with "x-" prefix
const propertyJsonSchema = Object.values(openapiSchema.patternProperties)[0]
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(opts, propertyJsonSchema)
openapiSchema.additionalProperties = propertyOpenapiSchema
delete openapiSchema.patternProperties
continue
}
if (key === 'properties') {
openapiSchema[key] = {}
for (const propertyName of Object.keys(value)) {
const propertyJsonSchema = value[propertyName]
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(opts, propertyJsonSchema)
openapiSchema[key][propertyName] = propertyOpenapiSchema
}
continue
}
openapiSchema[key] = convertJsonSchemaToOpenapi3(opts, value)
}
return openapiSchema
}
function prepareOpenapiSchemas (opts, jsonSchemas, ref) {
const openapiSchemas = {}
for (const schemaName of Object.keys(jsonSchemas)) {
const jsonSchema = { ...jsonSchemas[schemaName] }
const resolvedJsonSchema = ref.resolve(jsonSchema, { externalSchemas: [jsonSchemas] })
const openapiSchema = convertJsonSchemaToOpenapi3(opts, resolvedJsonSchema)
resolveSchemaExamplesRecursive(openapiSchema)
openapiSchemas[schemaName] = openapiSchema
}
return openapiSchemas
}
module.exports = {
prepareDefaultOptions,
prepareOpenapiObject,
prepareOpenapiMethod,
prepareOpenapiSchemas,
resolveServerUrls,
normalizeUrl
}

View File

@@ -0,0 +1,77 @@
'use strict'
const yaml = require('yaml')
const { shouldRouteHide } = require('../../util/should-route-hide')
const { prepareDefaultOptions, prepareSwaggerObject, prepareSwaggerMethod, normalizeUrl, prepareSwaggerDefinitions } = require('./utils')
module.exports = function (opts, cache, routes, Ref) {
let ref
const defOpts = prepareDefaultOptions(opts)
return function (opts) {
if (opts?.yaml) {
if (cache.string) return cache.string
} else {
if (cache.object) return cache.object
}
const swaggerObject = prepareSwaggerObject(defOpts)
ref = Ref()
swaggerObject.definitions = prepareSwaggerDefinitions({
...swaggerObject.definitions,
...(ref.definitions().definitions)
}, ref)
for (const route of routes) {
const transformResult = route.config?.swaggerTransform !== undefined
? route.config.swaggerTransform
? route.config.swaggerTransform({ schema: route.schema, url: route.url, route, swaggerObject })
: {}
: defOpts.transform
? defOpts.transform({ schema: route.schema, url: route.url, route, swaggerObject })
: {}
const schema = transformResult.schema || route.schema
const shouldRouteHideOpts = {
hiddenTag: defOpts.hiddenTag,
hideUntagged: defOpts.hideUntagged
}
if (shouldRouteHide(schema, shouldRouteHideOpts)) continue
let url = transformResult.url || route.url
url = normalizeUrl(url, defOpts.basePath, defOpts.stripBasePath)
const swaggerRoute = Object.assign({}, swaggerObject.paths[url])
const swaggerMethod = prepareSwaggerMethod(schema, ref, swaggerObject, url)
if (route.links) {
throw new Error('Swagger (Open API v2) does not support Links. Upgrade to OpenAPI v3 (see @fastify/swagger readme)')
}
// route.method should be either a String, like 'POST', or an Array of Strings, like ['POST','PUT','PATCH']
const methods = typeof route.method === 'string' ? [route.method] : route.method
for (const method of methods) {
swaggerRoute[method.toLowerCase()] = swaggerMethod
}
swaggerObject.paths[url] = swaggerRoute
}
const transformObjectResult = defOpts.transformObject
? defOpts.transformObject({ swaggerObject })
: swaggerObject
if (opts?.yaml) {
cache.string = yaml.stringify(transformObjectResult, { strict: false })
return cache.string
}
cache.object = transformObjectResult
return cache.object
}
}

View File

@@ -0,0 +1,354 @@
'use strict'
const { readPackageJson } = require('../../util/read-package-json')
const { formatParamUrl } = require('../../util/format-param-url')
const { resolveLocalRef } = require('../../util/resolve-local-ref')
const { resolveSchemaReference } = require('../../util/resolve-schema-reference')
const { xResponseDescription, xConsume } = require('../../constants')
const { generateParamsSchema } = require('../../util/generate-params-schema')
const { hasParams } = require('../../util/match-params')
function prepareDefaultOptions (opts) {
const swagger = opts.swagger
const info = swagger.info || null
const host = swagger.host || null
const schemes = swagger.schemes || null
const consumes = swagger.consumes || null
const produces = swagger.produces || null
const definitions = swagger.definitions || null
const paths = swagger.paths || null
const basePath = swagger.basePath || null
const securityDefinitions = swagger.securityDefinitions || null
const security = swagger.security || null
const tags = swagger.tags || null
const externalDocs = swagger.externalDocs || null
const stripBasePath = opts.stripBasePath
const transform = opts.transform
const transformObject = opts.transformObject
const hiddenTag = opts.hiddenTag
const hideUntagged = opts.hideUntagged
const extensions = []
for (const [key, value] of Object.entries(opts.swagger)) {
if (key.startsWith('x-')) {
extensions.push([key, value])
}
}
return {
info,
host,
schemes,
consumes,
produces,
definitions,
paths,
basePath,
securityDefinitions,
security,
tags,
externalDocs,
stripBasePath,
transform,
transformObject,
hiddenTag,
extensions,
hideUntagged
}
}
function prepareSwaggerObject (opts) {
const pkg = readPackageJson()
const swaggerObject = {
swagger: '2.0',
info: {
version: pkg.version || '1.0.0',
title: pkg.name || ''
},
definitions: {},
paths: {}
}
if (opts.info) swaggerObject.info = opts.info
if (opts.host) swaggerObject.host = opts.host
if (opts.schemes) swaggerObject.schemes = opts.schemes
if (opts.basePath) swaggerObject.basePath = opts.basePath
if (opts.consumes) swaggerObject.consumes = opts.consumes
if (opts.produces) swaggerObject.produces = opts.produces
if (opts.definitions) swaggerObject.definitions = opts.definitions
if (opts.paths) swaggerObject.paths = opts.paths
if (opts.securityDefinitions) swaggerObject.securityDefinitions = opts.securityDefinitions
if (opts.security) swaggerObject.security = opts.security
if (opts.tags) swaggerObject.tags = opts.tags
if (opts.externalDocs) swaggerObject.externalDocs = opts.externalDocs
for (const [key, value] of opts.extensions) {
// "x-" extension can not be typed
swaggerObject[key] = value
}
return swaggerObject
}
function normalizeUrl (url, basePath, stripBasePath) {
let path
if (stripBasePath && url.startsWith(basePath)) {
path = url.replace(basePath, '')
} else {
path = url
}
if (!path.startsWith('/')) {
path = '/' + String(path)
}
return formatParamUrl(path)
}
// For supported keys read:
// https://swagger.io/docs/specification/2-0/describing-parameters/
function plainJsonObjectToSwagger2 (container, jsonSchema, externalSchemas, securityIgnores = []) {
const obj = resolveLocalRef(jsonSchema, externalSchemas)
let toSwaggerProp
switch (container) {
case 'header':
case 'query':
toSwaggerProp = function (propertyName, jsonSchemaElement) {
// complex serialization is not supported by swagger
if (jsonSchemaElement[xConsume]) {
throw new Error('Complex serialization is not supported by Swagger. ' +
'Remove "' + xConsume + '" for "' + propertyName + '" querystring/header schema or ' +
'change specification to OpenAPI')
}
jsonSchemaElement.in = container
jsonSchemaElement.name = propertyName
return jsonSchemaElement
}
break
case 'formData':
toSwaggerProp = function (propertyName, jsonSchemaElement) {
delete jsonSchemaElement.$id
jsonSchemaElement.in = container
jsonSchemaElement.name = propertyName
// https://json-schema.org/understanding-json-schema/reference/non_json_data.html#contentencoding
if (jsonSchemaElement.contentEncoding === 'binary') {
delete jsonSchemaElement.contentEncoding // Must be removed
jsonSchemaElement.type = 'file'
}
return jsonSchemaElement
}
break
case 'path':
toSwaggerProp = function (propertyName, jsonSchemaElement) {
jsonSchemaElement.in = container
jsonSchemaElement.name = propertyName
jsonSchemaElement.required = true
return jsonSchemaElement
}
break
}
return Object.keys(obj)
.filter((propKey) => (!securityIgnores.includes(propKey)))
.map((propKey) => {
return toSwaggerProp(propKey, obj[propKey])
})
}
/*
* Map unsupported JSON schema definitions to Swagger definitions
*/
function replaceUnsupported (jsonSchema) {
if (typeof jsonSchema === 'object' && jsonSchema !== null) {
// Handle patternProperties, that is not part of OpenAPI definitions
if (jsonSchema.patternProperties) {
jsonSchema.additionalProperties = { type: 'string' }
delete jsonSchema.patternProperties
} else if (jsonSchema.const !== undefined) {
// Handle const, that is not part of OpenAPI definitions
jsonSchema.enum = [jsonSchema.const]
delete jsonSchema.const
}
Object.keys(jsonSchema).forEach(function (key) {
jsonSchema[key] = replaceUnsupported(jsonSchema[key])
})
}
return jsonSchema
}
function isConsumesFormOnly (schema) {
const consumes = schema.consumes
return (
consumes &&
consumes.length === 1 &&
(consumes[0] === 'application/x-www-form-urlencoded' ||
consumes[0] === 'multipart/form-data')
)
}
function resolveBodyParams (parameters, schema, ref) {
const resolved = ref.resolve(schema)
replaceUnsupported(resolved)
parameters.push({
name: 'body',
in: 'body',
description: resolved?.description,
schema: resolved
})
}
function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, securityIgnores) {
const resolved = ref.resolve(schema)
const arr = plainJsonObjectToSwagger2(container, resolved, sharedSchemas, securityIgnores)
arr.forEach(swaggerSchema => parameters.push(swaggerSchema))
}
function findReferenceDescription (rawSchema, ref) {
const resolved = resolveSchemaReference(rawSchema, ref)
return resolved?.description
}
// https://swagger.io/docs/specification/2-0/describing-responses/
function resolveResponse (fastifyResponseJson, ref) {
// if the user does not provided an out schema
if (!fastifyResponseJson) {
return { 200: { description: 'Default Response' } }
}
const responsesContainer = {}
const statusCodes = Object.keys(fastifyResponseJson)
statusCodes.forEach(statusCode => {
const rawJsonSchema = fastifyResponseJson[statusCode]
const resolved = ref.resolve(rawJsonSchema)
delete resolved.$schema
// 2xx is not supported by swagger
const deXXStatusCode = statusCode.toUpperCase().replace('XX', '00')
// conflict when we have both 2xx and 200
if (statusCode.toUpperCase().includes('XX') && statusCodes.includes(deXXStatusCode)) {
return
}
// converts statusCode to upper case only when it is not "default"
if (statusCode !== 'default') {
statusCode = deXXStatusCode
}
const response = {
description: rawJsonSchema[xResponseDescription] ||
rawJsonSchema.description ||
findReferenceDescription(rawJsonSchema, ref) ||
'Default Response'
}
// add headers when there are any.
if (rawJsonSchema.headers) {
response.headers = rawJsonSchema.headers
// remove invalid field
delete resolved.headers
}
// add schema when type is not 'null'
if (rawJsonSchema.type !== 'null') {
const schema = { ...resolved }
replaceUnsupported(schema)
delete schema[xResponseDescription]
response.schema = schema
}
responsesContainer[statusCode] = response
})
return responsesContainer
}
function prepareSwaggerMethod (schema, ref, swaggerObject, url) {
const swaggerMethod = {}
const parameters = []
// Parse out the security prop keys to ignore
const securityIgnores = [
...(swaggerObject?.security || []),
...(schema?.security || [])
]
.reduce((acc, securitySchemeGroup) => {
Object.keys(securitySchemeGroup).forEach((securitySchemeLabel) => {
const { name, in: category } = swaggerObject.securityDefinitions[securitySchemeLabel]
if (!acc[category]) {
acc[category] = []
}
acc[category].push(name)
})
return acc
}, {})
// All the data the user can give us, is via the schema object
if (schema) {
if (schema.operationId) swaggerMethod.operationId = schema.operationId
if (schema.summary) swaggerMethod.summary = schema.summary
if (schema.description) swaggerMethod.description = schema.description
if (schema.externalDocs) swaggerMethod.externalDocs = schema.externalDocs
if (schema.tags) swaggerMethod.tags = schema.tags
if (schema.produces) swaggerMethod.produces = schema.produces
if (schema.consumes) swaggerMethod.consumes = schema.consumes
if (schema.querystring) resolveCommonParams('query', parameters, schema.querystring, ref, swaggerObject.definitions, securityIgnores.query)
if (schema.body) {
const isConsumesAllFormOnly = isConsumesFormOnly(schema) || isConsumesFormOnly(swaggerObject)
isConsumesAllFormOnly
? resolveCommonParams('formData', parameters, schema.body, ref, swaggerObject.definitions)
: resolveBodyParams(parameters, schema.body, ref)
}
if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, swaggerObject.definitions)
if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, swaggerObject.definitions, securityIgnores.header)
if (parameters.length > 0) swaggerMethod.parameters = parameters
if (schema.deprecated) swaggerMethod.deprecated = schema.deprecated
if (schema.security) swaggerMethod.security = schema.security
for (const key of Object.keys(schema)) {
if (key.startsWith('x-')) {
swaggerMethod[key] = schema[key]
}
}
}
// If there is no schema or schema.params, we need to generate them
if ((!schema || !schema.params) && hasParams(url)) {
const schemaGenerated = generateParamsSchema(url)
resolveCommonParams('path', parameters, schemaGenerated.params, ref, swaggerObject.definitions)
swaggerMethod.parameters = parameters
}
swaggerMethod.responses = resolveResponse(schema ? schema.response : null, ref)
return swaggerMethod
}
function prepareSwaggerDefinitions (definitions, ref) {
return Object.entries(definitions)
.reduce((res, [name, definition]) => {
const _ = { ...definition }
const resolved = ref.resolve(_, { externalSchemas: [definitions] })
// Swagger doesn't accept $id on /definitions schemas.
// The $ids are needed by Ref() to check the URI so we need
// to remove them at the end of the process
delete resolved.$id
delete resolved.definitions
res[name] = resolved
return res
}, {})
}
module.exports = {
prepareDefaultOptions,
prepareSwaggerObject,
prepareSwaggerMethod,
normalizeUrl,
prepareSwaggerDefinitions
}

7
backend/node_modules/@fastify/swagger/lib/symbols.js generated vendored Normal file
View File

@@ -0,0 +1,7 @@
'use strict'
const rawRequired = Symbol('@fastify/swagger.rawRequired')
module.exports = {
rawRequired
}

View File

@@ -0,0 +1,82 @@
'use strict'
const Ref = require('json-schema-resolver')
const cloner = require('rfdc')({ proto: true, circles: false })
function addHook (fastify, pluginOptions) {
const routes = []
const sharedSchemasMap = new Map()
let hookRun = false
fastify.addHook('onRoute', (routeOptions) => {
const routeConfig = routeOptions.config || {}
const swaggerConfig = routeConfig.swagger || {}
if (routeOptions.method === 'HEAD' && pluginOptions.exposeHeadRoutes !== true && swaggerConfig.exposeHeadRoute !== true) {
return
}
if (
routeOptions.method === 'HEAD' &&
routeOptions.schema !== undefined &&
routeOptions.schema.operationId !== undefined
) {
routes.push(
// If two routes with operationId are added to the swagger
// object, it is no longer valid.
// therefore we suffix the operationId with `-head`.
Object.assign({}, routeOptions, {
schema: Object.assign({}, routeOptions.schema, {
operationId: `${routeOptions.schema.operationId}-head`
})
})
)
return
}
routes.push(routeOptions)
})
fastify.addHook('onRegister', async (instance) => {
// we need to wait the ready event to get all the .getSchemas()
// otherwise it will be empty
// TODO: better handle for schemaId
// when schemaId is the same in difference instance
// the latter will lost
instance.addHook('onReady', (done) => {
const allSchemas = instance.getSchemas()
for (const schemaId of Object.keys(allSchemas)) {
sharedSchemasMap.set(schemaId, allSchemas[schemaId])
}
done()
})
})
fastify.addHook('onReady', (done) => {
hookRun = true
const allSchemas = fastify.getSchemas()
for (const schemaId of Object.keys(allSchemas)) {
// it is the top-level, we do not expect to have duplicate id
sharedSchemasMap.set(schemaId, allSchemas[schemaId])
}
done()
})
return {
routes,
Ref () {
if (hookRun === false) {
throw new Error('.swagger() must be called after .ready()')
}
const externalSchemas = cloner(Array.from(sharedSchemasMap.values()))
return Ref(Object.assign(
{ applicationUri: 'todo.com' },
pluginOptions.refResolver,
{ clone: true, externalSchemas })
)
}
}
}
module.exports = {
addHook
}

View File

@@ -0,0 +1,86 @@
'use strict'
// The swagger standard does not accept the url param with ':'
// so '/user/:id' is not valid.
// This function converts the url in a swagger compliant url string
// => '/user/{id}'
// custom verbs at the end of a url are okay => /user::watch but should be rendered as /user:watch in swagger
const COLON = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
function formatParamUrl (str) {
let i, char
let state = 'skip'
let path = ''
let param = ''
let level = 0
// count for regex if no param exist
let regexp = 0
for (i = 0; i < str.length; i++) {
char = str[i]
switch (state) {
case 'colon': {
// we only accept a-zA-Z0-9_ in param
if (COLON.indexOf(char) !== -1) {
param += char
} else if (char === '(') {
state = 'regexp'
level++
} else {
// end
state = 'skip'
path += '{' + param + '}'
path += char
param = ''
}
break
}
case 'regexp': {
if (char === '(') {
level++
} else if (char === ')') {
level--
}
// we end if the level reach zero
if (level === 0) {
state = 'skip'
if (param === '') {
regexp++
param = 'regexp' + String(regexp)
}
path += '{' + param + '}'
param = ''
}
break
}
default: {
// we check if we need to change state
if (char === ':' && str[i + 1] === ':') {
// double colon -> single colon
path += char
// skip one more
i++
} else if (char === ':') {
// single colon -> state colon
state = 'colon'
} else if (char === '(') {
state = 'regexp'
level++
} else if (char === '*') {
// * -> {*}
// should be exist once only
path += '{*}'
} else {
path += char
}
}
}
}
// clean up
if (state === 'colon' && param !== '') {
path += '{' + param + '}'
}
return path
}
module.exports = {
formatParamUrl
}

View File

@@ -0,0 +1,35 @@
'use strict'
const { matchParams } = require('./match-params')
const namePattern = /\{([^{}]+)\}/u
function paramName (param) {
return param.replace(namePattern, (_, captured) => captured)
}
// Generates default parameters schema from the given URL. (ex: /example/{userId})
function generateParamsSchema (url) {
const params = matchParams(url)
const schema = {
params: {
type: 'object',
properties: {}
}
}
schema.params.properties = params.reduce((acc, param) => {
const name = paramName(param)
acc[name] = {
type: 'string'
}
return acc
}, {})
return schema
}
module.exports = {
generateParamsSchema,
paramName
}

View File

@@ -0,0 +1,18 @@
'use strict'
const paramPattern = /\{[^{}]+\}/gu
function hasParams (url) {
if (!url) return false
return paramPattern.test(url)
}
function matchParams (url) {
if (!url) return []
return url.match(paramPattern) || []
}
module.exports = {
hasParams,
matchParams
}

View File

@@ -0,0 +1,16 @@
'use strict'
const fs = require('node:fs')
const path = require('node:path')
function readPackageJson () {
try {
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json')))
} catch {
return {}
}
}
module.exports = {
readPackageJson
}

View File

@@ -0,0 +1,42 @@
'use strict'
const { rawRequired } = require('../symbols')
const { xConsume } = require('../constants')
function resolveLocalRef (jsonSchema, externalSchemas) {
if (jsonSchema.type !== undefined && jsonSchema.properties !== undefined) {
// for the shorthand querystring/params/headers declaration
const propertiesMap = Object.keys(jsonSchema.properties).reduce((acc, headers) => {
const rewriteProps = {}
rewriteProps.required = (Array.isArray(jsonSchema.required) && jsonSchema.required.indexOf(headers) >= 0) || false
// save raw required for next restore in the content/<media-type>
if (jsonSchema.properties[headers][xConsume]) {
rewriteProps[rawRequired] = jsonSchema.properties[headers].required
}
const newProps = Object.assign({}, jsonSchema.properties[headers], rewriteProps)
return Object.assign({}, acc, { [headers]: newProps })
}, {})
return propertiesMap
}
// for oneOf, anyOf, allOf support in querystring/params/headers
if (jsonSchema.oneOf || jsonSchema.anyOf || jsonSchema.allOf) {
const schemas = jsonSchema.oneOf || jsonSchema.anyOf || jsonSchema.allOf
return schemas.reduce((acc, schema) => Object.assign(acc, resolveLocalRef(schema, externalSchemas)), {})
}
// $ref is in the format: #/definitions/<resolved definition>/<optional fragment>
if (jsonSchema.$ref) {
const localRef = jsonSchema.$ref.split('/', 3)[2]
if (externalSchemas[localRef]) return resolveLocalRef(externalSchemas[localRef], externalSchemas)
// $ref is in the format: #/components/schemas/<resolved definition>
return resolveLocalRef(externalSchemas[jsonSchema.$ref.split('/', 4)[3]], externalSchemas)
}
return jsonSchema
}
module.exports = {
resolveLocalRef
}

View File

@@ -0,0 +1,18 @@
'use strict'
function resolveSchemaReference (rawSchema, ref) {
const resolvedReference = ref.resolve(rawSchema, { externalSchemas: [ref.definitions().definitions] })
// Ref has format `#/definitions/id`
const schemaId = resolvedReference?.$ref?.split('/', 3)[2]
if (schemaId === undefined) {
return undefined
}
return resolvedReference.definitions?.[schemaId]
}
module.exports = {
resolveSchemaReference
}

View File

@@ -0,0 +1,13 @@
'use strict'
function resolveSwaggerFunction (opts, cache, routes, Ref) {
if (opts.openapi === undefined || opts.openapi === null) {
return require('../spec/swagger')(opts, cache, routes, Ref)
} else {
return require('../spec/openapi')(opts, cache, routes, Ref)
}
}
module.exports = {
resolveSwaggerFunction
}

View File

@@ -0,0 +1,25 @@
'use strict'
function shouldRouteHide (schema, opts) {
const { hiddenTag, hideUntagged } = opts
if (schema?.hide) {
return true
}
const tags = schema?.tags || []
if (tags.length === 0 && hideUntagged) {
return true
}
if (tags.includes(hiddenTag)) {
return true
}
return false
}
module.exports = {
shouldRouteHide
}

View File

@@ -0,0 +1,2 @@
# Set default behavior to automatically convert line endings
* text=auto eol=lf

View File

@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10

View File

@@ -0,0 +1,21 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "discussion"
- "feature request"
- "bug"
- "help wanted"
- "plugin suggestion"
- "good first issue"
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -0,0 +1,28 @@
name: CI
on:
push:
branches:
- main
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
permissions:
contents: read
jobs:
test:
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
license-check: true
lint: true

View File

@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2017-present The Fastify team
The Fastify team members are listed at https://github.com/fastify/fastify#team.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,188 @@
# fastify-plugin
[![CI](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/fastify-plugin.svg?style=flat)](https://www.npmjs.com/package/fastify-plugin)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
`fastify-plugin` is a plugin helper for [Fastify](https://github.com/fastify/fastify).
When you build plugins for Fastify and you want them to be accessible in the same context where you require them, you have two ways:
1. Use the `skip-override` hidden property
2. Use this module
__Note: the v4.x series of this module covers Fastify v4__
__Note: the v2.x & v3.x series of this module covers Fastify v3. For Fastify v2 support, refer to the v1.x series.__
## Install
```sh
npm i fastify-plugin
```
## Usage
`fastify-plugin` can do three things for you:
- Add the `skip-override` hidden property
- Check the bare-minimum version of Fastify
- Pass some custom metadata of the plugin to Fastify
Example using a callback:
```js
const fp = require('fastify-plugin')
module.exports = fp(function (fastify, opts, done) {
// your plugin code
done()
})
```
Example using an [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) function:
```js
const fp = require('fastify-plugin')
// A callback function param is not required for async functions
module.exports = fp(async function (fastify, opts) {
// Wait for an async function to fulfill promise before proceeding
await exampleAsyncFunction()
})
```
## Metadata
In addition, if you use this module when creating new plugins, you can declare the dependencies, the name, and the expected Fastify version that your plugin needs.
#### Fastify version
If you need to set a bare-minimum version of Fastify for your plugin, just add the [semver](https://semver.org/) range that you need:
```js
const fp = require('fastify-plugin')
module.exports = fp(function (fastify, opts, done) {
// your plugin code
done()
}, { fastify: '5.x' })
```
If you need to check the Fastify version only, you can pass just the version string.
You can check [here](https://github.com/npm/node-semver#ranges) how to define a `semver` range.
#### Name
Fastify uses this option to validate the dependency graph, allowing it to ensure that no name collisions occur and making it possible to perform [dependency checks](https://github.com/fastify/fastify-plugin#dependencies).
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// your plugin code
done()
}
module.exports = fp(plugin, {
fastify: '5.x',
name: 'your-plugin-name'
})
```
#### Dependencies
You can also check if the `plugins` and `decorators` that your plugin intend to use are present in the dependency graph.
> *Note:* This is the point where registering `name` of the plugins become important, because you can reference `plugin` dependencies by their [name](https://github.com/fastify/fastify-plugin#name).
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// your plugin code
done()
}
module.exports = fp(plugin, {
fastify: '5.x',
decorators: {
fastify: ['plugin1', 'plugin2'],
reply: ['compress']
},
dependencies: ['plugin1-name', 'plugin2-name']
})
```
#### Encapsulate
By default, `fastify-plugin` breaks the [encapsulation](https://github.com/fastify/fastify/blob/HEAD/docs/Reference/Encapsulation.md) but you can optionally keep the plugin encapsulated.
This allows you to set the plugin's name and validate its dependencies without making the plugin accessible.
```js
const fp = require('fastify-plugin')
function plugin (fastify, opts, done) {
// the decorator is not accessible outside this plugin
fastify.decorate('util', function() {})
done()
}
module.exports = fp(plugin, {
name: 'my-encapsulated-plugin',
fastify: '5.x',
decorators: {
fastify: ['plugin1', 'plugin2'],
reply: ['compress']
},
dependencies: ['plugin1-name', 'plugin2-name'],
encapsulate: true
})
```
#### Bundlers and Typescript
`fastify-plugin` adds a `.default` and `[name]` property to the passed in function.
The type definition would have to be updated to leverage this.
## Known Issue: TypeScript Contextual Inference
[Documentation Reference](https://www.typescriptlang.org/docs/handbook/functions.html#inferring-the-types)
It is common for developers to inline their plugin with fastify-plugin such as:
```js
fp((fastify, opts, done) => { done() })
fp(async (fastify, opts) => { return })
```
TypeScript can sometimes infer the types of the arguments for these functions. Plugins in Fastify are recommended to be typed using either `FastifyPluginCallback` or `FastifyPluginAsync`. These two definitions only differ in two ways:
1. The third argument `done` (the callback part)
2. The return type `FastifyPluginCallback` or `FastifyPluginAsync`
At this time, TypeScript inference is not smart enough to differentiate by definition argument length alone.
Thus, if you are a TypeScript developer please use on the following patterns instead:
```ts
// Callback
// Assign type directly
const pluginCallback: FastifyPluginCallback = (fastify, options, done) => { }
fp(pluginCallback)
// or define your own function declaration that satisfies the existing definitions
const pluginCallbackWithTypes = (fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { }
fp(pluginCallbackWithTypes)
// or inline
fp((fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { })
// Async
// Assign type directly
const pluginAsync: FastifyPluginAsync = async (fastify, options) => { }
fp(pluginAsync)
// or define your own function declaration that satisfies the existing definitions
const pluginAsyncWithTypes = async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise<void> => { }
fp(pluginAsyncWithTypes)
// or inline
fp(async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise<void> => { })
```
## Acknowledgments
This project is kindly sponsored by:
- [nearForm](https://nearform.com)
- [LetzDoIt](https://www.letzdoitapp.com/)
## License
Licensed under [MIT](./LICENSE).

View File

@@ -0,0 +1,6 @@
'use strict'
module.exports = require('neostandard')({
ignores: require('neostandard').resolveIgnoresFromGitignore(),
ts: true
})

View File

@@ -0,0 +1,25 @@
'use strict'
const fpStackTracePattern = /at\s(?:.*\.)?plugin\s.*\n\s*(.*)/
const fileNamePattern = /(\w*(\.\w*)*)\..*/
module.exports = function getPluginName (fn) {
if (fn.name.length > 0) return fn.name
const stackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 10
try {
throw new Error('anonymous function')
} catch (e) {
Error.stackTraceLimit = stackTraceLimit
return extractPluginName(e.stack)
}
}
function extractPluginName (stack) {
const m = stack.match(fpStackTracePattern)
// get last section of path and match for filename
return m ? m[1].split(/[/\\]/).slice(-1)[0].match(fileNamePattern)[1] : 'anonymous'
}
module.exports.extractPluginName = extractPluginName

View File

@@ -0,0 +1,10 @@
'use strict'
module.exports = function toCamelCase (name) {
if (name[0] === '@') {
name = name.slice(1).replace('/', '-')
}
return name.replace(/-(.)/g, function (match, g1) {
return g1.toUpperCase()
})
}

View File

@@ -0,0 +1,70 @@
{
"name": "fastify-plugin",
"version": "5.1.0",
"description": "Plugin helper for Fastify",
"main": "plugin.js",
"type": "commonjs",
"types": "types/plugin.d.ts",
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:unit": "c8 --100 node --test",
"test:coverage": "c8 node --test && c8 report --reporter=html",
"test:typescript": "tsd"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fastify-plugin.git"
},
"keywords": [
"plugin",
"helper",
"fastify"
],
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
},
{
"name": "Aras Abbasi",
"email": "aras.abbasi@gmail.com"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fastify-plugin/issues"
},
"homepage": "https://github.com/fastify/fastify-plugin#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"@fastify/type-provider-typebox": "^5.1.0",
"@types/node": "^24.0.8",
"c8": "^10.1.2",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"neostandard": "^0.12.0",
"proxyquire": "^2.1.3",
"tsd": "^0.33.0"
}
}

View File

@@ -0,0 +1,67 @@
'use strict'
const getPluginName = require('./lib/getPluginName')
const toCamelCase = require('./lib/toCamelCase')
let count = 0
function plugin (fn, options = {}) {
let autoName = false
if (fn.default !== undefined) {
// Support for 'export default' behaviour in transpiled ECMAScript module
fn = fn.default
}
if (typeof fn !== 'function') {
throw new TypeError(
`fastify-plugin expects a function, instead got a '${typeof fn}'`
)
}
if (typeof options === 'string') {
options = {
fastify: options
}
}
if (
typeof options !== 'object' ||
Array.isArray(options) ||
options === null
) {
throw new TypeError('The options object should be an object')
}
if (options.fastify !== undefined && typeof options.fastify !== 'string') {
throw new TypeError(`fastify-plugin expects a version string, instead got '${typeof options.fastify}'`)
}
if (!options.name) {
autoName = true
options.name = getPluginName(fn) + '-auto-' + count++
}
fn[Symbol.for('skip-override')] = options.encapsulate !== true
fn[Symbol.for('fastify.display-name')] = options.name
fn[Symbol.for('plugin-meta')] = options
// Faux modules support
if (!fn.default) {
fn.default = fn
}
// TypeScript support for named imports
// See https://github.com/fastify/fastify/issues/2404 for more details
// The type definitions would have to be update to match this.
const camelCase = toCamelCase(options.name)
if (!autoName && !fn[camelCase]) {
fn[camelCase] = fn
}
return fn
}
module.exports = plugin
module.exports.default = plugin
module.exports.fastifyPlugin = plugin

View File

@@ -0,0 +1,110 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('webpack removes require.main.filename', t => {
const filename = require.main.filename
const info = console.info
t.after(() => {
require.main.filename = filename
console.info = info
})
require.main.filename = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('support faux modules', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(plugin.default, plugin)
})
test('support faux modules does not override existing default field in babel module', (t) => {
const module = {
default: (_fastify, _opts, next) => next()
}
module.default.default = 'Existing default field'
const plugin = fp(module)
t.assert.strictEqual(plugin.default, 'Existing default field')
})
test('support ts named imports', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello'
})
t.assert.strictEqual(plugin.hello, plugin)
})
test('from kebab-case to camelCase', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello-world'
})
t.assert.strictEqual(plugin.helloWorld, plugin)
})
test('from @-prefixed named imports', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: '@hello/world'
})
t.assert.strictEqual(plugin.helloWorld, plugin)
})
test('from @-prefixed named kebab-case to camelCase', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: '@hello/my-world'
})
t.assert.strictEqual(plugin.helloMyWorld, plugin)
})
test('from kebab-case to camelCase multiple words', (t) => {
const plugin = fp((_fastify, _opts, next) => {
next()
}, {
name: 'hello-long-world'
})
t.assert.strictEqual(plugin.helloLongWorld, plugin)
})
test('from kebab-case to camelCase multiple words does not override', (t) => {
const fn = (_fastify, _opts, next) => {
next()
}
const foobar = {}
fn.helloLongWorld = foobar
const plugin = fp(fn, {
name: 'hello-long-world'
})
t.assert.strictEqual(plugin.helloLongWorld, foobar)
})

View File

@@ -0,0 +1,67 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('checkVersion having require.main.filename', (t) => {
const info = console.info
t.assert.ok(require.main.filename)
t.after(() => {
console.info = info
})
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('checkVersion having no require.main.filename but process.argv[1]', (t) => {
const filename = require.main.filename
const info = console.info
t.after(() => {
require.main.filename = filename
console.info = info
})
require.main.filename = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})
test('checkVersion having no require.main.filename and no process.argv[1]', (t) => {
const filename = require.main.filename
const argv = process.argv
const info = console.info
t.after(() => {
require.main.filename = filename
process.argv = argv
console.info = info
})
require.main.filename = null
process.argv[1] = null
console.info = function (msg) {
t.assert.fail('logged: ' + msg)
}
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
})

View File

@@ -0,0 +1,14 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('anonymous function should be named composite.test0', (t) => {
t.plan(2)
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'composite.test-auto-0')
})

View File

@@ -0,0 +1,11 @@
import { test } from 'node:test'
import fp from '../../plugin.js'
test('esm base support', (t) => {
fp((_fastify, _opts, next) => {
next()
}, {
fastify: '^5.0.0'
})
t.assert.ok(true, 'fp function called without throwing an error')
})

View File

@@ -0,0 +1,11 @@
'use strict'
// Node v8 throw a `SyntaxError: Unexpected token import`
// even if this branch is never touch in the code,
// by using `eval` we can avoid this issue.
// eslint-disable-next-line
new Function('module', 'return import(module)')('./esm.mjs').catch((err) => {
process.nextTick(() => {
throw err
})
})

View File

@@ -0,0 +1,49 @@
'use strict'
const { test } = require('node:test')
const extractPluginName = require('../lib/getPluginName').extractPluginName
const winStack = `Error: anonymous function
at checkName (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:43:11)
at plugin (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:24:20)
at Test.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\test\\hello.test.js:9:14)
at bound (domain.js:396:14)
at Test.runBound (domain.js:409:12)
at ret (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:278:21)
at Test.main (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:282:7)
at writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:371:13)
at TAP.writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:403:5)
at Test.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:370:14)
at loop (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\function-loop\\index.js:35:15)
at TAP.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:683:7)
at TAP.processSubtest (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:369:12)
at TAP.process (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:306:14)
at TAP.sub (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:185:10)
at TAP.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:209:17)`
const nixStack = `Error: anonymous function
at checkName (/home/leonardo/desktop/fastify-plugin/index.js:43:11)
at plugin (/home/leonardo/desktop/fastify-plugin/index.js:24:20)
at Test.test (/home/leonardo/desktop/fastify-plugin/test/this.is.a.test.js:9:14)
at bound (domain.js:396:14)
at Test.runBound (domain.js:409:12)
at ret (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:278:21)
at Test.main (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:282:7)
at writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:371:13)
at TAP.writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:403:5)
at Test.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:370:14)
at loop (/home/leonardo/desktop/fastify-plugin/node_modules/function-loop/index.js:35:15)
at TAP.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:683:7)
at TAP.processSubtest (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:369:12)
at TAP.process (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:306:14)
at TAP.sub (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:185:10)
at TAP.test (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:209:17)`
const anonymousStack = 'Unable to parse this'
test('extractPluginName tests', (t) => {
t.plan(3)
t.assert.strictEqual(extractPluginName(winStack), 'hello.test')
t.assert.strictEqual(extractPluginName(nixStack), 'this.is.a.test')
t.assert.strictEqual(extractPluginName(anonymousStack), 'anonymous')
})

View File

@@ -0,0 +1,15 @@
'use strict'
const { test } = require('node:test')
const fp = require('../plugin')
test('anonymous function should be named mu1tip1e.composite.test', (t) => {
t.plan(2)
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'mu1tip1e.composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'mu1tip1e.composite.test-auto-0')
})

View File

@@ -0,0 +1,396 @@
'use strict'
const { test } = require('node:test')
const proxyquire = require('proxyquire')
const fp = require('../plugin')
const Fastify = require('fastify')
const pkg = require('../package.json')
test('fastify-plugin is a function', (t) => {
t.plan(1)
t.assert.ok(typeof fp === 'function')
})
test('should return the function with the skip-override Symbol', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin)
t.assert.ok(plugin[Symbol.for('skip-override')])
})
test('should support "default" function from babel module', (t) => {
t.plan(1)
const plugin = {
default: () => { }
}
try {
fp(plugin)
t.assert.ok(true)
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'object\'')
}
})
test('should throw if the plugin is not a function', (t) => {
t.plan(1)
try {
fp('plugin')
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'string\'')
}
})
test('should check the fastify version', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
try {
fp(plugin, { fastify: '>=0.10.0' })
t.assert.ok(true)
} catch {
t.assert.fail()
}
})
test('should check the fastify version', (t) => {
t.plan(1)
function plugin (_fastify, _opts, next) {
next()
}
try {
fp(plugin, '>=0.10.0')
t.assert.ok(true)
} catch {
t.assert.fail()
}
})
test('the options object should be an object', (t) => {
t.plan(2)
try {
fp(() => { }, null)
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
}
try {
fp(() => { }, [])
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
}
})
test('should throw if the version number is not a string', (t) => {
t.plan(1)
try {
fp(() => { }, { fastify: 12 })
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'fastify-plugin expects a version string, instead got \'number\'')
}
})
test('Should accept an option object', (t) => {
t.plan(2)
const opts = { hello: 'world' }
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin, opts)
t.assert.ok(plugin[Symbol.for('skip-override')], 'skip-override symbol should be present')
t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts, 'plugin-meta should match opts')
})
test('Should accept an option object and checks the version', (t) => {
t.plan(2)
const opts = { hello: 'world', fastify: '>=0.10.0' }
function plugin (_fastify, _opts, next) {
next()
}
fp(plugin, opts)
t.assert.ok(plugin[Symbol.for('skip-override')])
t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts)
})
test('should set anonymous function name to file it was called from with a counter', (t) => {
const fp = proxyquire('../plugin.js', { stubs: {} })
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')
const fn2 = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
})
test('should set function name if Error.stackTraceLimit is set to 0', (t) => {
const stackTraceLimit = Error.stackTraceLimit = 0
const fp = proxyquire('../plugin.js', { stubs: {} })
const fn = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')
const fn2 = fp((_fastify, _opts, next) => {
next()
})
t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
Error.stackTraceLimit = stackTraceLimit
})
test('should set display-name to meta name', (t) => {
t.plan(2)
const functionName = 'superDuperSpecialFunction'
const fn = fp((_fastify, _opts, next) => next(), {
name: functionName
})
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, functionName)
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], functionName)
})
test('should preserve fastify version in meta', (t) => {
t.plan(1)
const opts = { hello: 'world', fastify: '>=0.10.0' }
const fn = fp((_fastify, _opts, next) => next(), opts)
t.assert.strictEqual(fn[Symbol.for('plugin-meta')].fastify, '>=0.10.0')
})
test('should check fastify dependency graph - plugin', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
dependencies: ['plugin1-name', 'plugin2-name']
}))
await t.assert.rejects(fastify.ready(), { message: "The dependency 'plugin2-name' of plugin 'test' is not registered" })
})
test('should check fastify dependency graph - decorate', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorate('plugin1', fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
decorators: { fastify: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" })
})
test('should check fastify dependency graph - decorateReply', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorateReply('plugin1', fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'plugin1-name'
}))
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
decorators: { reply: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Reply" })
})
test('should accept an option to encapsulate', async (t) => {
t.plan(3)
const fastify = Fastify()
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('accessible', true)
next()
}, {
name: 'accessible-plugin'
}))
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('alsoAccessible', true)
next()
}, {
name: 'accessible-plugin2',
encapsulate: false
}))
fastify.register(fp((fastify, _opts, next) => {
fastify.decorate('encapsulated', true)
next()
}, {
name: 'encapsulated-plugin',
encapsulate: true
}))
await fastify.ready()
t.assert.ok(fastify.hasDecorator('accessible'))
t.assert.ok(fastify.hasDecorator('alsoAccessible'))
t.assert.ok(!fastify.hasDecorator('encapsulated'))
})
test('should check dependencies when encapsulated', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
name: 'test',
dependencies: ['missing-dependency-name'],
encapsulate: true
}))
await t.assert.rejects(fastify.ready(), { message: "The dependency 'missing-dependency-name' of plugin 'test' is not registered" })
})
test(
'should check version when encapsulated',
{ skip: /\d-.+/.test(pkg.devDependencies.fastify) },
async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fp((_fastify, _opts, next) => next(), {
name: 'test',
fastify: '<=2.10.0',
encapsulate: true
}))
await t.assert.rejects(fastify.ready(), { message: /fastify-plugin: test - expected '<=2.10.0' fastify version, '\d.\d+.\d+' is installed/ })
}
)
test('should check decorators when encapsulated', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.decorate('plugin1', 'foo')
fastify.register(fp((_fastify, _opts, next) => next(), {
fastify: '5.x',
name: 'test',
encapsulate: true,
decorators: { fastify: ['plugin1', 'plugin2'] }
}))
await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" })
})
test('plugin name when encapsulated', async (t) => {
t.plan(6)
const fastify = Fastify()
fastify.register(function plugin (_instance, _opts, next) {
next()
})
fastify.register(fp(getFn('hello'), {
fastify: '5.x',
name: 'hello',
encapsulate: true
}))
fastify.register(function plugin (fastify, _opts, next) {
fastify.register(fp(getFn('deep'), {
fastify: '5.x',
name: 'deep',
encapsulate: true
}))
fastify.register(fp(function genericPlugin (fastify, _opts, next) {
t.assert.strictEqual(fastify.pluginName, 'deep-deep', 'should be deep-deep')
fastify.register(fp(getFn('deep-deep-deep'), {
fastify: '5.x',
name: 'deep-deep-deep',
encapsulate: true
}))
fastify.register(fp(getFn('deep-deep -> not-encapsulated-2'), {
fastify: '5.x',
name: 'not-encapsulated-2'
}))
next()
}, {
fastify: '5.x',
name: 'deep-deep',
encapsulate: true
}))
fastify.register(fp(getFn('plugin -> not-encapsulated'), {
fastify: '5.x',
name: 'not-encapsulated'
}))
next()
})
await fastify.ready()
function getFn (expectedName) {
return function genericPlugin (fastify, _opts, next) {
t.assert.strictEqual(fastify.pluginName, expectedName, `should be ${expectedName}`)
next()
}
}
})

View File

@@ -0,0 +1,24 @@
'use strict'
const { test } = require('node:test')
const toCamelCase = require('../lib/toCamelCase')
test('from kebab-case to camelCase', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('hello-world'), 'helloWorld')
})
test('from @-prefixed named imports', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('@hello/world'), 'helloWorld')
})
test('from @-prefixed named kebab-case to camelCase', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('@hello/my-world'), 'helloMyWorld')
})
test('from kebab-case to camelCase multiple words', (t) => {
t.plan(1)
t.assert.strictEqual(toCamelCase('hello-long-world'), 'helloLongWorld')
})

View File

@@ -0,0 +1,19 @@
import { FastifyPluginAsync } from 'fastify'
type FastifyExampleAsync = FastifyPluginAsync<fastifyExampleAsync.FastifyExampleAsyncOptions>
declare namespace fastifyExampleAsync {
export interface FastifyExampleAsyncOptions {
foo?: 'bar'
}
export interface FastifyExampleAsyncPluginOptions extends FastifyExampleAsyncOptions {
}
export const fastifyExampleAsync: FastifyExampleAsync
export { fastifyExampleAsync as default }
}
declare function fastifyExampleAsync (...params: Parameters<FastifyExampleAsync>): ReturnType<FastifyExampleAsync>
export default fastifyExampleAsync

View File

@@ -0,0 +1,19 @@
import { FastifyPluginCallback } from 'fastify'
type FastifyExampleCallback = FastifyPluginCallback<fastifyExampleCallback.FastifyExampleCallbackOptions>
declare namespace fastifyExampleCallback {
export interface FastifyExampleCallbackOptions {
foo?: 'bar'
}
export interface FastifyExampleCallbackPluginOptions extends FastifyExampleCallbackOptions {
}
export const fastifyExampleCallback: FastifyExampleCallback
export { fastifyExampleCallback as default }
}
declare function fastifyExampleCallback (...params: Parameters<FastifyExampleCallback>): ReturnType<FastifyExampleCallback>
export default fastifyExampleCallback

View File

@@ -0,0 +1,61 @@
/// <reference types="fastify" />
import {
FastifyPluginCallback,
FastifyPluginAsync,
FastifyPluginOptions,
RawServerBase,
RawServerDefault,
FastifyTypeProvider,
FastifyTypeProviderDefault,
FastifyBaseLogger,
} from 'fastify'
type FastifyPlugin = typeof fastifyPlugin
declare namespace fastifyPlugin {
export interface PluginMetadata {
/** Bare-minimum version of Fastify for your plugin, just add the semver range that you need. */
fastify?: string,
name?: string,
/** Decorator dependencies for this plugin */
decorators?: {
fastify?: (string | symbol)[],
reply?: (string | symbol)[],
request?: (string | symbol)[]
},
/** The plugin dependencies */
dependencies?: string[],
encapsulate?: boolean
}
// Exporting PluginOptions for backward compatibility after renaming it to PluginMetadata
/**
* @deprecated Use PluginMetadata instead
*/
export interface PluginOptions extends PluginMetadata {}
export const fastifyPlugin: FastifyPlugin
export { fastifyPlugin as default }
}
/**
* This function does three things for you:
* 1. Add the `skip-override` hidden property
* 2. Check bare-minimum version of Fastify
* 3. Pass some custom metadata of the plugin to Fastify
* @param fn Fastify plugin function
* @param options Optional plugin options
*/
declare function fastifyPlugin<
Options extends FastifyPluginOptions = Record<never, never>,
RawServer extends RawServerBase = RawServerDefault,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
Logger extends FastifyBaseLogger = FastifyBaseLogger,
Fn extends FastifyPluginCallback<Options, RawServer, TypeProvider, Logger> | FastifyPluginAsync<Options, RawServer, TypeProvider, Logger> = FastifyPluginCallback<Options, RawServer, TypeProvider, Logger>
> (
fn: Fn extends unknown ? Fn extends (...args: any) => Promise<any> ? FastifyPluginAsync<Options, RawServer, TypeProvider, Logger> : FastifyPluginCallback<Options, RawServer, TypeProvider, Logger> : Fn,
options?: fastifyPlugin.PluginMetadata | string
): Fn
export = fastifyPlugin

View File

@@ -0,0 +1,166 @@
import fastifyPlugin from '..'
import fastify, { FastifyPluginCallback, FastifyPluginAsync, FastifyError, FastifyInstance, FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger } from 'fastify'
import { expectAssignable, expectError, expectNotType, expectType } from 'tsd'
import { Server } from 'node:https'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import fastifyExampleCallback from './example-callback.test-d'
import fastifyExampleAsync from './example-async.test-d'
interface Options {
foo: string
}
const testSymbol = Symbol('foobar')
// Callback
const pluginCallback: FastifyPluginCallback = (_fastify, _options, _next) => { }
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback))
const pluginCallbackWithTypes = (_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }
expectAssignable<FastifyPluginCallback>(fastifyPlugin(pluginCallbackWithTypes))
expectNotType<any>(fastifyPlugin(pluginCallbackWithTypes))
expectAssignable<FastifyPluginCallback>(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }))
expectNotType<any>(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { }))
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback, ''))
expectType<FastifyPluginCallback>(fastifyPlugin(pluginCallback, {
fastify: '',
name: '',
decorators: {
fastify: ['', testSymbol],
reply: ['', testSymbol],
request: ['', testSymbol]
},
dependencies: [''],
encapsulate: true
}))
const pluginCallbackWithOptions: FastifyPluginCallback<Options> = (_fastify, options, _next) => {
expectType<string>(options.foo)
}
expectType<FastifyPluginCallback<Options>>(fastifyPlugin(pluginCallbackWithOptions))
const pluginCallbackWithServer: FastifyPluginCallback<Options, Server> = (fastify, _options, _next) => {
expectType<Server>(fastify.server)
}
expectType<FastifyPluginCallback<Options, Server>>(fastifyPlugin(pluginCallbackWithServer))
const pluginCallbackWithTypeProvider: FastifyPluginCallback<Options, Server, TypeBoxTypeProvider> = (_fastify, _options, _next) => { }
expectType<FastifyPluginCallback<Options, Server, TypeBoxTypeProvider>>(fastifyPlugin(pluginCallbackWithTypeProvider))
// Async
const pluginAsync: FastifyPluginAsync = async (_fastify, _options) => { }
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync))
const pluginAsyncWithTypes = async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise<void> => { }
expectType<FastifyPluginAsync<FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault>>(fastifyPlugin(pluginAsyncWithTypes))
expectType<FastifyPluginAsync<FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault>>(fastifyPlugin(async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise<void> => { }))
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync, ''))
expectType<FastifyPluginAsync>(fastifyPlugin(pluginAsync, {
fastify: '',
name: '',
decorators: {
fastify: ['', testSymbol],
reply: ['', testSymbol],
request: ['', testSymbol]
},
dependencies: [''],
encapsulate: true
}))
const pluginAsyncWithOptions: FastifyPluginAsync<Options> = async (_fastify, options) => {
expectType<string>(options.foo)
}
expectType<FastifyPluginAsync<Options>>(fastifyPlugin(pluginAsyncWithOptions))
const pluginAsyncWithServer: FastifyPluginAsync<Options, Server> = async (fastify, _options) => {
expectType<Server>(fastify.server)
}
expectType<FastifyPluginAsync<Options, Server>>(fastifyPlugin(pluginAsyncWithServer))
const pluginAsyncWithTypeProvider: FastifyPluginAsync<Options, Server, TypeBoxTypeProvider> = async (_fastify, _options) => { }
expectType<FastifyPluginAsync<Options, Server, TypeBoxTypeProvider>>(fastifyPlugin(pluginAsyncWithTypeProvider))
// Fastify register
const server = fastify()
server.register(fastifyPlugin(pluginCallback))
server.register(fastifyPlugin(pluginCallbackWithTypes), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithOptions), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithServer), { foo: 'bar' })
server.register(fastifyPlugin(pluginCallbackWithTypeProvider), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsync))
server.register(fastifyPlugin(pluginAsyncWithTypes), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithOptions), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithServer), { foo: 'bar' })
server.register(fastifyPlugin(pluginAsyncWithTypeProvider), { foo: 'bar' })
// properly handling callback and async
fastifyPlugin(function (fastify, options, next) {
expectType<FastifyInstance>(fastify)
expectType<Record<never, never>>(options)
expectType<(err?: Error) => void>(next)
})
fastifyPlugin<Options>(function (fastify, options, next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
expectType<(err?: Error) => void>(next)
})
fastifyPlugin<Options>(async function (fastify, options) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
expectAssignable<FastifyPluginAsync<Options, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger>>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { }))
expectNotType<any>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { }))
fastifyPlugin(async function (fastify, options: Options) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
fastifyPlugin(async function (fastify, options) {
expectType<FastifyInstance>(fastify)
expectType<Record<never, never>>(options)
})
expectError(
fastifyPlugin(async function (fastify, options: Options, _next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
})
)
expectAssignable<FastifyPluginCallback<Options>>(fastifyPlugin(function (_fastify, _options, _next) { }))
expectNotType<any>(fastifyPlugin(function (_fastify, _options, _next) { }))
fastifyPlugin(function (fastify, options: Options, next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
expectType<(err?: Error) => void>(next)
})
expectError(
fastifyPlugin(function (fastify, options: Options, _next) {
expectType<FastifyInstance>(fastify)
expectType<Options>(options)
return Promise.resolve()
})
)
server.register(fastifyExampleCallback, { foo: 'bar' })
expectError(server.register(fastifyExampleCallback, { foo: 'baz' }))
server.register(fastifyExampleAsync, { foo: 'bar' })
expectError(server.register(fastifyExampleAsync, { foo: 'baz' }))

89
backend/node_modules/@fastify/swagger/package.json generated vendored Normal file
View File

@@ -0,0 +1,89 @@
{
"name": "@fastify/swagger",
"version": "9.6.1",
"description": "Serve Swagger/OpenAPI documentation for Fastify, supporting dynamic generation",
"main": "index.js",
"type": "commonjs",
"types": "index.d.ts",
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:typescript",
"test:typescript": "tsd",
"test:unit": "c8 --100 node --test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/fastify-swagger.git"
},
"keywords": [
"fastify",
"swagger",
"openapi",
"serve",
"generate",
"static"
],
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
},
{
"name": "Aras Abbasi",
"email": "aras.abbasi@gmail.com"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fastify-swagger/issues"
},
"homepage": "https://github.com/fastify/fastify-swagger#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"@apidevtools/swagger-parser": "^12.0.0",
"@fastify/cookie": "^11.0.1",
"@types/node": "^24.0.10",
"c8": "^10.1.3",
"eslint": "^9.17.0",
"fastify": "^5.0.0",
"fluent-json-schema": "^6.0.0",
"joi": "^17.13.1",
"joi-to-json": "^5.0.0",
"neostandard": "^0.12.0",
"qs": "^6.12.1",
"tsd": "^0.33.0"
},
"dependencies": {
"fastify-plugin": "^5.0.0",
"json-schema-resolver": "^3.0.0",
"openapi-types": "^12.1.3",
"rfdc": "^1.3.1",
"yaml": "^2.4.2"
},
"tsd": {
"directory": "test-types"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,72 @@
import fastify from 'fastify'
import fastifySwagger from '..'
import { minimalOpenApiV3Document } from './minimal-openapiV3-document'
const app = fastify({
http2: true
})
app.register(fastifySwagger)
app.register(fastifySwagger, {})
app.register(fastifySwagger, {
transform: ({ schema, url }) => ({
schema,
url,
})
})
app.register(fastifySwagger, {
mode: 'static',
specification: {
document: minimalOpenApiV3Document
}
})
app.put('/some-route/:id', {
schema: {
description: 'put me some data',
tags: ['user', 'code'],
summary: 'qwerty',
security: [{ apiKey: [] }]
}
}, () => {})
app.get('/public/route', {
schema: {
description: 'returns 200 OK',
summary: 'qwerty',
security: []
}
}, () => {})
app
.register(fastifySwagger, {
swagger: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
externalDocs: {
url: 'https://swagger.io',
description: 'Find more info here'
},
host: 'localhost',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json'],
tags: [
{ name: 'user', description: 'User related end-points' },
{ name: 'code', description: 'Code related end-points' }
],
securityDefinitions: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
}
}
})
.ready(() => {
app.swagger()
})

View File

@@ -0,0 +1,22 @@
import fastify from 'fastify'
import swaggerDefault, { fastifySwagger, SwaggerOptions } from '..'
import * as fastifySwaggerStar from '..'
import { minimalOpenApiV3Document } from './minimal-openapiV3-document'
const app = fastify()
const fastifySwaggerOptions: SwaggerOptions = {
mode: 'static',
specification: {
document: minimalOpenApiV3Document,
}
}
app.register(swaggerDefault, fastifySwaggerOptions)
app.register(fastifySwagger, fastifySwaggerOptions)
app.register(fastifySwaggerStar.default, fastifySwaggerOptions)
app.register(fastifySwaggerStar.fastifySwagger, fastifySwaggerOptions)
app.ready(() => {
app.swagger()
})

View File

@@ -0,0 +1,11 @@
import { OpenAPIV3 } from 'openapi-types'
export const minimalOpenApiV3Document: OpenAPIV3.Document = {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Test OpenApiv3 specification',
},
paths: {
}
}

View File

@@ -0,0 +1,113 @@
import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'
import { expectAssignable } from 'tsd'
expectAssignable<OpenAPIV3.Document>({
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Test OpenApiv3 specification',
},
components: {
securitySchemes: {
myAuth: {
type: 'oauth2',
'x-tokenName': 'id_token',
flows: {
implicit: {
authorizationUrl: 'http.../login/oauth/authorize',
scopes: {},
},
},
},
}
},
paths: {}
})
expectAssignable<OpenAPIV2.Document>({
swagger: '2.0.0',
info: {
title: 'Test OpenApiv2 specification',
version: '2.0.0'
},
securityDefinitions: {
OAuth2AccessCodeFlow: {
type: 'oauth2',
flow: 'accessCode',
authorizationUrl: 'https://example.com/oauth/authorize',
tokenUrl: 'https://example.com/oauth/token',
'x-tokenName': 'id_token',
scopes: { }
},
OAuth2ApplicationFlow: {
type: 'oauth2',
flow: 'application',
tokenUrl: 'https://example.com/oauth/token',
'x-tokenName': 'id_token',
scopes: { }
},
OAuth2ImplicitFlow: {
type: 'oauth2',
flow: 'implicit',
authorizationUrl: 'https://example.com/oauth/authorize',
'x-tokenName': 'id_token',
scopes: { }
},
OAuth2PasswordFlow: {
type: 'oauth2',
flow: 'password',
tokenUrl: 'https://example.com/oauth/token',
'x-tokenName': 'id_token',
scopes: { }
},
},
paths: {}
})
expectAssignable<OpenAPIV2.Document>({
swagger: '2.0.0',
info: {
title: 'Test OpenApiv2 specification',
version: '2.0.0'
},
paths: {
'/users/{userId}': {
get: {
summary: 'Gets a user by ID.',
responses: {
},
parameters: [
{
in: 'path',
name: 'userId',
type: 'integer',
required: true,
description: 'Numeric ID of the user to get.',
'x-example': 'BADC0FFEE'
},
{
in: 'query',
name: 'offset',
type: 'integer',
description: 'The number of items to skip before starting to collect the result set.',
'x-example': 1337
},
{
in: 'header',
name: 'X-Request-ID',
type: 'string',
required: true,
'x-example': 'wget'
},
{
in: 'formData',
name: 'name',
type: 'string',
description: "A person's name.",
'x-example': 'John Doe'
}
]
}
}
}
})

View File

@@ -0,0 +1,272 @@
import fastify, { FastifySchema, RouteOptions } from 'fastify'
import fastifySwagger, {
formatParamUrl,
SwaggerOptions,
} from '..'
import { minimalOpenApiV3Document } from './minimal-openapiV3-document'
import { expectType } from 'tsd'
import {
OpenAPI,
OpenAPIV2,
OpenAPIV3,
// eslint-disable-next-line camelcase
OpenAPIV3_1
} from 'openapi-types'
const app = fastify()
app.register(fastifySwagger)
app.register(fastifySwagger, {})
app.register(fastifySwagger, {
transform: ({ schema, url }) => ({
schema,
url,
})
})
app.register(fastifySwagger, {
mode: 'static',
specification: {
document: minimalOpenApiV3Document
}
})
app.register(fastifySwagger, { convertConstToEnum: false })
const fastifySwaggerOptions: SwaggerOptions = {
mode: 'static',
specification: {
document: minimalOpenApiV3Document
}
}
app.register(fastifySwagger, fastifySwaggerOptions)
const fastifyDynamicSwaggerOptions: SwaggerOptions = {
mode: 'dynamic',
hiddenTag: 'X-HIDDEN',
hideUntagged: true,
stripBasePath: true,
refResolver: {
buildLocalReference: (_json, _baseUri, fragment, i) => `${fragment}-${i}`
}
}
app.register(fastifySwagger, fastifyDynamicSwaggerOptions)
app.get('/deprecated', {
schema: {
deprecated: true,
hide: true
}
}, () => {})
app.put('/some-route/:id', {
schema: {
description: 'put me some data',
tags: ['user', 'code'],
summary: 'qwerty',
consumes: ['application/json', 'multipart/form-data'],
security: [{ apiKey: [] }],
operationId: 'opeId',
externalDocs: {
url: 'https://swagger.io',
description: 'Find more info here'
},
}
}, () => {})
app.put('/image.png', {
schema: {
description: 'returns an image',
summary: 'qwerty',
consumes: ['application/json', 'multipart/form-data'],
produces: ['image/png'],
response: {
200: {
type: 'string',
format: 'binary'
}
}
}
}, async (_req, reply) => {
reply
.type('image/png')
.send(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAAgSURBVBhXY/iPCkB8BgYkEiSIBICiCCEoB0SBwf///wGHRzXLSklJLQAAAABJRU5ErkJggg==', 'base64'))
})
app.get('/public/route', {
schema: {
description: 'returns 200 OK',
summary: 'qwerty',
security: [],
response: { 200: {} }
},
links: {
200: { 'some-route': { operationId: 'opeId' } }
}
}, () => {})
app.get('/public/readonly-schema-route', {
schema: {
description: 'returns 200 OK',
tags: ['foo'],
summary: 'qwerty',
security: [],
response: { 200: {} }
},
links: {
200: { 'some-route': { operationId: 'opeId' } }
}
} as const, () => {})
app
.register(fastifySwagger, {
swagger: {
info: {
title: 'Test swagger',
description: 'testing the fastify swagger api',
version: '0.1.0'
},
externalDocs: {
url: 'https://swagger.io',
description: 'Find more info here'
},
host: 'localhost',
schemes: ['http'],
consumes: ['application/json'],
produces: ['application/json'],
tags: [
{ name: 'user', description: 'User related end-points' },
{ name: 'code', description: 'Code related end-points' }
],
securityDefinitions: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header'
}
}
}
})
.ready(() => {
app.swagger()
})
app
.register(fastifySwagger, {
openapi: {
info: {
title: 'Test openapi',
description: 'testing the fastify swagger api',
version: '0.1.0',
},
servers: [{ url: 'http://localhost' }],
externalDocs: {
url: 'https://swagger.io',
description: 'Find more info here',
},
components: {
schemas: {},
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'header',
},
},
},
}
})
.ready(() => {
app.swagger()
})
app.register(fastifySwagger, {
openapi: {
components: {
schemas: {
Model: {
type: 'object',
properties: {
name: { type: 'null' },
},
required: ['name']
}
}
}
},
})
.ready(() => {
app.swagger()
})
app.register(fastifySwagger, {
})
.ready(() => {
app.swagger()
})
app.get(
'/endpoint-transform-function',
{
config: {
swaggerTransform: ({
schema,
url,
route,
...documentObject
}) => {
schema satisfies FastifySchema
url satisfies string
route satisfies RouteOptions
// eslint-disable-next-line camelcase
documentObject satisfies { swaggerObject: Partial<OpenAPIV2.Document> } | { openapiObject: Partial<OpenAPIV3.Document | OpenAPIV3_1.Document> }
return { schema, url }
},
},
},
() => {}
)
app.get(
'/endpoint-transform-false',
{
config: {
swaggerTransform: false,
},
},
() => {}
)
expectType<OpenAPI.Document>(app.swagger())
expectType<OpenAPI.Document>(app.swagger({ yaml: false }))
expectType<string>(app.swagger({ yaml: true }))
expectType<OpenAPI.Document | string>(app.swagger({ yaml: Boolean(process.env.YAML) }))
expectType<(arg: string)=>string>(formatParamUrl)
app.register(fastifySwagger, {
decorator: 'swagger'
})
app.register(fastifySwagger, {
decorator: 'customSwagger'
})
app.register(fastifySwagger, {
exposeHeadRoutes: true
})
app.register(fastifySwagger, {
exposeHeadRoutes: false
})
app.get(
'/endpoint-expose-head-route',
{
config: {
swagger: {
exposeHeadRoute: true
}
},
},
() => {}
)

View File

@@ -0,0 +1,45 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const fastifySwagger = require('../index')
test('fastify.swagger should exist', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger)
await fastify.ready()
t.assert.ok(fastify.swagger)
})
test('fastify.swagger should throw if called before ready', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger)
t.assert.throws(fastify.swagger.bind(fastify))
})
test('fastify.swagger should throw if called before ready (openapi)', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: {}
})
t.assert.throws(fastify.swagger.bind(fastify))
})
test('decorator can be overridden', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, { decorator: 'customSwaggerDecorator' })
await fastify.ready()
t.assert.ok(fastify.customSwaggerDecorator())
})

14
backend/node_modules/@fastify/swagger/test/esm/esm.mjs generated vendored Normal file
View File

@@ -0,0 +1,14 @@
import t from 'node:test'
import Fastify from 'fastify'
import swaggerDefault from '../../index.js'
t.test('esm support', async t => {
const fastify = Fastify()
await fastify.register(swaggerDefault)
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.swagger, '2.0')
})

View File

@@ -0,0 +1,11 @@
'use strict'
// Node v8 throw a `SyntaxError: Unexpected token import`
// even if this branch is never touch in the code,
// by using `eval` we can avoid this issue.
// eslint-disable-next-line
new Function('module', 'return import(module)')('./esm.mjs').catch((err) => {
process.nextTick(() => {
throw err
})
})

View File

@@ -0,0 +1,510 @@
'use strict'
const path = require('node:path')
const { test } = require('node:test')
const Fastify = require('fastify')
const fastifySwagger = require('../../index')
const fastifySwaggerDynamic = require('../../lib/mode/dynamic')
const Swagger = require('@apidevtools/swagger-parser')
const readFileSync = require('node:fs').readFileSync
const resolve = require('node:path').resolve
const yaml = require('yaml')
test('specification validation check works', async (t) => {
const specificationTests = [
{
specification: '',
error: 'Error: specification is missing in the module options'
},
{
specification: '123',
error: 'Error: specification is not an object'
},
{
specification: {},
error: 'Error: both specification.path and specification.document are missing, should be path to the file or swagger document spec'
},
{
specification: {
path: 123
},
error: 'Error: specification.path is not a string'
},
{
specification: {
path: '/hello/lionel.richie'
},
error: 'Error: /hello/lionel.richie does not exist'
},
{
specification: {
path: './examples/example-static-specification.yaml',
postProcessor: 'hello'
},
error: 'Error: specification.postProcessor should be a function'
}
]
t.plan(specificationTests.length * 2)
for (const specificationTest of specificationTests) {
try {
const fastify = Fastify()
await fastify.register(fastifySwagger, {
mode: 'static',
specification: specificationTest.specification
})
} catch (err) {
t.assert.notEqual(err, undefined)
t.assert.strictEqual(err.toString(), specificationTest.error)
}
}
})
test('registering plugin with invalid mode throws an error', async (t) => {
const config = {
mode: 'error'
}
t.plan(1)
const fastify = Fastify()
try {
await fastify.register(fastifySwagger, config)
} catch (err) {
t.assert.strictEqual(err.message, 'unsupported mode, should be one of [\'static\', \'dynamic\']')
}
})
test('unsupported file extension in specification.path throws an error', async (t) => {
const config = {
mode: 'static',
specification: {
path: './examples/example-static-specification.js'
}
}
t.plan(1)
const fastify = Fastify()
try {
await fastify.register(fastifySwagger, config)
} catch (err) {
t.assert.strictEqual(err.message, 'specification.path extension name is not supported, should be one from [\'.yaml\', \'.json\']')
}
})
test('non-string specification.baseDir throws an error ', async (t) => {
const config = {
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml',
baseDir: 1
}
}
t.plan(1)
const fastify = Fastify()
try {
await fastify.register(fastifySwagger, config)
} catch (err) {
t.assert.strictEqual(err.message, 'specification.baseDir should be string')
}
})
test('valid specification.baseDir is handled properly /1', async (t) => {
const config = {
mode: 'static',
specification: {
path: './examples/example-static-specification.json',
baseDir: __dirname
}
}
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
await fastify.ready()
t.assert.deepStrictEqual(
JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'examples', 'example-static-specification.json'), 'utf8')),
fastify.swagger({ json: true })
)
})
test('valid specification.baseDir is handled properly /2', async (t) => {
const config = {
mode: 'static',
specification: {
path: './examples/example-static-specification.json',
baseDir: __dirname + '/' // eslint-disable-line n/no-path-concat
}
}
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
await fastify.ready()
t.assert.deepStrictEqual(
JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'examples', 'example-static-specification.json'), 'utf8')),
fastify.swagger({ json: true })
)
})
test('valid yaml-specification is converted properly to json', async (t) => {
const config = {
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml'
}
}
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
await fastify.ready()
t.assert.deepStrictEqual(
JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'examples', 'example-static-specification.json'), 'utf8')),
fastify.swagger()
)
})
test('valid specification yaml is properly handled as yaml', async (t) => {
const config = {
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml',
baseDir: __dirname
}
}
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
await fastify.ready()
t.assert.strictEqual(
readFileSync(resolve(__dirname, '..', '..', 'examples', 'example-static-specification.yaml'), 'utf8'),
fastify.swagger({ yaml: true })
)
})
test('non-object specification.document throws an error', async (t) => {
const config = {
mode: 'static',
specification: {
document: 'doc'
}
}
t.plan(1)
const fastify = new Fastify()
await t.assert.rejects(async () => await fastify.register(fastifySwagger, config), new Error('specification.document is not an object'))
})
test('object specification.document', async (t) => {
const config = {
mode: 'static',
specification: {
document: {
type: 'object'
}
}
}
t.plan(1)
const fastify = new Fastify()
fastify.register(fastifySwagger, config)
await fastify.ready()
t.assert.deepStrictEqual(fastify.swagger(), { type: 'object' })
})
test('inserts default opts in fastifySwagger', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger)
t.assert.ok(true, 'Inserted default option for fastifySwagger.')
})
test('inserts default package name', async (t) => {
const config = {
mode: 'dynamic',
specification: {
path: './examples/example-static-specification.json'
}
}
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
const originalPathJoin = path.join
const testPackageJSON = path.join(__dirname, '../../examples/test-package.json')
path.join = (...args) => {
if (args[3] === 'package.json') {
return testPackageJSON
}
return originalPathJoin(...args)
}
await fastify.ready()
t.assert.deepStrictEqual(fastify.swagger(), {
swagger: '2.0',
info: { version: '1.0.0', title: 'test' },
definitions: {},
paths: {}
})
})
test('inserts default package name - openapi', async (t) => {
const config = {
mode: 'dynamic',
openapi: {
servers: []
},
specification: {
path: './examples/example-static-specification.json'
}
}
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
const originalPathJoin = path.join
const testPackageJSON = path.join(__dirname, '../../examples/test-package.json')
path.join = (...args) => {
if (args[3] === 'package.json') {
return testPackageJSON
}
return originalPathJoin(...args)
}
await fastify.ready()
t.assert.deepStrictEqual(fastify.swagger(), {
openapi: '3.0.3',
info: { version: '1.0.0', title: 'test' },
components: { schemas: {} },
paths: {},
servers: []
})
})
test('throws an error if cannot parse package\'s JSON', async (t) => {
const config = {
mode: 'dynamic',
specification: {
path: './examples/example-static-specification.json'
}
}
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
const originalPathJoin = path.join
const testPackageJSON = path.join(__dirname, '')
path.join = (...args) => {
if (args[3] === 'package.json') {
return testPackageJSON
}
return originalPathJoin(...args)
}
await fastify.ready()
t.assert.deepStrictEqual(fastify.swagger(), {
swagger: '2.0',
info: { version: '1.0.0', title: '' },
definitions: {},
paths: {}
})
})
test('throws an error if cannot parse package\'s JSON - openapi', async (t) => {
const config = {
mode: 'dynamic',
openapi: {
servers: []
},
specification: {
path: './examples/example-static-specification.json'
}
}
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
const originalPathJoin = path.join
const testPackageJSON = path.join(__dirname, '')
path.join = (...args) => {
if (args[3] === 'package.json') {
return testPackageJSON
}
return originalPathJoin(...args)
}
await fastify.ready()
t.assert.deepStrictEqual(fastify.swagger(), {
openapi: '3.0.3',
info: { version: '1.0.0', title: '' },
components: { schemas: {} },
paths: {},
servers: []
})
})
test('inserts default opts in fastifySwaggerDynamic (dynamic.js)', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwaggerDynamic)
t.assert.ok(true, 'Inserted default option for fastifySwagger.')
})
test('should still return valid swagger object when missing package.json', async (t) => {
const config = {
mode: 'dynamic',
specification: {
path: './examples/example-static-specification.json'
}
}
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
const originalPathJoin = path.join
const testPackageJSON = path.join(__dirname, 'missing.json')
path.join = (...args) => {
if (args[3] === 'package.json') {
return testPackageJSON
}
return originalPathJoin(...args)
}
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(typeof swaggerObject, 'object')
await Swagger.validate(swaggerObject)
t.assert.ok(true, 'Swagger object is still valid.')
})
test('.swagger() returns cache.swaggerObject on second request in static mode', async (t) => {
const config = {
mode: 'static',
specification: {
path: './examples/example-static-specification.json'
}
}
t.plan(3)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
await fastify.ready()
const swaggerJson1 = fastify.swagger()
t.assert.strictEqual(typeof swaggerJson1, 'object')
const swaggerJson2 = fastify.swagger()
t.assert.strictEqual(typeof swaggerJson2, 'object')
t.assert.strictEqual(swaggerJson1, swaggerJson2)
})
test('.swagger({ yaml: true }) returns cache.swaggerString on second request in static mode', async (t) => {
const config = {
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml'
}
}
t.plan(3)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
await fastify.ready()
const swaggerYaml1 = fastify.swagger({ yaml: true })
t.assert.strictEqual(typeof swaggerYaml1, 'string')
const swaggerYaml2 = fastify.swagger({ yaml: true })
t.assert.strictEqual(typeof swaggerYaml2, 'string')
t.assert.strictEqual(swaggerYaml1, swaggerYaml2)
})
test('postProcessor works, swagger route returns updated yaml', async (t) => {
const config = {
mode: 'static',
specification: {
path: './examples/example-static-specification.yaml',
postProcessor: function (swaggerObject) {
swaggerObject.servers[0].url = 'http://localhost:4000/'
return swaggerObject
}
}
}
const expectedYaml = `openapi: 3.0.0
info:
description: Test swagger specification
version: 1.0.0
title: Test swagger specification
contact:
email: super.developer@gmail.com
servers:
- url: http://localhost:4000/
description: Localhost (uses test data)
paths:
/status:
get:
description: Status route, so we can check if server is alive
tags:
- Status
responses:
"200":
description: Server is alive
content:
application/json:
schema:
type: object
properties:
health:
type: boolean
date:
type: string
example:
health: true
date: 2018-02-19T15:36:46.758Z
`
t.plan(3)
const fastify = Fastify()
await fastify.register(fastifySwagger, config)
await fastify.ready()
// check that yaml is there
const res = fastify.swagger({ yaml: true })
t.assert.strictEqual(typeof res, 'string')
yaml.parse(res)
t.assert.strictEqual(res, expectedYaml)
t.assert.ok(true, 'valid swagger yaml')
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,564 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const Swagger = require('@apidevtools/swagger-parser')
const fastifySwagger = require('../../../index')
const openapiOption = {
openapi: {},
refResolver: {
buildLocalReference: (json, _baseUri, _fragment, i) => {
return json.$id || `def-${i}`
}
}
}
test('support $ref schema', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'Order', type: 'object', properties: { id: { type: 'integer', examples: [25] } } })
instance.post('/', { schema: { body: { $ref: 'Order#' }, response: { 200: { $ref: 'Order#' } } } }, () => {})
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
t.assert.deepStrictEqual(Object.keys(openapiObject.components.schemas), ['Order'])
t.assert.strictEqual(openapiObject.components.schemas.Order.properties.id.example, 25)
await Swagger.validate(openapiObject)
})
test('support $ref relative pointers in params', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.register(async (instance) => {
instance.addSchema({
$id: 'Order',
type: 'object',
properties: {
OrderId: {
type: 'object',
properties: {
id: {
type: 'string'
}
}
}
}
})
instance.get('/:id', { schema: { params: { $ref: 'Order#/properties/OrderId' }, response: { 200: { $ref: 'Order#' } } } }, () => {})
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
t.assert.deepStrictEqual(Object.keys(openapiObject.components.schemas), ['Order'])
await Swagger.validate(openapiObject)
})
test('support nested $ref schema : simple test', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'OrderItem', type: 'object', properties: { id: { type: 'integer' } }, examples: [{ id: 1 }] })
instance.addSchema({ $id: 'ProductItem', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'Order', type: 'object', properties: { products: { type: 'array', items: { $ref: 'OrderItem' } } } })
instance.post('/', { schema: { body: { $ref: 'Order' }, response: { 200: { $ref: 'Order' } } } }, () => {})
instance.post('/other', { schema: { body: { $ref: 'ProductItem' } } }, () => {})
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const schemas = openapiObject.components.schemas
t.assert.deepStrictEqual(Object.keys(schemas), ['OrderItem', 'ProductItem', 'Order'])
// ref must be prefixed by '#/components/schemas/'
t.assert.strictEqual(schemas.Order.properties.products.items.$ref, '#/components/schemas/OrderItem')
t.assert.deepStrictEqual(schemas.OrderItem.example, { id: 1 })
await Swagger.validate(openapiObject)
})
test('support nested $ref schema : complex case', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'schemaB', type: 'object', properties: { id: { type: 'string', examples: ['ABC'] } } })
instance.addSchema({ $id: 'schemaC', type: 'object', properties: { a: { type: 'array', items: { $ref: 'schemaA' } } } })
instance.addSchema({ $id: 'schemaD', type: 'object', properties: { b: { $ref: 'schemaB' }, c: { $ref: 'schemaC' } } })
instance.post('/url1', { schema: { body: { $ref: 'schemaD' }, response: { 200: { $ref: 'schemaB' } } } }, () => {})
instance.post('/url2', { schema: { body: { $ref: 'schemaC' }, response: { 200: { $ref: 'schemaA' } } } }, () => {})
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const schemas = openapiObject.components.schemas
t.assert.deepStrictEqual(Object.keys(schemas), ['schemaA', 'schemaB', 'schemaC', 'schemaD'])
// ref must be prefixed by '#/components/schemas/'
t.assert.strictEqual(schemas.schemaC.properties.a.items.$ref, '#/components/schemas/schemaA')
t.assert.strictEqual(schemas.schemaD.properties.b.$ref, '#/components/schemas/schemaB')
t.assert.strictEqual(schemas.schemaD.properties.c.$ref, '#/components/schemas/schemaC')
t.assert.strictEqual(schemas.schemaB.properties.id.example, 'ABC')
await Swagger.validate(openapiObject)
})
test('support $ref in response schema', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.register(function (instance, _, done) {
instance.addSchema({ $id: 'order', type: 'string', enum: ['foo'] })
instance.post('/', { schema: { response: { 200: { type: 'object', properties: { order: { $ref: 'order' } } } } } }, () => {})
done()
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
await Swagger.validate(openapiObject)
})
test('support $ref for enums in other schemas', async (t) => {
const fastify = Fastify()
const enumSchema = { $id: 'order', anyOf: [{ type: 'string', const: 'foo' }, { type: 'string', const: 'bar' }] }
const enumRef = { $ref: 'order' }
const objectWithEnumSchema = { $id: 'object', type: 'object', properties: { type: enumRef }, required: ['type'] }
await fastify.register(fastifySwagger, openapiOption)
await fastify.register(async (instance) => {
instance.addSchema(enumSchema)
instance.addSchema(objectWithEnumSchema)
instance.post('/', { schema: { body: { type: 'object', properties: { order: { $ref: 'order' } } } } }, async () => ({ result: 'OK' }))
})
await fastify.ready()
const responseBeforeSwagger = await fastify.inject({ method: 'POST', url: '/', payload: { order: 'foo' } })
t.assert.strictEqual(responseBeforeSwagger.statusCode, 200)
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
await Swagger.validate(openapiObject)
const responseAfterSwagger = await fastify.inject({ method: 'POST', url: '/', payload: { order: 'foo' } })
t.assert.strictEqual(responseAfterSwagger.statusCode, 200)
})
test('support nested $ref schema : complex case without modifying buildLocalReference', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, { openapi: {} })
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'schemaB', type: 'object', properties: { id: { type: 'string' } } })
instance.addSchema({ $id: 'schemaC', type: 'object', properties: { a: { type: 'array', items: { $ref: 'schemaA' } } } })
instance.addSchema({ $id: 'schemaD', type: 'object', properties: { b: { $ref: 'schemaB' }, c: { $ref: 'schemaC' } } })
instance.post('/url1', { schema: { body: { $ref: 'schemaD' }, response: { 200: { $ref: 'schemaB' } } } }, () => {})
instance.post('/url2', { schema: { body: { $ref: 'schemaC' }, response: { 200: { $ref: 'schemaA' } } } }, () => {})
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const schemas = openapiObject.components.schemas
t.assert.deepStrictEqual(Object.keys(schemas), ['def-0', 'def-1', 'def-2', 'def-3'])
// ref must be prefixed by '#/components/schemas/'
t.assert.strictEqual(schemas['def-2'].properties.a.items.$ref, '#/components/schemas/def-0')
t.assert.strictEqual(schemas['def-3'].properties.b.$ref, '#/components/schemas/def-1')
t.assert.strictEqual(schemas['def-3'].properties.c.$ref, '#/components/schemas/def-2')
await Swagger.validate(openapiObject)
})
test('support nested $ref with patternProperties', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, { openapi: {} })
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'schemaB', type: 'object', patternProperties: { '^[A-z]{1,10}$': { $ref: 'schemaA#' } } })
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const schemas = openapiObject.components.schemas
t.assert.deepStrictEqual(Object.keys(schemas), ['def-0', 'def-1'])
// ref must be prefixed by '#/components/schemas/'
t.assert.strictEqual(schemas['def-1'].additionalProperties.$ref, '#/components/schemas/def-0')
await Swagger.validate(openapiObject)
})
test('support $ref schema in allOf in querystring', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, { openapi: {} })
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { field1: { type: 'integer' } } })
instance.get('/url1', { schema: { query: { type: 'object', allOf: [{ $ref: 'schemaA#' }, { type: 'object', properties: { field3: { type: 'boolean' } } }] }, response: { 200: { type: 'object' } } } }, async () => ({ result: 'OK' }))
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const schemas = openapiObject.components.schemas
t.assert.deepStrictEqual(Object.keys(schemas), ['def-0'])
await Swagger.validate(openapiObject)
const responseAfterSwagger = await fastify.inject({ method: 'GET', url: '/url1', query: { field1: 10, field3: false } })
t.assert.strictEqual(responseAfterSwagger.statusCode, 200)
})
test('support $ref schema in allOf in headers', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, { openapi: {} })
fastify.register(async (instance) => {
instance.addSchema({ $id: 'headerA', type: 'object', properties: { 'x-header-1': { type: 'string' } } })
instance.get('/url1', { schema: { headers: { allOf: [{ $ref: 'headerA#' }, { type: 'object', properties: { 'x-header-2': { type: 'string' } } }] }, response: { 200: { type: 'object' } } } }, async () => ({ result: 'OK' }))
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const schemas = openapiObject.components.schemas
t.assert.deepStrictEqual(Object.keys(schemas), ['def-0'])
await Swagger.validate(openapiObject)
const responseAfterSwagger = await fastify.inject({ method: 'GET', url: '/url1', headers: { 'x-header-1': 'test', 'x-header-2': 'test' } })
t.assert.strictEqual(responseAfterSwagger.statusCode, 200)
})
test('uses examples if has property required in body', async (t) => {
t.plan(3)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.get('/', {
schema: {
query: {
type: 'object',
oneOf: [
{
properties: {
bar: { type: 'number' }
}
},
{
properties: {
foo: { type: 'string' }
}
}
]
},
response: {
200: {
type: 'object',
properties: {
result: { type: 'string' }
}
}
}
}
}, () => ({ result: 'OK' }))
await fastify.ready()
const openapiObject = fastify.swagger()
const schema = openapiObject.paths['/'].get
t.assert.ok(schema)
t.assert.ok(schema.parameters)
t.assert.deepStrictEqual(schema.parameters[0].in, 'query')
})
test('renders $ref schema with enum in headers', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, { openapi: {} })
fastify.register(async (instance) => {
instance.addSchema({ $id: 'headerA', type: 'object', properties: { 'x-enum-header': { type: 'string', enum: ['OK', 'NOT_OK'] } } })
instance.get('/url1', { schema: { headers: { $ref: 'headerA#' }, response: { 200: { type: 'object' } } } }, async () => ({ result: 'OK' }))
})
await fastify.ready()
const openapiObject = fastify.swagger()
await Swagger.validate(openapiObject)
// the OpenAPI spec should show the enum
t.assert.deepStrictEqual(openapiObject.paths['/url1'].get.parameters[0].schema, { type: 'string', enum: ['OK', 'NOT_OK'] })
})
test('renders $ref schema with additional keywords', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, { openapi: {} })
await fastify.register(require('@fastify/cookie'))
const cookie = {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'string' },
c: { type: 'string' }
},
minProperties: 2
}
fastify.register(async (instance) => {
instance.addSchema({
$id: 'headerA',
type: 'object',
properties: {
cookie
}
})
instance.get('/url1', {
preValidation: async (request) => {
request.headers.cookie = request.cookies
},
schema: {
headers: {
$ref: 'headerA#'
}
}
}, async (req) => (req.headers))
})
await fastify.ready()
const openapiObject = fastify.swagger()
await Swagger.validate(openapiObject)
t.assert.deepStrictEqual(openapiObject.paths['/url1'].get.parameters[0].schema, cookie)
let res = await fastify.inject({ method: 'GET', url: 'url1', cookies: { a: 'hi', b: 'asd' } })
t.assert.deepStrictEqual(res.statusCode, 200)
res = await fastify.inject({ method: 'GET', url: 'url1', cookies: { a: 'hi' } })
t.assert.deepStrictEqual(res.statusCode, 400)
t.assert.deepStrictEqual(openapiObject.paths['/url1'].get.parameters[0].schema, cookie)
})
test('support $ref in callbacks', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'Subscription', type: 'object', properties: { callbackUrl: { type: 'string', examples: ['https://example.com'] } } })
instance.addSchema({ $id: 'Event', type: 'object', properties: { message: { type: 'string', examples: ['Some event happened'] } } })
instance.post('/subscribe', {
schema: {
body: {
$ref: 'Subscription#'
},
response: {
200: {
$ref: 'Subscription#'
}
},
callbacks: {
myEvent: {
'{$request.body#/callbackUrl}': {
post: {
requestBody: {
content: {
'application/json': {
schema: { $ref: 'Event#' }
}
}
},
responses: {
200: {
description: 'Success'
}
}
}
}
}
}
}
}, () => {})
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
t.assert.deepStrictEqual(Object.keys(openapiObject.components.schemas), ['Subscription', 'Event'])
t.assert.strictEqual(openapiObject.components.schemas.Subscription.properties.callbackUrl.example, 'https://example.com')
t.assert.strictEqual(openapiObject.components.schemas.Event.properties.message.example, 'Some event happened')
await Swagger.validate(openapiObject)
})
test('should return only ref if defs and ref is defined', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, { openapi: { openapi: '3.1.0' } })
fastify.addSchema({
$id: 'sharedSchema',
humanModule: {
$defs: {
AddressSchema: {
type: 'object',
properties: {
street: {
type: 'string',
},
streetNumber: {
type: 'number',
},
},
required: [
'street',
'streetNumber',
],
$id: 'AddressSchema',
},
PersonSchema: {
type: 'object',
properties: {
name: {
type: 'string',
},
homeAddress: {
$ref: 'AddressSchema',
},
workAddress: {
$ref: 'AddressSchema',
},
},
required: [
'name',
'homeAddress',
'workAddress',
],
$id: 'PersonSchema',
},
PostRequestSchema: {
type: 'object',
properties: {
person: {
$ref: 'PersonSchema',
},
},
required: [
'person',
],
$id: 'PostRequestSchema',
},
},
},
})
fastify.get('/person', {
schema: {
response: {
200:
{
$defs: {
AddressSchema: {
type: 'object',
properties: {
street: {
type: 'string',
},
streetNumber: {
type: 'number',
},
},
required: [
'street',
'streetNumber',
],
$id: 'AddressSchema',
},
PersonSchema: {
type: 'object',
properties: {
name: {
type: 'string',
},
homeAddress: {
$ref: 'AddressSchema',
},
workAddress: {
$ref: 'AddressSchema',
},
},
required: [
'name',
'homeAddress',
'workAddress',
],
$id: 'PersonSchema',
},
PostRequestSchema: {
type: 'object',
properties: {
person: {
$ref: 'PersonSchema',
},
},
required: [
'person',
],
$id: 'PostRequestSchema',
},
},
$ref: 'PersonSchema',
}
}
},
}, async () => ({ result: 'OK' }))
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const expectedPathContent = { 'application/json': { schema: { $ref: '#/components/schemas/def-2' } } }
t.assert.deepStrictEqual(openapiObject.paths['/person'].get.responses[200].content, expectedPathContent)
await Swagger.validate(openapiObject)
})

View File

@@ -0,0 +1,899 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const Swagger = require('@apidevtools/swagger-parser')
const yaml = require('yaml')
const fastifySwagger = require('../../../index')
const {
openapiOption,
openapiRelativeOptions,
schemaBody,
schemaConsumes,
schemaCookies,
schemaExtension,
schemaHeaders,
schemaHeadersParams,
schemaParams,
schemaProduces,
schemaQuerystring,
schemaSecurity,
schemaOperationId
} = require('../../../examples/options')
test('openapi should return a valid swagger object', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.get('/', () => {})
fastify.post('/', () => {})
fastify.get('/example', schemaQuerystring, () => {})
fastify.post('/example', schemaBody, () => {})
fastify.get('/parameters/:id', schemaParams, () => {})
fastify.get('/headers', schemaHeaders, () => {})
fastify.get('/headers/:id', schemaHeadersParams, () => {})
fastify.get('/security', schemaSecurity, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
await Swagger.validate(openapiObject)
t.assert.ok(true, 'valid swagger object')
})
test('openapi should return a valid swagger yaml', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.get('/', () => {})
fastify.post('/', () => {})
fastify.get('/example', schemaQuerystring, () => {})
fastify.post('/example', schemaBody, () => {})
fastify.get('/parameters/:id', schemaParams, () => {})
fastify.get('/headers', schemaHeaders, () => {})
fastify.get('/headers/:id', schemaHeadersParams, () => {})
fastify.get('/security', schemaSecurity, () => {})
await fastify.ready()
const swaggerYaml = fastify.swagger({ yaml: true })
t.assert.strictEqual(typeof swaggerYaml, 'string')
yaml.parse(swaggerYaml)
t.assert.ok(true, 'valid swagger yaml')
})
test('route options - deprecated', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
const opts = {
schema: {
deprecated: true,
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
}
}
}
fastify.post('/', opts, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
await Swagger.validate(openapiObject)
t.assert.ok(true, 'valid swagger object')
t.assert.ok(openapiObject.paths['/'])
})
test('route options - meta', async (t) => {
t.plan(7)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
const opts = {
schema: {
operationId: 'doSomething',
summary: 'Route summary',
tags: ['tag'],
description: 'Route description',
servers: [
{
url: 'https://localhost'
}
],
externalDocs: {
description: 'Find more info here',
url: 'https://swagger.io'
}
}
}
fastify.get('/', opts, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
const api = await Swagger.validate(openapiObject)
const definedPath = api.paths['/'].get
t.assert.ok(definedPath)
t.assert.strictEqual(opts.schema.operationId, definedPath.operationId)
t.assert.strictEqual(opts.schema.summary, definedPath.summary)
t.assert.deepEqual(opts.schema.tags, definedPath.tags)
t.assert.strictEqual(opts.schema.description, definedPath.description)
t.assert.strictEqual(opts.schema.servers, definedPath.servers)
t.assert.strictEqual(opts.schema.externalDocs, definedPath.externalDocs)
})
test('route options - produces', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.get('/', schemaProduces, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
const api = await Swagger.validate(openapiObject)
const definedPath = api.paths['/'].get
t.assert.ok(definedPath)
t.assert.deepEqual(definedPath.responses[200].content, {
'*/*': {
schema: {
type: 'object',
properties: {
hello: {
description: 'hello',
type: 'string'
}
},
required: ['hello']
}
}
})
})
test('route options - cookies', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.get('/', schemaCookies, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
const api = await Swagger.validate(openapiObject)
const definedPath = api.paths['/'].get
t.assert.ok(definedPath)
t.assert.deepEqual(definedPath.parameters, [
{
required: false,
in: 'cookie',
name: 'bar',
schema: {
type: 'string'
}
}
])
})
test('route options - extension', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(fastifySwagger, { openapi: { 'x-ternal': true } })
fastify.get('/', schemaExtension, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
const api = await Swagger.validate(openapiObject)
t.assert.ok(api['x-ternal'])
t.assert.deepEqual(api['x-ternal'], true)
const definedPath = api.paths['/'].get
t.assert.ok(definedPath)
t.assert.deepEqual(definedPath['x-tension'], true)
})
test('parses form parameters when all api consumes application/x-www-form-urlencoded', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.post('/', schemaConsumes, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
const api = await Swagger.validate(openapiObject)
const definedPath = api.paths['/'].post
t.assert.ok(definedPath)
t.assert.deepEqual(definedPath.requestBody.content, {
'application/x-www-form-urlencoded': {
schema: {
type: 'object',
properties: {
hello: {
description: 'hello',
type: 'string'
}
},
required: ['hello']
}
}
})
})
test('route options - method', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.route({
method: ['GET', 'POST'],
url: '/',
handler: function (request, reply) {
reply.send({ hello: 'world' })
}
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
await Swagger.validate(openapiObject)
t.assert.ok(true, 'valid swagger object')
})
test('cookie, query, path description', async (t) => {
t.plan(6)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
const schemaCookies = {
schema: {
cookies: {
type: 'object',
properties: {
bar: { type: 'string', description: 'Bar' }
}
}
}
}
const schemaQuerystring = {
schema: {
querystring: {
type: 'object',
properties: {
hello: { type: 'string', description: 'Hello' }
}
}
}
}
// test without description as other test case for params already have description
const schemaParams = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
}
}
}
fastify.get('/', schemaCookies, () => {})
fastify.get('/example', schemaQuerystring, () => {})
fastify.get('/parameters/:id', schemaParams, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
const api = await Swagger.validate(openapiObject)
const cookiesPath = api.paths['/'].get
t.assert.ok(cookiesPath)
t.assert.deepEqual(cookiesPath.parameters, [
{
required: false,
in: 'cookie',
name: 'bar',
description: 'Bar',
schema: {
type: 'string'
}
}
])
const querystringPath = api.paths['/example'].get
t.assert.ok(querystringPath)
t.assert.deepEqual(querystringPath.parameters, [
{
required: false,
in: 'query',
name: 'hello',
description: 'Hello',
schema: {
type: 'string'
}
}
])
const paramPath = api.paths['/parameters/{id}'].get
t.assert.ok(paramPath)
t.assert.deepEqual(paramPath.parameters, [
{
required: true,
in: 'path',
name: 'id',
schema: {
type: 'string'
}
}
])
})
test('cookie and query with serialization type', async (t) => {
t.plan(4)
const fastify = Fastify({
ajv: {
plugins: [
function (ajv) {
ajv.addKeyword({
keyword: 'x-consume'
})
}
]
}
})
await fastify.register(fastifySwagger, openapiOption)
const schemaCookies = {
schema: {
cookies: {
type: 'object',
properties: {
bar: {
type: 'object',
'x-consume': 'application/json',
required: ['foo'],
properties: {
foo: { type: 'string' },
bar: { type: 'string' }
}
}
}
}
}
}
const schemaQuerystring = {
schema: {
querystring: {
type: 'object',
properties: {
hello: {
type: 'object',
'x-consume': 'application/json',
required: ['bar'],
properties: {
bar: { type: 'string' },
baz: { type: 'string' }
}
}
}
}
}
}
fastify.get('/', schemaCookies, () => {})
fastify.get('/example', schemaQuerystring, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
const api = await Swagger.validate(openapiObject)
const cookiesPath = api.paths['/'].get
t.assert.ok(cookiesPath)
t.assert.deepEqual(cookiesPath.parameters, [
{
[Symbol.for('@fastify/swagger.rawRequired')]: ['foo'],
required: false,
in: 'cookie',
name: 'bar',
content: {
'application/json': {
schema: {
type: 'object',
required: ['foo'],
properties: {
foo: { type: 'string' },
bar: { type: 'string' }
}
}
}
}
}
])
const querystringPath = api.paths['/example'].get
t.assert.ok(querystringPath)
t.assert.deepEqual(querystringPath.parameters, [
{
required: false,
in: 'query',
name: 'hello',
content: {
'application/json': {
schema: {
type: 'object',
required: ['bar'],
properties: {
bar: { type: 'string' },
baz: { type: 'string' }
}
}
}
}
}
])
})
test('openapi should pass through operationId', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.get('/hello', schemaOperationId, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
await Swagger.validate(openapiObject)
t.assert.ok(true, 'valid swagger object')
})
test('openapi should pass through Links', async (t) => {
t.plan(3)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.get('/user/:id', {
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'the user identifier, as userId'
}
},
required: ['id']
},
response: {
200: {
type: 'object',
properties: {
uuid: {
type: 'string',
format: 'uuid'
}
}
}
}
},
links: {
200: {
address: {
operationId: 'getUserAddress',
parameters: {
id: '$request.path.id'
}
}
}
}
}, () => {})
fastify.get('/user/:id/address', {
schema: {
operationId: 'getUserAddress',
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'the user identifier, as userId'
}
},
required: ['id']
},
response: {
200: {
type: 'string'
}
}
}
}, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const api = await Swagger.validate(openapiObject)
t.assert.ok(true, 'valid swagger object')
t.assert.deepEqual(api.paths['/user/{id}'].get.responses['200'].links, {
address: {
operationId: 'getUserAddress',
parameters: {
id: '$request.path.id'
}
}
})
})
test('links without status code', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.get('/user/:id', {
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'the user identifier, as userId'
}
},
required: ['id']
},
response: {
200: {
type: 'object',
properties: {
uuid: {
type: 'string',
format: 'uuid'
}
}
}
}
},
links: {
201: {
address: {
operationId: 'getUserAddress',
parameters: {
id: '$request.path.id'
}
}
}
}
}, () => {})
fastify.get('/user/:id/address', {
schema: {
operationId: 'getUserAddress',
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'the user identifier, as userId'
}
},
required: ['id']
},
response: {
200: {
type: 'string'
}
}
}
}, () => {})
await fastify.ready()
t.assert.throws(() => fastify.swagger(), new Error('missing status code 201 in route /user/:id'))
})
test('security headers ignored when declared in security and securityScheme', async (t) => {
t.plan(6)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiOption)
fastify.get('/address1/:id', {
schema: {
headers: {
type: 'object',
properties: {
apiKey: {
type: 'string',
description: 'api token'
},
bearerAuth: {
type: 'string',
description: 'authorization bearer'
},
id: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
fastify.get('/address2/:id', {
schema: {
headers: {
type: 'object',
properties: {
authKey: {
type: 'string',
description: 'auth token'
},
id: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const api = await Swagger.validate(openapiObject)
t.assert.ok(true, 'valid swagger object')
t.assert.ok(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'id')))
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'id')))
t.assert.strictEqual(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'apiKey')), undefined)
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'authKey')))
})
test('security querystrings ignored when declared in security and securityScheme', async (t) => {
t.plan(6)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: {
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'query'
}
}
},
security: [{
apiKey: []
}]
}
})
fastify.get('/address1/:id', {
schema: {
querystring: {
type: 'object',
properties: {
apiKey: {
type: 'string',
description: 'api token'
},
id: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
fastify.get('/address2/:id', {
schema: {
querystring: {
type: 'object',
properties: {
authKey: {
type: 'string',
description: 'auth token'
},
id: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const api = await Swagger.validate(openapiObject)
t.assert.ok(true, 'valid swagger object')
t.assert.ok(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'id')))
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'id')))
t.assert.strictEqual(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'apiKey')), undefined)
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'authKey')))
})
test('security cookies ignored when declared in security and securityScheme', async (t) => {
t.plan(6)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: {
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'cookie'
}
}
},
security: [{
apiKey: []
}]
}
})
fastify.get('/address1/:id', {
schema: {
cookies: {
type: 'object',
properties: {
apiKey: {
type: 'string',
description: 'api token'
},
id: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
fastify.get('/address2/:id', {
schema: {
cookies: {
type: 'object',
properties: {
authKey: {
type: 'string',
description: 'auth token'
},
id: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(typeof openapiObject, 'object')
const api = await Swagger.validate(openapiObject)
t.assert.ok(true, 'valid swagger object')
t.assert.ok(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'id')))
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'id')))
t.assert.strictEqual(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'apiKey')), undefined)
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'authKey')))
})
test('path params on relative url', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiRelativeOptions)
const schemaParams = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
}
}
}
fastify.get('/parameters/:id', schemaParams, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
const api = await Swagger.validate(openapiObject)
const paramPath = api.paths['/parameters/{id}'].get
t.assert.ok(paramPath)
t.assert.deepEqual(paramPath.parameters, [
{
required: true,
in: 'path',
name: 'id',
schema: {
type: 'string'
}
}
])
})
test('verify generated path param definition with route prefixing', async (t) => {
const opts = {
schema: {}
}
const fastify = Fastify()
await fastify.register(fastifySwagger, openapiRelativeOptions)
await fastify.register(function (app, _, done) {
app.get('/:userId', opts, () => {})
done()
}, { prefix: '/v1' })
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/v1/{userId}'].get
t.assert.deepEqual(definedPath.parameters, [{
schema: {
type: 'string'
},
in: 'path',
name: 'userId',
required: true
}])
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,470 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const Swagger = require('@apidevtools/swagger-parser')
const yaml = require('yaml')
const fastifySwagger = require('../../../index')
const { readPackageJson } = require('../../../lib/util/read-package-json')
const { swaggerOption } = require('../../../examples/options')
test('swagger should have default version', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger)
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.swagger, '2.0')
})
test('swagger should have default info properties', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger)
await fastify.ready()
const swaggerObject = fastify.swagger()
const pkg = readPackageJson()
t.assert.strictEqual(swaggerObject.info.title, pkg.name)
t.assert.strictEqual(swaggerObject.info.version, pkg.version)
})
test('swagger basic properties', async (t) => {
t.plan(5)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
const opts = {
schema: {
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
}
}
}
fastify.post('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.info, swaggerOption.swagger.info)
t.assert.strictEqual(swaggerObject.host, swaggerOption.swagger.host)
t.assert.strictEqual(swaggerObject.schemes, swaggerOption.swagger.schemes)
t.assert.ok(swaggerObject.paths)
t.assert.ok(swaggerObject.paths['/'])
})
test('swagger definitions', async (t) => {
t.plan(1)
const fastify = Fastify()
swaggerOption.swagger.definitions = {
ExampleModel: {
type: 'object',
properties: {
id: {
type: 'integer',
description: 'Some id'
},
name: {
type: 'string',
description: 'Name of smthng'
}
}
}
}
await fastify.register(fastifySwagger, swaggerOption)
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(swaggerObject.definitions)), swaggerOption.swagger.definitions)
delete swaggerOption.swagger.definitions // remove what we just added
})
test('swagger paths', async (t) => {
t.plan(1)
const fastify = Fastify()
swaggerOption.swagger.paths = {
'/status': {
get: {
description: 'Status route, so we can check if server is alive',
tags: [
'Status'
],
responses: {
200: {
description: 'Server is alive',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
health: {
type: 'boolean'
},
date: {
type: 'string'
}
},
example: {
health: true,
date: '2018-02-19T15:36:46.758Z'
}
}
}
}
}
}
}
}
}
await fastify.register(fastifySwagger, swaggerOption)
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.deepStrictEqual(swaggerObject.paths, swaggerOption.swagger.paths)
delete swaggerOption.swagger.paths // remove what we just added
})
test('swagger tags', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.tags, swaggerOption.swagger.tags)
})
test('swagger externalDocs', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.externalDocs, swaggerOption.swagger.externalDocs)
})
test('basePath support', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
swagger: Object.assign({}, swaggerOption.swagger, {
basePath: '/prefix'
})
})
fastify.get('/prefix/endpoint', {}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths['/prefix/endpoint'], undefined)
t.assert.ok(swaggerObject.paths['/endpoint'])
})
test('basePath support with prefix', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
prefix: '/prefix',
swagger: Object.assign({}, swaggerOption.swagger, {
basePath: '/prefix'
})
})
fastify.get('/endpoint', {}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths['/prefix/endpoint'], undefined)
t.assert.ok(swaggerObject.paths['/endpoint'])
})
test('basePath ensure leading slash', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
swagger: Object.assign({}, swaggerOption.swagger, {
basePath: '/'
})
})
fastify.get('/endpoint', {}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths.endpoint, undefined)
t.assert.ok(swaggerObject.paths['/endpoint'])
})
test('basePath with prefix ensure leading slash', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
prefix: '/',
swagger: Object.assign({}, swaggerOption.swagger, {
basePath: '/'
})
})
fastify.get('/endpoint', {}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths.endpoint, undefined)
t.assert.ok(swaggerObject.paths['/endpoint'])
})
test('basePath maintained when stripBasePath is set to false', async (t) => {
t.plan(3)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
stripBasePath: false,
swagger: Object.assign({}, swaggerOption.swagger, {
basePath: '/foo'
})
})
fastify.get('/foo/endpoint', {}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths.endpoint, undefined)
t.assert.strictEqual(swaggerObject.paths['/endpoint'], undefined)
t.assert.ok(swaggerObject.paths['/foo/endpoint'])
})
// hide testing
test('hide support - property', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
const opts = {
schema: {
hide: true,
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
}
}
}
fastify.post('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths['/'], undefined)
})
test('hide support when property set in transform() - property', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
...swaggerOption,
transform: ({ schema, url }) => {
return { schema: { ...schema, hide: true }, url }
}
})
const opts = {
schema: {
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
}
}
}
fastify.post('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths['/'], undefined)
})
test('hide support - tags Default', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
const opts = {
schema: {
tags: ['X-HIDDEN'],
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
}
}
}
fastify.post('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths['/'], undefined)
})
test('hide support - tags Custom', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, { ...swaggerOption, hiddenTag: 'NOP' })
const opts = {
schema: {
tags: ['NOP'],
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
}
}
}
fastify.post('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths['/'], undefined)
})
test('hide support - hidden untagged', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, { ...swaggerOption, hideUntagged: true })
const opts = {
schema: {
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
}
}
}
fastify.post('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.paths['/'], undefined)
})
test('cache - json', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
await fastify.ready()
fastify.swagger()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(typeof swaggerObject, 'object')
await Swagger.validate(swaggerObject)
t.assert.ok(true, 'valid swagger object')
})
test('cache - yaml', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
await fastify.ready()
fastify.swagger({ yaml: true })
const swaggerYaml = fastify.swagger({ yaml: true })
t.assert.strictEqual(typeof swaggerYaml, 'string')
yaml.parse(swaggerYaml)
t.assert.ok(true, 'valid swagger yaml')
})
module.exports = { swaggerOption }

View File

@@ -0,0 +1,164 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const Swagger = require('@apidevtools/swagger-parser')
const fastifySwagger = require('../../../index')
const { FST_ERR_SCH_ALREADY_PRESENT } = require('fastify/lib/errors')
test('support $ref schema', async t => {
t.plan(1)
const fastify = Fastify()
fastify.addSchema({
$id: 'example',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
await fastify.register(fastifySwagger)
fastify.register((instance, _opts, next) => {
instance.addSchema({
$id: 'subschema-two',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
instance.register((subinstance, _opts, next) => {
subinstance.addSchema({
$id: 'subschema-three',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
subinstance.post('/:hello', {
handler () {},
schema: {
body: { $ref: 'example#/properties/hello' },
querystring: { $ref: 'subschema-two#/properties/hello' },
params: { $ref: 'subschema-two#/properties/hello' },
headers: { $ref: 'subschema-three#/properties/hello' },
response: {
200: { $ref: 'example#/properties/hello' }
}
}
})
next()
})
next()
})
await fastify.ready()
await Swagger.validate(fastify.swagger())
t.assert.ok(true, 'valid swagger object')
})
test('support nested $ref schema : complex case', async (t) => {
const options = {
swagger: {},
refResolver: {
buildLocalReference: (json, _baseUri, _fragment, i) => {
return json.$id || `def-${i}`
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger, options)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'schemaB', type: 'object', properties: { id: { type: 'string' } } })
instance.addSchema({ $id: 'schemaC', type: 'object', properties: { a: { type: 'array', items: { $ref: 'schemaA' } } } })
instance.addSchema({ $id: 'schemaD', type: 'object', properties: { b: { $ref: 'schemaB' }, c: { $ref: 'schemaC' } } })
instance.post('/url1', { schema: { body: { $ref: 'schemaD' }, response: { 200: { $ref: 'schemaB' } } } }, () => {})
instance.post('/url2', { schema: { body: { $ref: 'schemaC' }, response: { 200: { $ref: 'schemaA' } } } }, () => {})
})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(typeof swaggerObject, 'object')
const definitions = swaggerObject.definitions
t.assert.deepStrictEqual(Object.keys(definitions), ['schemaA', 'schemaB', 'schemaC', 'schemaD'])
// ref must be prefixed by '#/definitions/'
t.assert.strictEqual(definitions.schemaC.properties.a.items.$ref, '#/definitions/schemaA')
t.assert.strictEqual(definitions.schemaD.properties.b.$ref, '#/definitions/schemaB')
t.assert.strictEqual(definitions.schemaD.properties.c.$ref, '#/definitions/schemaC')
await Swagger.validate(swaggerObject)
})
test('support nested $ref schema : complex case without modifying buildLocalReference', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'schemaB', type: 'object', properties: { id: { type: 'string' } } })
instance.addSchema({ $id: 'schemaC', type: 'object', properties: { a: { type: 'array', items: { $ref: 'schemaA' } } } })
instance.addSchema({ $id: 'schemaD', type: 'object', properties: { b: { $ref: 'schemaB' }, c: { $ref: 'schemaC' } } })
instance.post('/url1', { schema: { body: { $ref: 'schemaD' }, response: { 200: { $ref: 'schemaB' } } } }, () => {})
instance.post('/url2', { schema: { body: { $ref: 'schemaC' }, response: { 200: { $ref: 'schemaA' } } } }, () => {})
})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(typeof swaggerObject, 'object')
const definitions = swaggerObject.definitions
t.assert.deepStrictEqual(Object.keys(definitions), ['def-0', 'def-1', 'def-2', 'def-3'])
// ref must be prefixed by '#/definitions/'
t.assert.strictEqual(definitions['def-2'].properties.a.items.$ref, '#/definitions/def-0')
t.assert.strictEqual(definitions['def-3'].properties.b.$ref, '#/definitions/def-1')
t.assert.strictEqual(definitions['def-3'].properties.c.$ref, '#/definitions/def-2')
await Swagger.validate(swaggerObject)
})
test('trying to overwriting a schema results in a FST_ERR_SCH_ALREADY_PRESENT', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
t.assert.throws(() => instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } }), new FST_ERR_SCH_ALREADY_PRESENT('schemaA'))
})
await fastify.ready()
})
test('renders $ref schema with enum in headers', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'headerA', type: 'object', properties: { 'x-enum-header': { type: 'string', enum: ['OK', 'NOT_OK'] } } })
instance.get('/url1', { schema: { headers: { $ref: 'headerA#' }, response: { 200: { type: 'object' } } } }, async () => ({ result: 'OK' }))
})
await fastify.ready()
const swagger = fastify.swagger()
await Swagger.validate(swagger)
t.assert.deepStrictEqual(
swagger.paths['/url1'].get.parameters[0],
{
type: 'string',
enum: ['OK', 'NOT_OK'],
in: 'header',
name: 'x-enum-header',
required: false
}
)
})

View File

@@ -0,0 +1,585 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const Swagger = require('@apidevtools/swagger-parser')
const yaml = require('yaml')
const fastifySwagger = require('../../../index')
const {
swaggerOption,
schemaBody,
schemaConsumes,
schemaExtension,
schemaHeaders,
schemaHeadersParams,
schemaParams,
schemaQuerystring,
schemaSecurity
} = require('../../../examples/options')
test('swagger should return valid swagger object', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
fastify.get('/', () => {})
fastify.post('/', () => {})
fastify.get('/example', schemaQuerystring, () => {})
fastify.post('/example', schemaBody, () => {})
fastify.get('/parameters/:id', schemaParams, () => {})
fastify.get('/headers', schemaHeaders, () => {})
fastify.get('/headers/:id', schemaHeadersParams, () => {})
fastify.get('/security', schemaSecurity, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(typeof swaggerObject, 'object')
await Swagger.validate(swaggerObject)
t.assert.ok(true, 'valid swagger object')
})
test('swagger should return a valid swagger yaml', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
fastify.get('/', () => {})
fastify.route({
method: ['POST'],
url: '/',
handler: () => {}
})
fastify.get('/example', schemaQuerystring, () => {})
fastify.post('/example', schemaBody, () => {})
fastify.get('/parameters/:id', schemaParams, () => {})
fastify.get('/headers', schemaHeaders, () => {})
fastify.get('/headers/:id', schemaHeadersParams, () => {})
fastify.get('/security', schemaSecurity, () => {})
await fastify.ready()
const swaggerYaml = fastify.swagger({ yaml: true })
t.assert.strictEqual(typeof swaggerYaml, 'string')
yaml.parse(swaggerYaml)
t.assert.ok(true, 'valid swagger yaml')
})
test('route options - deprecated', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
const opts = {
schema: {
deprecated: true,
body: {
type: 'object',
properties: {
hello: { type: 'string' },
obj: {
type: 'object',
properties: {
some: { type: 'string' }
}
}
}
}
}
}
fastify.post('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
await Swagger.validate(swaggerObject)
t.assert.ok(true, 'valid swagger object')
t.assert.ok(swaggerObject.paths['/'])
})
test('route options - meta', async (t) => {
t.plan(8)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
const opts = {
schema: {
operationId: 'doSomething',
summary: 'Route summary',
tags: ['tag'],
description: 'Route description',
produces: ['application/octet-stream'],
consumes: ['application/x-www-form-urlencoded'],
externalDocs: {
description: 'Find more info here',
url: 'https://swagger.io'
}
}
}
fastify.get('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.ok(definedPath)
t.assert.strictEqual(opts.schema.operationId, definedPath.operationId)
t.assert.strictEqual(opts.schema.summary, definedPath.summary)
t.assert.deepStrictEqual(opts.schema.tags, definedPath.tags)
t.assert.strictEqual(opts.schema.description, definedPath.description)
t.assert.deepStrictEqual(opts.schema.produces, definedPath.produces)
t.assert.deepStrictEqual(opts.schema.consumes, definedPath.consumes)
t.assert.strictEqual(opts.schema.externalDocs, definedPath.externalDocs)
})
test('route options - consumes', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
fastify.post('/', schemaConsumes, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].post
t.assert.ok(definedPath)
t.assert.deepStrictEqual(definedPath.parameters, [{
in: 'formData',
name: 'hello',
description: 'hello',
required: true,
type: 'string'
}])
})
test('route options - extension', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(fastifySwagger, { swagger: { 'x-ternal': true } })
fastify.get('/', schemaExtension, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
t.assert.ok(api['x-ternal'])
t.assert.deepStrictEqual(api['x-ternal'], true)
const definedPath = api.paths['/'].get
t.assert.ok(definedPath)
t.assert.deepStrictEqual(definedPath['x-tension'], true)
})
test('route options - querystring', async (t) => {
t.plan(2)
const opts = {
schema: {
querystring: {
type: 'object',
properties: {
hello: { type: 'string' },
world: { type: 'string', description: 'world description' }
},
required: ['hello']
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
fastify.get('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.ok(definedPath)
t.assert.deepStrictEqual(definedPath.parameters, [
{
in: 'query',
name: 'hello',
type: 'string',
required: true
},
{
in: 'query',
name: 'world',
type: 'string',
required: false,
description: 'world description'
}
])
})
test('swagger json output should not omit enum part in params config', async (t) => {
t.plan(2)
const opts = {
schema: {
params: {
type: 'object',
properties: {
enumKey: { type: 'string', enum: ['enum1', 'enum2'] }
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
fastify.get('/test/:enumKey', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/test/{enumKey}'].get
t.assert.ok(definedPath)
t.assert.deepStrictEqual(definedPath.parameters, [{
in: 'path',
name: 'enumKey',
type: 'string',
enum: ['enum1', 'enum2'],
required: true
}])
})
test('custom verbs should not be interpreted as path params', async (t) => {
t.plan(2)
const opts = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
fastify.get('/resource/:id/sub-resource::watch', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/resource/{id}/sub-resource:watch'].get
t.assert.ok(definedPath)
t.assert.deepStrictEqual(definedPath.parameters, [{
in: 'path',
name: 'id',
type: 'string',
required: true
}])
})
test('swagger json output should not omit consume in querystring schema', async (t) => {
t.plan(1)
const fastify = Fastify({
ajv: {
plugins: [
function (ajv) {
ajv.addKeyword({ keyword: 'x-consume' })
}
]
}
})
await fastify.register(fastifySwagger, swaggerOption)
const schemaQuerystring = {
schema: {
querystring: {
type: 'object',
properties: {
hello: {
type: 'object',
'x-consume': 'application/json',
required: ['bar'],
properties: {
bar: { type: 'string' },
baz: { type: 'string' }
}
}
}
}
}
}
fastify.get('/', schemaQuerystring, () => {})
await fastify.ready()
try {
fastify.swagger()
t.assert.fail('error was not thrown')
} catch (err) {
if (err.message.startsWith('Complex serialization is not supported by Swagger')) {
t.assert.ok(true, 'error was thrown')
} else {
t.error(err)
}
}
})
test('swagger should not support Links', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
fastify.get('/user/:id', {
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'the user identifier, as userId'
}
},
required: ['id']
},
response: {
200: {
type: 'object',
properties: {
uuid: {
type: 'string',
format: 'uuid'
}
}
}
}
},
links: {
200: {
address: {
operationId: 'getUserAddress',
parameters: {
id: '$request.path.id'
}
}
}
}
}, () => {})
fastify.get('/user/:id/address', {
schema: {
operationId: 'getUserAddress',
params: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'the user identifier, as userId'
}
},
required: ['id']
},
response: {
200: {
type: 'string'
}
}
}
}, () => {})
await fastify.ready()
t.assert.throws(() => fastify.swagger(), new Error('Swagger (Open API v2) does not support Links. Upgrade to OpenAPI v3 (see @fastify/swagger readme)'))
})
test('security headers ignored when declared in security and securityScheme', async (t) => {
t.plan(6)
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
fastify.get('/address1/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
headers: {
type: 'object',
properties: {
apiKey: {
type: 'string',
description: 'api token'
},
somethingElse: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
fastify.get('/address2/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
headers: {
type: 'object',
properties: {
authKey: {
type: 'string',
description: 'auth token'
},
somethingElse: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(typeof swaggerObject, 'object')
const api = await Swagger.validate(swaggerObject)
t.assert.ok(true, 'valid swagger object')
t.assert.ok(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'id')))
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'id')))
t.assert.strictEqual(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'apiKey')), undefined)
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'authKey')))
})
test('security querystrings ignored when declared in security and securityScheme', async (t) => {
t.plan(6)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
swagger: {
securityDefinitions: {
apiKey: {
type: 'apiKey',
name: 'apiKey',
in: 'query'
}
},
security: [{
apiKey: []
}]
}
})
fastify.get('/address1/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
querystring: {
type: 'object',
properties: {
apiKey: {
type: 'string',
description: 'api token'
},
somethingElse: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
fastify.get('/address2/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
querystring: {
type: 'object',
properties: {
authKey: {
type: 'string',
description: 'auth token'
},
somethingElse: {
type: 'string',
description: 'common field'
}
}
}
}
}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(typeof swaggerObject, 'object')
const api = await Swagger.validate(swaggerObject)
t.assert.ok(true, 'valid swagger object')
t.assert.ok(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'somethingElse')))
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'somethingElse')))
t.assert.strictEqual(api.paths['/address1/{id}'].get.parameters.find(({ name }) => (name === 'apiKey')), undefined)
t.assert.ok(api.paths['/address2/{id}'].get.parameters.find(({ name }) => (name === 'authKey')))
})
test('verify generated path param definition with route prefixing', async (t) => {
const opts = {
schema: {}
}
const fastify = Fastify()
await fastify.register(fastifySwagger, swaggerOption)
await fastify.register(function (app, _, done) {
app.get('/:userId', opts, () => {})
done()
}, { prefix: '/v1' })
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/v1/{userId}'].get
t.assert.deepStrictEqual(definedPath.parameters, [{
in: 'path',
name: 'userId',
type: 'string',
required: true
}])
})

View File

@@ -0,0 +1,790 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const Swagger = require('@apidevtools/swagger-parser')
const fastifySwagger = require('../../../index')
const S = require('fluent-json-schema')
test('support file in json schema', async t => {
const opts7 = {
schema: {
consumes: ['application/x-www-form-urlencoded'],
body: {
type: 'object',
properties: {
hello: {
description: 'hello',
type: 'string',
contentEncoding: 'binary'
}
},
required: ['hello']
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.post('/', opts7, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].post
t.assert.ok(definedPath)
t.assert.deepStrictEqual(definedPath.parameters, [{
in: 'formData',
name: 'hello',
description: 'hello',
required: true,
type: 'file'
}])
})
test('support response description', async t => {
const opts8 = {
schema: {
response: {
200: {
description: 'Response OK!',
type: 'object'
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/', opts8, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(definedPath.responses['200'].description, 'Response OK!')
})
test('support response description fallback to its $ref', async t => {
const opts = {
schema: {
response: {
200: {
$ref: 'my-ref#'
}
}
}
}
const fastify = Fastify()
fastify.addSchema({
$id: 'my-ref',
description: 'Response OK!',
type: 'string'
})
await fastify.register(fastifySwagger)
fastify.get('/', opts, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(definedPath.responses['200'].description, 'Response OK!')
})
test('response default description', async t => {
const opts9 = {
schema: {
response: {
200: {
type: 'object'
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/', opts9, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(definedPath.responses['200'].description, 'Default Response')
})
test('response 2xx', async t => {
const opt = {
schema: {
response: {
'2xx': {
type: 'object'
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/', opt, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(definedPath.responses['200'].description, 'Default Response')
t.assert.strictEqual(definedPath.responses['2XX'], undefined)
})
test('response conflict 2xx and 200', async t => {
const opt = {
schema: {
response: {
'2xx': {
type: 'object',
description: '2xx'
},
200: {
type: 'object',
description: '200'
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/', opt, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(definedPath.responses['200'].description, '200')
t.assert.strictEqual(definedPath.responses['2XX'], undefined)
})
test('support status code 204', async t => {
const opt = {
schema: {
response: {
204: {
type: 'null',
description: 'No Content'
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/', opt, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(definedPath.responses['204'].description, 'No Content')
t.assert.strictEqual(definedPath.responses['204'].schema, undefined)
})
test('support empty response body for different status than 204', async t => {
const opt = {
schema: {
response: {
204: {
type: 'null',
description: 'No Content'
},
503: {
type: 'null',
description: 'Service Unavailable'
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/', opt, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(definedPath.responses['204'].description, 'No Content')
t.assert.strictEqual(definedPath.responses['204'].content, undefined)
t.assert.strictEqual(definedPath.responses['503'].type, undefined)
t.assert.deepStrictEqual(definedPath.responses['503'].description, 'Service Unavailable')
t.assert.strictEqual(definedPath.responses['503'].content, undefined)
t.assert.strictEqual(definedPath.responses['503'].type, undefined)
})
test('support response headers', async t => {
const opt = {
schema: {
response: {
200: {
type: 'object',
properties: {
hello: {
type: 'string'
}
},
headers: {
'X-WORLD': {
type: 'string'
}
}
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/', opt, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(definedPath.responses['200'].headers, opt.schema.response['200'].headers)
t.assert.strictEqual(definedPath.responses['200'].schema.headers, undefined)
})
test('response: description and x-response-description', async () => {
const description = 'description - always that of response body, sometimes also that of response as a whole'
const responseDescription = 'description only for the response as a whole'
await test('description without x-response-description doubles as response description', async t => {
// Given a /description endpoint with only a |description| field in its response schema
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/description', {
schema: {
response: {
200: {
description,
type: 'string'
}
}
}
}, () => {})
await fastify.ready()
// When the Swagger schema is generated
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
// Then the /description endpoint uses the |description| as both the description of the Response Object as well as of its Schema Object
const responseObject = api.paths['/description'].get.responses['200']
t.assert.ok(responseObject)
t.assert.strictEqual(responseObject.description, description)
t.assert.strictEqual(responseObject.schema.description, description)
})
await test('description alongside x-response-description only describes response body', async t => {
// Given a /responseDescription endpoint that also has a |'x-response-description'| field in its response schema
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/responseDescription', {
schema: {
response: {
200: {
'x-response-description': responseDescription,
description,
type: 'string'
}
}
}
}, () => {})
await fastify.ready()
// When the Swagger schema is generated
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
// Then the /responseDescription endpoint uses the |responseDescription| only for the Response Object and the |description| only for the Schema Object
const responseObject = api.paths['/responseDescription'].get.responses['200']
t.assert.ok(responseObject)
t.assert.strictEqual(responseObject.description, responseDescription)
t.assert.strictEqual(responseObject.schema.description, description)
t.assert.strictEqual(responseObject.schema.responseDescription, undefined)
})
})
test('support "default" parameter', async t => {
const opt = {
schema: {
response: {
200: {
description: 'Expected Response',
type: 'object',
properties: {
foo: {
type: 'string'
}
}
},
default: {
description: 'Default Response',
type: 'object',
properties: {
bar: {
type: 'string'
}
}
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/', opt, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(definedPath.responses.default)), {
description: 'Default Response',
schema: {
description: 'Default Response',
type: 'object',
properties: {
bar: {
type: 'string'
}
}
}
})
})
test('fluent-json-schema', async t => {
const opt = {
schema: {
response: {
200: S.object()
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger, { swagger: true })
fastify.get('/', opt, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].get
t.assert.deepStrictEqual(definedPath.responses['200'].description, 'Default Response')
})
test('support "patternProperties" in json schema', async t => {
const opt = {
schema: {
body: {
type: 'object',
patternProperties: {
'^[a-z]{2,3}-[a-zA-Z]{2}$': {
type: 'string'
}
}
},
response: {
200: {
description: 'Expected Response',
type: 'object',
properties: {
foo: {
type: 'object',
patternProperties: {
'^[a-z]{2,3}-[a-zA-Z]{2}$': {
type: 'string'
}
},
additionalProperties: false
}
}
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger, { swagger: true })
fastify.post('/', opt, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].post
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(definedPath.parameters[0].schema)), {
type: 'object',
additionalProperties: { type: 'string' }
})
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(definedPath.responses[200])), {
description: 'Expected Response',
schema: {
description: 'Expected Response',
type: 'object',
properties: {
foo: {
type: 'object',
additionalProperties: { type: 'string' }
}
}
}
})
})
test('support "const" keyword', async t => {
const opt = {
schema: {
body: {
type: 'object',
properties: {
obj: {
type: 'object',
properties: {
constantProp: { const: 'my-const' },
constantPropNull: { const: null },
constantPropZero: { const: 0 },
constantPropFalse: { const: false },
constantPropEmptyString: { const: '' }
}
}
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.post('/', opt, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].post
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(definedPath.parameters[0].schema)), {
type: 'object',
properties: {
obj: {
type: 'object',
properties: {
constantProp: {
enum: ['my-const']
},
constantPropZero: {
enum: [0]
},
constantPropNull: {
enum: [null]
},
constantPropFalse: {
enum: [false]
},
constantPropEmptyString: {
enum: ['']
}
}
}
}
})
})
test('support "description" keyword', async t => {
const opt = {
schema: {
body: {
type: 'object',
description: 'Body description',
properties: {
foo: {
type: 'number'
}
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.post('/', opt, () => { })
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/'].post
t.assert.deepStrictEqual(definedPath.parameters[0].description, 'Body description')
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(definedPath.parameters[0].schema)), {
type: 'object',
description: 'Body description',
properties: {
foo: {
type: 'number'
}
}
})
})
test('no head routes by default', async (t) => {
const fastify = Fastify({ exposeHeadRoutes: true })
await fastify.register(fastifySwagger, {
routePrefix: '/docs',
exposeRoute: true
})
fastify.get('/with-head', {
schema: {
operationId: 'with-head',
response: {
200: {
description: 'Expected Response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
}
}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
t.assert.deepStrictEqual(
api.paths['/with-head'].get.responses['200'].description,
'Expected Response'
)
t.assert.deepStrictEqual(
api.paths['/with-head'].head,
undefined
)
})
test('support "exposeHeadRoutes" option', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, {
routePrefix: '/docs',
exposeHeadRoutes: true,
exposeRoute: true
})
fastify.get('/with-head', {
schema: {
operationId: 'with-head',
response: {
200: {
description: 'Expected Response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
}
}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
t.assert.deepStrictEqual(
api.paths['/with-head'].get.responses['200'].description,
'Expected Response'
)
t.assert.deepStrictEqual(
api.paths['/with-head'].head.responses['200'].description,
'Expected Response'
)
})
test('support "exposeHeadRoutes" option at route level', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, {
routePrefix: '/docs',
exposeRoute: true
})
fastify.get('/with-head', {
schema: {
operationId: 'with-head',
response: {
200: {
description: 'Expected Response',
type: 'object',
properties: {
foo: { type: 'string' }
}
}
}
},
config: {
swagger: {
exposeHeadRoute: true
}
}
}, () => {})
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
t.assert.deepStrictEqual(
api.paths['/with-head'].get.responses['200'].description,
'Expected Response'
)
t.assert.deepStrictEqual(
api.paths['/with-head'].head.responses['200'].description,
'Expected Response'
)
})
test('add default properties for url params when missing schema', async t => {
const opt = {}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.get('/:userId', opt, () => { })
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/{userId}'].get
t.assert.deepStrictEqual(definedPath.parameters[0], {
type: 'string',
required: true,
in: 'path',
name: 'userId'
})
})
test('add default properties for url params when missing schema.params', async t => {
const opt = {
schema: {
body: {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.post('/:userId', opt, () => { })
await fastify.ready()
const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)
const definedPath = api.paths['/{userId}'].post
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(definedPath.parameters[0].schema)), {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
})
t.assert.deepStrictEqual(definedPath.parameters[1], {
in: 'path',
name: 'userId',
type: 'string',
required: true
})
})
test('avoid overwriting params when schema.params is provided', async t => {
const opt = {
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string'
}
}
},
body: {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
}
}
}
const fastify = Fastify()
await fastify.register(fastifySwagger)
fastify.post('/:userId', opt, () => { })
await fastify.ready()
const swaggerObject = fastify.swagger()
const definedPath = swaggerObject.paths['/{userId}'].post
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(definedPath.parameters[0].schema)), {
type: 'object',
properties: {
bio: {
type: 'string'
}
}
})
t.assert.deepStrictEqual(definedPath.parameters[1], {
in: 'path',
name: 'id',
type: 'string',
required: true
})
})

View File

@@ -0,0 +1,414 @@
'use strict'
const { test } = require('node:test')
const Fastify = require('fastify')
const fastifySwagger = require('..')
const Joi = require('joi')
const Convert = require('joi-to-json')
const params = Joi
.object()
.keys({
property: Joi.string().required()
})
const opts = {
schema: { params }
}
const convertible = ['params', 'body', 'querystring']
const validTransform = ({ schema, url }) => {
const newSchema = Object.keys(schema).reduce((transformed, key) => {
transformed[key] = convertible.includes(key)
? Convert(schema[key])
: schema[key]
return transformed
},
{})
return { schema: newSchema, url }
}
const valid = {
transform: validTransform
}
const invalid = {
transform: 'wrong type'
}
test('transform should fail with a value other than Function', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, invalid)
fastify.setValidatorCompiler(({ schema }) => params.validate(schema))
fastify.get('/example', opts, () => {})
await fastify.ready()
t.assert.throws(fastify.swagger)
})
test('transform should work with a Function', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, valid)
fastify.setValidatorCompiler(({ schema }) => params.validate(schema))
fastify.get('/example', opts, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('transform can access route', async (t) => {
t.plan(5)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: { info: { version: '1.0.0' } },
transform: ({ route }) => {
t.assert.ok(route)
t.assert.strictEqual(route.method, 'GET')
t.assert.strictEqual(route.url, '/example')
t.assert.strictEqual(route.constraints.version, '1.0.0')
return { schema: route.schema, url: route.url }
}
})
fastify.get('/example', { constraints: { version: '1.0.0' } }, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('transform can access openapi object', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: { info: { version: '1.0.0' } },
transform: ({ route, openapiObject }) => {
t.assert.ok(openapiObject)
t.assert.strictEqual(openapiObject.openapi, '3.0.3')
t.assert.strictEqual(openapiObject.info.version, '1.0.0')
return {
schema: route.schema,
url: route.url
}
}
})
fastify.get('/example', () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('transform can access swagger object', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
swagger: { info: { version: '1.0.0' } },
transform: ({ route, swaggerObject }) => {
t.assert.ok(swaggerObject)
t.assert.strictEqual(swaggerObject.swagger, '2.0')
t.assert.strictEqual(swaggerObject.info.version, '1.0.0')
return {
schema: route.schema,
url: route.url
}
}
})
fastify.get('/example', () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('transform can hide routes based on openapi version', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: { info: { version: '2.0.0' } },
transform: ({ schema, route, openapiObject }) => {
const transformedSchema = Object.assign({}, schema)
if (route?.constraints?.version !== openapiObject.info.version) transformedSchema.hide = true
return { schema: transformedSchema, url: route.url }
}
})
fastify.get('/example', { constraints: { version: '1.0.0' } }, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(openapiObject.paths['/example'], undefined)
})
test('endpoint transform should fail with a value other than Function', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, {})
fastify.setValidatorCompiler(({ schema }) => params.validate(schema))
fastify.get('/example', {
...opts,
config: {
swaggerTransform: 'wrong type'
}
}, () => {})
await fastify.ready()
t.assert.throws(fastify.swagger)
})
test('endpoint transform should work with a Function', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, valid)
fastify.setValidatorCompiler(({ schema }) => params.validate(schema))
fastify.get('/example', {
...opts,
config: { swaggerTransform: validTransform }
}, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('endpoint transform can access route', async (t) => {
t.plan(5)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: { info: { version: '1.0.0' } }
})
fastify.get('/example', {
constraints: { version: '1.0.0' },
config: {
swaggerTransform: ({ route }) => {
t.assert.ok(route)
t.assert.strictEqual(route.method, 'GET')
t.assert.strictEqual(route.url, '/example')
t.assert.strictEqual(route.constraints.version, '1.0.0')
return { schema: route.schema, url: route.url }
}
}
}, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('endpoint transform can access openapi object', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: { info: { version: '1.0.0' } }
})
fastify.get('/example', {
config: {
swaggerTransform: ({ route, openapiObject }) => {
t.assert.ok(openapiObject)
t.assert.strictEqual(openapiObject.openapi, '3.0.3')
t.assert.strictEqual(openapiObject.info.version, '1.0.0')
return {
schema: route.schema,
url: route.url
}
}
}
}, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('endpoint transform can access swagger object', async (t) => {
t.plan(4)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
swagger: { info: { version: '1.0.0' } }
})
fastify.get('/example', {
config: {
swaggerTransform: ({ route, swaggerObject }) => {
t.assert.ok(swaggerObject)
t.assert.strictEqual(swaggerObject.swagger, '2.0')
t.assert.strictEqual(swaggerObject.info.version, '1.0.0')
return {
schema: route.schema,
url: route.url
}
}
}
}, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('endpoint transform can hide routes based on openapi version', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: { info: { version: '2.0.0' } }
})
fastify.get('/example', {
constraints: { version: '1.0.0' },
config: {
swaggerTransform: ({ schema, route, openapiObject }) => {
const transformedSchema = Object.assign({}, schema)
if (route?.constraints?.version !== openapiObject.info.version) transformedSchema.hide = true
return { schema: transformedSchema, url: route.url }
}
}
}, () => {})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(openapiObject.paths['/example'], undefined)
})
test('endpoint transform takes precedence over global swagger transform', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
swagger: { info: { version: '1.0.0' } },
transform: ({ schema, url }) => {
t.assert.fail('the global transform function should be ignored')
return validTransform({ schema, url })
}
})
fastify.get('/example', {
config: {
swaggerTransform: ({ schema, route }) => {
const transformedSchema = Object.assign({}, schema)
t.assert.ok(transformedSchema)
return { schema: transformedSchema, url: route.url }
}
}
}, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('endpoint transform takes precedence over global openapi transform', async (t) => {
t.plan(2)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: { info: { version: '2.0.0' } },
transform: ({ schema, url }) => {
t.assert.fail('the global transform function should be ignored')
return validTransform({ schema, url })
}
})
fastify.get('/example', {
config: {
swaggerTransform: ({ schema, route }) => {
const transformedSchema = Object.assign({}, schema)
t.assert.ok(transformedSchema)
return { schema: transformedSchema, url: route.url }
}
}
}, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('endpoint transform with value "false" disables the global swagger transform', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, {
swagger: { info: { version: '1.0.0' } },
transform: () => { throw Error('should not be run') }
})
fastify.get('/example/:id', {
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string'
}
}
}
},
config: {
swaggerTransform: false
}
}, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('endpoint transform with value "false" disables the global openapi transform', async (t) => {
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: { info: { version: '2.0.0' } },
transform: () => { throw Error('should not be run') }
})
fastify.get('/example/:id', {
schema: {
params: {
type: 'object',
properties: {
id: {
type: 'string'
}
}
}
},
config: {
swaggerTransform: false
}
}, () => {})
await fastify.ready()
t.assert.doesNotThrow(fastify.swagger)
})
test('transformObject can modify the openapi object', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: { info: { version: '2.0.0' } },
transformObject: ({ openapiObject }) => {
openapiObject.info.title = 'Transformed'
return openapiObject
}
})
await fastify.ready()
const openapiObject = fastify.swagger()
t.assert.strictEqual(openapiObject.info.title, 'Transformed')
})
test('transformObject can modify the swagger object', async (t) => {
t.plan(1)
const fastify = Fastify()
await fastify.register(fastifySwagger, {
swagger: { info: { version: '2.0.0' } },
transformObject: ({ swaggerObject }) => {
swaggerObject.info.title = 'Transformed'
return swaggerObject
}
})
await fastify.ready()
const swaggerObject = fastify.swagger()
t.assert.strictEqual(swaggerObject.info.title, 'Transformed')
})

163
backend/node_modules/@fastify/swagger/test/util.test.js generated vendored Normal file
View File

@@ -0,0 +1,163 @@
'use strict'
const { test, describe } = require('node:test')
const { formatParamUrl } = require('../lib/util/format-param-url')
const { hasParams, matchParams } = require('../lib/util/match-params')
const { generateParamsSchema, paramName } = require('../lib/util/generate-params-schema')
const { shouldRouteHide } = require('../lib/util/should-route-hide')
const cases = [
['/example/:userId', '/example/{userId}'],
['/example/:userId/:secretToken', '/example/{userId}/{secretToken}'],
['/example/near/:lat-:lng/radius/:r', '/example/near/{lat}-{lng}/radius/{r}'],
['/example/near/:lat_1-:lng_1/radius/:r_1', '/example/near/{lat_1}-{lng_1}/radius/{r_1}'],
['/example/*', '/example/{*}'],
['/example/:file(^\\d+).png', '/example/{file}.png'],
['/example/at/:hour(^\\d{2})h:minute(^\\d{2})m', '/example/at/{hour}h{minute}m'],
['/example/at/(^\\d{2})h(^\\d{2})m', '/example/at/{regexp1}h{regexp2}m'],
['/example/at/(^([0-9]{2})h$)-(^([0-9]{2})m$)', '/example/at/{regexp1}-{regexp2}'],
['/name::verb', '/name:verb'],
['/api/v1/postalcode-jp/:code(^[0-9]{7}$)', '/api/v1/postalcode-jp/{code}'],
['/api/v1/postalcode-jp/(^[0-9]{7}$)', '/api/v1/postalcode-jp/{regexp1}']
]
describe('formatParamUrl', () => {
for (const kase of cases) {
test(`formatParamUrl ${kase}`, (t) => {
t.assert.strictEqual(formatParamUrl(kase[0]), kase[1])
})
}
})
describe('hasParams function', () => {
test('should return false for empty url', (t) => {
const url = ''
const result = hasParams(url)
t.assert.strictEqual(result, false)
})
test('should return true for url with parameters', (t) => {
const url = '/example/{userId}'
const result = hasParams(url)
t.assert.strictEqual(result, true)
})
test('should return true for url with multiple parameters', (t) => {
const url = '/example/{userId}/{secretToken}'
const result = hasParams(url)
t.assert.strictEqual(result, true)
})
test('should return false for url without parameters', (t) => {
const url = '/example/path'
const result = hasParams(url)
t.assert.strictEqual(result, false)
})
})
describe('matchParams function', (t) => {
test('should return an empty array for empty url', (t) => {
const url = ''
const result = matchParams(url)
t.assert.deepStrictEqual(result, [])
})
test('should return an array of matched parameters', (t) => {
const url = '/example/{userId}/{secretToken}'
const result = matchParams(url)
t.assert.deepStrictEqual(result, ['{userId}', '{secretToken}'])
})
test('should return an empty array for url without parameters', (t) => {
const url = '/example/path'
const result = matchParams(url)
t.assert.deepStrictEqual(result, [])
})
})
describe('generateParamsSchema function', (t) => {
const urlsToShemas = [
[
'/example/{userId}', {
params: {
type: 'object',
properties: {
userId: {
type: 'string'
}
}
}
}
],
[
'/example/{userId}/{secretToken}', {
params: {
type: 'object',
properties: {
userId: {
type: 'string'
},
secretToken: {
type: 'string'
}
}
}
}
],
[
'/example/near/{lat}-{lng}', {
params: {
type: 'object',
properties: {
lat: {
type: 'string'
},
lng: {
type: 'string'
}
}
}
}
]
]
test('generateParamsSchema', (t) => {
for (const [url, expectedSchema] of urlsToShemas) {
const result = generateParamsSchema(url)
t.assert.deepStrictEqual(result, expectedSchema)
}
})
})
describe('paramName function', () => {
test('should return the captured value from the param', (t) => {
const param = '{userId}'
const result = paramName(param)
t.assert.strictEqual(result, 'userId')
})
test('should return the same value if there are no captures', (t) => {
const param = 'userId'
const result = paramName(param)
t.assert.strictEqual(result, 'userId')
})
})
describe('shouldRouteHide', () => {
test('shouldRouteHide should return true for hidden route', (t) => {
t.assert.ok(shouldRouteHide({ hide: true }, {}))
})
test('shouldRouteHide should return true for hideUntagged', (t) => {
t.assert.ok(shouldRouteHide({ tags: [] }, { hideUntagged: true }))
})
test('shouldRouteHide should return true for hiddenTag', (t) => {
t.assert.ok(shouldRouteHide({ tags: ['x-test'] }, { hiddenTag: 'x-test' }))
})
test('shouldRouteHide should return false for non hidden route', (t) => {
t.assert.equal(shouldRouteHide({}, {}), false)
})
})