feathers-swagger
feathers-swagger copied to clipboard
Add documentation to your FeatherJS services and feed them to Swagger UI.
feathers-swagger
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());
// ... configure app
app.configure(swagger({
specs: {
info: {
title: 'A test',
description: 'A description',
version: '1.0.0',
},
schemes: ['http', 'https'] // Optionally set the protocol schema used (sometimes required when host on https)
}
}));
// now you can register feathers services
app.use('/message', messageService);
Options:
-
specs
(required) - Global specifications that should at least contain the info section to generate a valid swagger specification -
openApiVersion
- (optional, default:2
) - OpenApi version the specification will be generated for. Allowed: 2 or 3 -
docsPath
(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 atdocsPaths
-
true
- Enable default swagger ui with index from node_modules package -
'path/to/doc.html'
- Enable swagger ui with the provided file as index -
function(req, res)
- A function with customized initialization
-
-
idType
(optional) - The default swagger type of ids used in paths,'integer'
will be used when not provided -
prefix
(optional) - Used for automatic tag and name generation for services -
versionPrefix
(optional) - Used for automatic tag and name generation for services -
include
(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 generated -
paths
- 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 generated -
paths
- 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 disable -
defaults
(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 generation -
getOperationsRefs(model, service)
- method to generate refs that the methods for operations will consume, see service.docs.refs option -
schemasGenerator(service, model, modelName, schemas)
- method to generate the json schemas for a service -
operationGenerators
- generator functions to fully customize specification generation for operations-
find
|get
|create
|update
|patch
|remove
- generator function for the specific operation -
updateMulti
|patchMulti
|removeMulti
- generator function for the "multi mode" version of the specific operation -
custom
- 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 operation -
updateMulti
|patchMulti
|removeMulti
- to change defaults for "multi mode" of a specific operation -
all
- to change defaults of all operations
-
-
multi
- array with operations that should also be available in "multi mode", use'all'
to enable for all operations that support it
-
service.id
Defines the name of the id in the swagger path, by default it is 'id'
;
This can also be an array, if multiple ids are used. For example with feathers-objection or feathers-cassandra.
For services that extend adapter-commons this is set as parameter in the options when initializing the service.
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.
// service generation
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 path -
description
(optional) - Provide a description for the service documentation (tag) -
externalDocs
(optional) - Add external docs to service documentation (tag) -
tags
(optional) - Give multiple tags -
model
(optional) - Override model that is parsed from path -
modelName
(optional) - Override modelName that is parsed from path -
idType
(optional) - The swagger type of ids used in paths for this service. Value can be an array of types whenservice.id
is set as an array. -
idNames
(optional) - Object with path parameter names, to customize the idName on operation / method level-
get
|update
|patch
|remove
- name of the path parameter for the specific method, use service.id to change it for all
-
-
definition
(alsoschema
for openapi v3) (optional) - Swagger definition of the model of the service, will be merged into global definitions (with all additional generated definitions) -
definitions
(alsoschemas
for openapi v3) (optional) - Swagger definitions that will merged in the global definitions -
securities
(optional) - Array of operation names that are secured by global security definition, use'all'
to enable security for all operations of the service -
multi
(optional) - Array of operation names that will also be available in "multi mode" (without id path parameter to operate on multiple resources), use'all'
to enable it for all operations that support it -
operations
(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. -
updateMulti
|patchMulti
|removeMulti
- if "multi mode" is enabled for the specific method, custom parts of the specification can be overwritten. 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 parameters -
overwriteTagSpec
(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 swagger = require('feathers-swagger');
const messageService = memory();
// swagger spec for this service, see http://swagger.io/specification/
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 editPATH.service.js
- Change from this:
// Initialize our service with any options it requires
app.use('/events', createService(options));
to this:
const events = createService(options);
events.docs = {
//overwrite things here.
//if we want to add a mongoose style $search hook to find, we can write this:
operations: {
find: {
'parameters[]': {
description: 'Property to query results',
in: 'query',
name: '$search',
type: 'string'
},
},
},
//if we want to add the mongoose model to the 'definitions' so it is a named model in the swagger ui:
definitions: {
event: mongooseToJsonLibraryYouImport(Model), //import your own library, use the 'Model' object in this file.
'event_list': { //this library currently configures the return documentation to look for ``${tag} 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 swagger = require('feathers-swagger');
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:
<!-- HTML for static distribution bundle build -->
<!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 () {
// Pre load translate...
if(window.SwaggerTranslator) {
window.SwaggerTranslator.translate();
}
const url = '/docs';
// Build a system
const ui = SwaggerUIBundle({
url,
dom_id: '#swagger-ui',
// fine tuning...
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 app
.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 app
.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);
Model Schemas
Please note that feathers-swagger does not generate model schemas so you might encounter errors similar to the one below when initially using the Swagger UI.
Could not resolve reference: Could not resolve pointer: /components/schemas/
does not exist in document
To resolve, either manually define your model schemas or consider automated alternatives like:
Migration
Version 1.0.0 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: {
operations: {
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: { /* definition */ },
'message list': {
type: 'array',
items: { $ref: '#/definitions/message' }
}
}
};
// Versioned service
messageV1Service.docs = {
definitions: {
'message v1': { /* definition */ },
'message v1 list': {
type: 'array',
items: { $ref: '#/definitions/message v1' }
}
}
};
After
messageService.docs = {
definitions: {
message: { /* definition */ },
message_list: {
type: 'array',
items: { $ref: '#/definitions/message' }
}
}
};
// Versioned service
messageV1Service.docs = {
definitions: {
message_v1: { /* definition */ },
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: definition(Model),
};
After
const sequelizeJsonSchema = require('sequelize-json-schema');
messageService.docs = {
definition: 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);
// use tag from second service
const docs = { description: 'My Project Service' };
app.use('/projects', Object.assign(service(options), { docs }) );
After
// ...
app.use('/projects/:projectId/sync', projectsSyncService);
// use tag from second service
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.