feathers-swagger
![Greenkeeper badge](https://badges.greenkeeper.io/feathersjs-ecosystem/feathers-swagger.svg)
![Download Status](https://img.shields.io/npm/dm/feathers-swagger.svg?style=flat-square)
Add documentation to your Featherjs services and show them in the Swagger UI.
This version is configured to work with Swagger UI 3.x
Installation
npm install feathers-swagger --save
API
swagger(options)
Initializes the module. Should be provided to app.configure before the registration of services.
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const swagger = require('feathers-swagger');
const app = express(feathers())
.use(express.json())
.use(express.urlencoded({ extended: true }))
.configure(express.rest());
app.configure(swagger({
specs: {
info: {
title: 'A test',
description: 'A description',
version: '1.0.0',
},
}
}));
app.use('/message', messageService);
Options:
specs
(required) - Global specifications that should at least contain the info section to generate a valid swagger specificationopenApiVersion
- (optional, default: 2
) - OpenApi version the specification will be generated for. Allowed: 2 or 3docsPath
(optional, default: '/docs'
) - The path where the swagger json / ui will be available.docsJsonPath
(optional) - The path where the swagger json will be available (independently of request Accept header).uiIndex
(optional) - Configuration of swagger ui initialization, possibilities:
false
- Disable swagger ui, the json specification will be available at docsPaths
true
- Enable default swagger ui with index from node_modules package'path/to/doc.html'
- Enable swagger ui with the provided file as indexfunction(req, res)
- A function with customized initialization
idType
(optional) - The default swagger type of ids used in paths, 'integer'
will be used when not providedprefix
(optional) - Used for automatic tag and name generation for servicesversionPrefix
(optional) - Used for automatic tag and name generation for servicesinclude
(optional) - Object to configure for which services documentation will be generated, empty means all will be included:
tags
- Array of tags for that service documentation will be generatedpaths
- Array of paths (string or regex) for that service documentation will be generated, Notice: paths dont start with /
ignore
(optional) - Object to configure to ignore with the following keys:
tags
- Array of tags for that no service documentation will be generatedpaths
- Array of paths (string or regex) for that no service documentation will be generated, Notice: paths dont start with /
appProperty
(optional, default: docs
) - Property of the feathers app object that the generated specification will be saved to, allows custom post processing; set empty to disabledefaults
(optional) - Object to customize the defaults for generation of the specification
getOperationArgs({ service, path, config, apiPath, version })
- method to generate args that the methods for operations will consume, can also customize default tag and model generationgetOperationsRefs(model, service)
- method to generate refs that the methods for operations will consume, see service.docs.refs optionoperationGenerators
- generator functions to fully customize specification generation for operations
find
|get
|create
|update
|patch
|remove
- generator function for the specific operationcustom
- generator function for all custom operations
operations
- objects with defaults for the operations, with path support to update nested structures
find
|get
|create
|update
|patch
|remove
|nameOfCustomMethod
- to change defaults of a specific operationall
- to change defaults of all operations
service.id
Defines the name of the id in the swagger path, by default it is 'id'
;
service.docs
If you want to customize the specifications generation for a service you can configure it by providing a options object as docs
property of the service.
messageService.docs = {
description: 'My service description',
definition: {
type: 'object',
required: [
'text'
],
properties: {
text: {
type: 'string',
description: 'The message text'
},
userId: {
type: 'string',
description: 'The id of the user that send the message'
}
}
}
};
Options:
tag
(optional) - Override tag that is parsed from pathdescription
(optional) - Provide a description for the service documentation (tag)externalDocs
(optional) - Add external docs to service documentation (tag)tags
(optional) - Give multiple tagsmodel
(optional) - Override model that is parsed from pathmodelName
(optional) - Override modelName that is parsed from pathidType
(optional) - The swagger type of ids used in paths for this servicedefinition
(also schema
for openapi v3) (optional) - Swagger definition of the model of the service, will be merged into global definitions (with all additional generated definitions)definitions
(also schemas
for openapi v3) (optional) - Swagger definitions that will merged in the global definitionssecurities
(optional) - Array of operation names that are secured by global security definition, use all
to enable security for all operations of the serviceoperations
(optional) - Object with specifications for the operations / methods of the service. Support path keys to update specific nested structures.
find
|get
|create
|update
|patch
|remove
|nameOfCustomMethod
- Custom (parts of the) specification for the operation, can alternatively be set as doc property of the method. To disable the generation set to false.all
- Custom (parts of the) specification for all operations.
refs
(optional) - Change the refs that are used for different operations: findResponse, getResponse, createRequest, createResponse, updateRequest, updateResponse, patchRequest, patchResponse, removeResponse, {customMethodName[Request|Response]}pathParams
(optional) - Object with param name as key and the definition as value, should be used when using "global" path parametersoverwriteTagSpec
(optional, default: false
) - If tag is already defined in the specs, should be overwritten from this service
Path support to update nested structures
To be able to set only parts of a nested structure the keys of a specification object (used to define operation specifications) can be the path that should be updated.
For that the set
method of lodash is used with additional support to push and unshift for arrays. Also setting undefined will remove the value at the given path.
Take into account that the order of defined keys matters!
Valid push syntax:
path[]
path[+]
path[+D]
with D being digits (needed to be able to define more than one element to push, the digits does not refer to a position)
Valid unshift syntax:
path[-]
path[-D]
with D being digits (needed to be able to define more than one element to unshift, the digits does not refer to a position)
Examples
Notice: There are more detailed examples in the example folder.
npm install @feathersjs/feathers @feathersjs/express feathers-memory feathers-swagger
Basic example
Here's an example of a Feathers server that uses feathers-swagger
.
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const memory = require('feathers-memory');
const messageService = memory();
messageService.docs = {
description: 'A service to send and receive messages',
definitions: {
messages: {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "The message text"
},
"userId": {
"type": "string",
"description": "The id of the user that sent the message"
}
}
}
}
};
const app = express(feathers())
.use(express.json())
.use(express.urlencoded({ extended: true }))
.configure(express.rest())
.configure(swagger({
docsPath: '/docs',
specs: {
info: {
title: 'A test',
description: 'A description',
version: '1.0.0',
},
},
}))
.use('/messages', messageService);
app.listen(3030);
Go to http://localhost:3030/docs to see the Swagger JSON documentation.
Example with Feathers Generate app
- Go into your
src/services/
folder, and open the service you want to edit PATH.service.js
- Change from this:
app.use('/events', createService(options));
to this:
const events = createService(options);
events.docs = {
operations: {
find: {
'parameters[]': {
description: 'Property to query results',
in: 'query',
name: '$search',
type: 'string'
},
},
},
definitions: {
event: mongooseToJsonLibraryYouImport(Model),
'event_list': {
type: 'array',
items: { $ref: '#/definitions/event' }
}
}
};
app.use('/events', events);
The overrides work at a property level - if you pass in find.parameters, that whole object will be used, it is not merged in.
If you want to update only parts there is support of path keys to update specific nested structures.
You can find more information in the utils.js file to get an idea of what is passed in.
Example with UI
The uiIndex
option allows to set a Swagger UI index file which will host the UI at docsPath
.
const path = require('path');
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const memory = require('feathers-memory');
const messageService = memory();
messageService.docs = {
description: 'A service to send and receive messages',
definitions: {
messages: {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "The message text"
},
"useId": {
"type": "string",
"description": "The id of the user that send the message"
}
}
}
}
};
const app = express(feathers())
.use(express.json())
.use(express.urlencoded({ extended: true }))
.configure(express.rest())
.configure(swagger({
docsPath: '/docs',
uiIndex: path.join(__dirname, 'docs.html'),
specs: {
info: {
title: 'A test',
description: 'A description',
version: '1.0.0',
},
},
}))
.use('/messages', messageService);
app.listen(3030);
Create a docs.html
page like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI - Simple</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700"
rel="stylesheet">
<link rel="stylesheet" type="text/css" href="./swagger-ui.css">
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"
/>
</symbol>
<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"
/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"
/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"
/>
</symbol>
<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z" />
</symbol>
<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" />
</symbol>
</defs>
</svg>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js">
</script>
<script src="./swagger-ui-standalone-preset.js">
</script>
<script>
window.onload = function () {
if(window.SwaggerTranslator) {
window.SwaggerTranslator.translate();
}
const url = '/docs';
const ui = SwaggerUIBundle({
url,
dom_id: '#swagger-ui',
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
onComplete: function(swaggerApi, swaggerUi){
if(typeof initOAuth == "function") {
initOAuth({
clientId: "your-client-id",
clientSecret: "your-client-secret-if-required",
realm: "your-realms",
appName: "your-app-name",
scopeSeparator: " ",
additionalQueryStringParams: {}
});
}
if(window.SwaggerTranslator) {
window.SwaggerTranslator.translate();
}
},
onFailure: function(data) {
log("Unable to Load SwaggerUI");
},
docExpansion: "none",
jsonEditor: false,
defaultModelRendering: 'schema',
showRequestHeaders: false,
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
window.ui = ui;
$('#input_apiKey').change(function() {
var key = $('#input_apiKey')[0].value;
log("key: " + key);
if(key && key.trim() !== "") {
log("added key " + key);
window.authorizations.add("key", new ApiKeyAuthorization("api_key", key, "query"));
}
});
}
</script>
</body>
</html>
Now http://localhost:3030/docs/ will show the documentation in the browser using the Swagger UI.
You can also use uiIndex: true
to use the default Swagger UI.
Prefixed routes
If your are using versioned or prefixed routes for your API like /api/<version>/users
, you can configure it using the
prefix
property so all your services don't end up in the same group. The value of the prefix
property can be either
a string or a RegEx.
const app = express(feathers())
.configure(swagger({
prefix: /api\/v\d\//,
docsPath: '/docs',
specs: {
info: {
title: 'A test',
description: 'A description',
version: '1.0.0',
},
},
}))
.use('/api/v1/messages', messageService);
app.listen(3030);
To display your API version alongside the service name, you can also define a versionPrefix
to be extracted:
const app = express(feathers())
.configure(swagger({
prefix: /api\/v\d\//,
versionPrefix: /v\d/,
docsPath: '/docs',
specs: {
info: {
title: 'A test',
description: 'A description',
version: '1.0.0',
},
},
}))
.use('/api/v1/messages', messageService);
app.listen(3030);
Migration
Version X.X.X introduces some breaking changes to previous 0.7.x versions. These changes and ways to migrate to the new release will be described here.
Introduction of specs option
To not mix up options and specification as before all specifications go into the new specs option.
Before
swagger({
prefix: /api\/v\d\//,
versionPrefix: /v\d/,
docsPath: '/docs',
info: {
title: 'A test',
description: 'A description',
version: '1.0.0'
},
definitions: {
},
})
After
swagger({
prefix: /api\/v\d\//,
versionPrefix: /v\d/,
docsPath: '/docs',
specs: {
info: {
title: 'A test',
description: 'A description',
version: '1.0.0'
},
definitions: {
},
},
})
Introduction of operations option of the service.doc config
With introduction of custom methods support it made more sense (also because of TypeScript) to nest operations specifications.
Before
messageService.docs = {
find: {
description: 'My description',
},
};
After
messageService.docs = {
operations: {
find: {
description: 'My description',
},
},
};
Remove of findQueryParameters option
This option was very specific to add (prepend) parameters to all find operations.
With the introduced option to customize defaults you can also add more default find parameters.
Before
swagger({
findQueryParameters: [
{
description: 'My custom query parameter',
in: 'query',
name: '$custom',
type: 'string'
},
],
})
After
swagger({
defaults: {
find: {
'parameters[-]': {
description: 'My custom query parameter',
in: 'query',
name: '$custom',
type: 'string'
}
}
}
})
Generate RFC3986-compliant percent-encoded URIs
To generate valid specifications there may not be spaces in $ref links.
Therefor concatenation is done with _ by default now. This applies to list and version refs of the previous version.
Before
messageService.docs = {
definitions: {
message: { },
'message list': {
type: 'array',
items: { $ref: '#/definitions/message' }
}
}
};
messageV1Service.docs = {
definitions: {
'message v1': { },
'message v1 list': {
type: 'array',
items: { $ref: '#/definitions/message v1' }
}
}
};
After
messageService.docs = {
definitions: {
message: { },
message_list: {
type: 'array',
items: { $ref: '#/definitions/message' }
}
}
};
messageV1Service.docs = {
definitions: {
message_v1: { },
message_v1_list: {
type: 'array',
items: { $ref: '#/definitions/message_v1' }
}
}
};
Removal of sequelize related utils
Because sequelize is not in the scope of this package the utils getType, getFormat, property and definition were removed.
If you used them extract them from an old version or use other packages which provide this functionality.
Before
const { definition } = 'feathers-swagger';
messageService.docs = {
definition: {
message: definition(Model),
}
};
After
const sequelizeJsonSchema = require('sequelize-json-schema');
messageService.docs = {
definition: {
message: sequelizeJsonSchema(Model),
}
};
Overwriting of already defined tags specifications is now opt-in
Introduced with PR: Fix: docs ignored when path already exists #69
the last registered service will always overwrite previously defined tags. To be able to handle it by config
the overwriteTagSpec
was introduced. It defaults to false, which is a breaking change.
Before
app.use('/projects/:projectId/sync', projectsSyncService);
const docs = { description: 'My Project Service' };
app.use('/projects', Object.assign(service(options), { docs }) );
After
app.use('/projects/:projectId/sync', projectsSyncService);
const docs = { description: 'My Project Service', overwriteTagSpec: true };
app.use('/projects', Object.assign(service(options), { docs }) );
License
Copyright (c) 2016 - 2019
Licensed under the MIT license.