├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── contributing.md ├── pull_request_template.md └── workflows │ └── ci.yaml ├── .github_changelog_generator ├── .gitignore ├── .mocharc.js ├── .npmignore ├── .nycrc.json ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── CHANGELOG.md ├── _sidebar.md ├── api.md ├── custom-methods.md ├── docsify │ ├── prism-lines.js │ └── replace.js ├── examples │ ├── _sidebar.md │ ├── authentication_v5.md │ ├── authentication_v5_plugin.json │ ├── basic.md │ ├── custom_methods.json │ ├── custom_methods.md │ ├── generated_app_v4.md │ ├── generated_service_v5.md │ ├── index.md │ ├── prefixed_routes.md │ └── ui.md ├── getting-started.md ├── index.html ├── migrations │ ├── MIGRATIONS_v1.md │ ├── MIGRATIONS_v2.md │ └── MIGRATIONS_v3.md └── swagger-ui │ ├── index.html │ └── swagger-initializer.js ├── example ├── app.js ├── docs.html ├── index.html ├── openapi-v3 │ ├── customMethods.js │ ├── customTags.js │ ├── definitionWithCustomizedSpec.js │ ├── definitions.js │ ├── docs.html │ ├── doveSchemas.js │ ├── idNames.js │ ├── multi.js │ └── security.js └── swagger-v2 │ ├── customMethods.js │ ├── customTags.js │ ├── definitionWithCustomizedSpec.js │ ├── definitions.js │ ├── docs.html │ ├── multi.js │ └── security.js ├── lib ├── custom-methods.js ├── helpers.js ├── index.js ├── openapi.js ├── swagger-ui-dist.js ├── utils.js ├── v2 │ └── generator.js └── v3 │ └── generator.js ├── package-lock.json ├── package.json ├── test ├── custom-methods-v4.test.js ├── custom-methods-v5.test.js ├── custom-methods.test.js ├── helper.js ├── helpers.test.js ├── index.test.js ├── ui.test.js ├── utils.test.js ├── v2 │ ├── expected-memory-spec-multi-only.json │ ├── expected-memory-spec-pagination-find.json │ ├── expected-memory-spec.json │ └── generator.test.js └── v3 │ ├── expected-memory-spec-multi-only.json │ ├── expected-memory-spec-pagination-find.json │ ├── expected-memory-spec.json │ └── generator.test.js └── types ├── index.d.ts ├── index.test-d.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "Bug:" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | (First please check that this issue is not already solved as [described 11 | here](https://github.com/feathersjs-ecosystem/feathers-swagger/blob/master/.github/contributing.md#report-a-bug)) 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior. 18 | If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **System configuration** 24 | Tell us about the applicable parts of your setup. 25 | - Package versions: [e.g. feathers-swagger 1.0.0, @feathersjs/feathers 3.3.0 ] 26 | - NodeJS version: [e.g. 8.4.0] 27 | - OS [e.g. windows] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "Feature:" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: If you have question use this template 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | If you have something to ask that is not related to reporting a bug or creating a feature request use this template. 11 | 12 | For direct contact there is also the possibility to try out [Slack](https://feathersjs.slack.com). 13 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers Ecosystem 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | ## Report a bug 6 | 7 | Before creating an issue please make sure you have checked out the docs. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 8 | 9 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 10 | 11 | Issues can be reported in the [issue tracker](https://github.com/feathersjs-ecosystem/feathers-swagger/issues). 12 | 13 | ## Pull Requests 14 | 15 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute. 16 | 17 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A FeathersJS maintainer will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 18 | 19 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context, historical knowledge or information regarding the roadmap that the maintainers have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 20 | 21 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules. If needed updates to TypeScript types should be included.** 22 | 23 | ### Code style 24 | 25 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 26 | 27 | ### Tests 28 | 29 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 30 | 31 | ### TypeScript types 32 | 33 | As the TypeScript types are part of the repository they have to be updated with code changes. They are located in `types` and can be tested running `npm run dtslint` or `npm test`. 34 | 35 | ### Documentation 36 | 37 | Updates to the documentation should be made in the README.md. 38 | 39 | ## Contributor Code of Conduct 40 | 41 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 42 | 43 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 44 | 45 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 46 | 47 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 48 | 49 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 50 | 51 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 52 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs-ecosystem/feathers-swagger/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | Pull requests should contain updates to documentation, tests and typescript types when necessary. 13 | 14 | ### Other Information 15 | 16 | If there's anything else that's important and relevant to your pull 17 | request, mention that information here. This could include 18 | benchmarks, or other information. 19 | 20 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. 21 | 22 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 23 | 24 | Thanks for contributing to Feathers! :heart: 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [14.x, 16.x, 18.x, 20.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: install npm@8 21 | if: matrix.node-version == '14.x' 22 | run: npm i -g npm@8 23 | - run: npm ci 24 | - run: npm run test:ci 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | user=feathersjs-ecosystem 2 | project=feathers-swagger 3 | future-release=v4.0.0 4 | release-branch=master 5 | issues-of-open-milestones=false 6 | unreleased=false 7 | output=docs/CHANGELOG.md 8 | exclude-tags-regex=.*-pre.* 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | .nyc_output/ 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # Commenting this out is preferred by some people, see 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 28 | node_modules 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | 33 | dist/ 34 | /nbproject/ 35 | .idea/ 36 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | recursive: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .jshintrc 3 | .travis.yml 4 | .istanbul.yml 5 | .babelrc 6 | .idea/ 7 | .vscode/ 8 | test/ 9 | coverage/ 10 | .nyc_output 11 | .nycrc.json 12 | mocha.opts 13 | .github/ 14 | types/index.test-d.ts 15 | types/tsconfig.json 16 | example 17 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "per-file": true, 4 | "lines": [50, 80], 5 | "statements": [50, 80], 6 | "functions": [50, 80], 7 | "branches": [50, 80], 8 | "reporter": [ 9 | "html", 10 | "text", 11 | "text-summary", 12 | "lcov" 13 | ], 14 | "include": [ 15 | "lib/**/*.js" 16 | ], 17 | "cache": true, 18 | "all": true 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-swagger 2 | 3 | [![CI](https://github.com/feathersjs-ecosystem/feathers-swagger/actions/workflows/ci.yaml/badge.svg)](https://github.com/feathersjs-ecosystem/feathers-swagger/actions/workflows/ci.yaml) 4 | [![Download Status](https://img.shields.io/npm/dm/feathers-swagger.svg?style=flat-square)](https://www.npmjs.com/package/feathers-swagger) 5 | 6 | Add [OpenAPI](https://swagger.io/resources/open-api/) documentation to your Feathers services and optionally show them in the [Swagger UI](https://swagger.io/tools/swagger-ui/). 7 | 8 | You can also add custom methods to feathers services that will be available as rest routes but not via the feathers client. 9 | 10 | Checkout the [Documentation](https://feathersjs-ecosystem.github.io/feathers-swagger) for further information. 11 | 12 | [Changelog](./docs/CHANGELOG.md) 13 | 14 | ## License 15 | 16 | Copyright (c) 2016 - 2023 17 | 18 | Licensed under the [MIT license](LICENSE). 19 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-swagger/657ab29c9f470b0341805b193791adbeeb1693df/docs/.nojekyll -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [Getting Started](/) 2 | * [API](/api.md) 3 | * [Custom Methods](/custom-methods.md) 4 | * [Examples](/examples/index.md) 5 | * Migrations 6 | * * [Migrations for Version 1](/migrations/MIGRATIONS_v1.md) 7 | * * [Migrations for Version 2](/migrations/MIGRATIONS_v2.md) 8 | * * [Migrations for version 3](/migrations/MIGRATIONS_v3.md) 9 | -------------------------------------------------------------------------------- /docs/custom-methods.md: -------------------------------------------------------------------------------- 1 | # Custom Methods 2 | 3 | ## Introduction 4 | 5 | If you want to define non-CRUD action there are some possibilities. The [feathers documentation](https://docs.feathersjs.com/help/faq.html#how-do-i-create-custom-methods) proposes the usage of hooks or custom service. 6 | 7 | In feathers 4 custom (express) methods were introduced but never documented. There is also the drawback that they are registered after the default methods, so a /service/{id} will always win over a custom /service/custom (only POST has no /{id} route and can be used without restrictions). 8 | 9 | Feathers 5 replaced the custom methods implementation and now custom methods are also supported via websockets and feathers-client. Here the drawback is, that the custom methods for http transports are only kind of second class citizen by utilizing headers. They cannot be documented with OpenAPI in a good way. 10 | 11 | If you want to have custom RPC style actions, that can be documented via OpenAPI you can use the custom method "wrapper" that feathers-swagger provides. 12 | 13 | ## Requirements 14 | 15 | To use custom methods with feathers 5 the `customMethodHandler` has to be registered, before rest services. For koa the `@koa/router` has to be installed. 16 | 17 | See [Usage](/#installation) for detailed information. 18 | 19 | ## Usage 20 | 21 | ### Feathers 4 22 | 23 | For feathers 4 it is enough to wrap / annotate the custom method 24 | 25 | 26 | 27 | #### **Javascript object** 28 | 29 | ```js 30 | const { customMethod } = require('feathers-swagger'); 31 | 32 | const someService = { 33 | async find() {}, 34 | custom: customMethod('POST', '/some/custom')(async (data, params) => { 35 | return { data, queryParams: params.query, routeParams: params.route }; 36 | }), 37 | }; 38 | ``` 39 | 40 | #### **Typescript with decorators** 41 | 42 | ```typescript 43 | import { customMethod } from 'feathers-swagger'; 44 | 45 | class SomeService { 46 | async find() {} 47 | 48 | @customMethod('POST', '/some/custom') 49 | async custom(data: any, params: Params) { 50 | return { data, queryParams: params.query, routeParams: params.route }; 51 | } 52 | } 53 | ``` 54 | 55 | 56 | 57 | ### Feathers 5 58 | 59 | For feathers 5 in addition to the wrapping / annotation of the custom method 60 | you have to define the custom method when registering the service. 61 | 62 | 63 | 64 | #### **Javascript object** 65 | 66 | ```js 67 | const { customMethod } = require('feathers-swagger'); 68 | 69 | const someService = { 70 | async find() {}, 71 | custom: customMethod('POST', '/custom')(async (data, params) => { 72 | return { data, queryParams: params.query, routeParams: params.route }; 73 | }), 74 | }; 75 | 76 | app.use('some', someService, { 77 | methods: ['find', 'custom'], 78 | events: [] 79 | }); 80 | ``` 81 | 82 | #### **Typescript with decorators** 83 | 84 | ```typescript 85 | import { customMethod } from 'feathers-swagger'; 86 | 87 | class SomeService { 88 | async find(param: Params) { 89 | return []; 90 | } 91 | 92 | @customMethod('POST', '/custom') 93 | async custom(data: any, params: Params) { 94 | return { data, queryParams: params.query, routeParams: params.route }; 95 | } 96 | } 97 | 98 | app.use('some', new SomeService(options), { 99 | methods: ['find', 'custom'], 100 | events: [] 101 | }); 102 | ``` 103 | 104 | 105 | 106 | ## Example 107 | 108 | Check out the [example](/examples/custom_methods.md) to see it in action. 109 | 110 | ## Limitations 111 | 112 | - In Feathers 4 the custom methods are registered after the default methods of a service and therefore 113 | the /service/:id route will "win" over a /service/custom route for all methods but POST. 114 | - At least one default method has to be existent in a service to be registered 115 | -------------------------------------------------------------------------------- /docs/docsify/prism-lines.js: -------------------------------------------------------------------------------- 1 | function install (hook, vm) { 2 | const codeContainer = { 3 | counter: 0, 4 | codes: {} 5 | }; 6 | 7 | hook.beforeEach(function (content) { 8 | codeContainer.counter = 0; 9 | codeContainer.codes = {}; 10 | return content; 11 | }); 12 | 13 | hook.mounted(function () { 14 | if (vm.compiler._marked.Renderer.prototype.code) { 15 | const original = vm.compiler._marked.defaults.renderer.code; 16 | 17 | const code = (code, infostring, escaped) => { 18 | const infostringParts = infostring.split(' '); 19 | let options; 20 | 21 | if (infostringParts.length > 1) { 22 | const [lang, ...rest] = infostringParts; 23 | infostring = lang; 24 | try { 25 | options = JSON.parse(rest.join(' ')); 26 | } catch (e) { 27 | console.error('invalid code options'); 28 | } 29 | } 30 | 31 | const originalResult = original(code, infostring, escaped); 32 | 33 | if (typeof options === 'object') { 34 | let { lineNumbers, highlight } = options; 35 | 36 | if (typeof lineNumbers === 'boolean') { 37 | lineNumbers = { enabled: lineNumbers }; 38 | } else if (typeof lineNumbers === 'number') { 39 | lineNumbers = { enabled: true, start: lineNumbers }; 40 | } 41 | 42 | const additionalAttributes = []; 43 | if (typeof lineNumbers === 'object') { 44 | if (lineNumbers.enabled) { 45 | additionalAttributes.push(`class="language-${infostring} line-numbers"`); 46 | } 47 | if (lineNumbers.start) { 48 | additionalAttributes.push(`data-start="${lineNumbers.start}"`); 49 | } 50 | } 51 | 52 | if (typeof highlight === 'string') { 53 | highlight = { line: highlight }; 54 | } 55 | 56 | if (typeof highlight === 'object') { 57 | if (highlight.line) { 58 | additionalAttributes.push(`data-line="${highlight.line}"`); 59 | } 60 | if (highlight.lineOffset) { 61 | additionalAttributes.push(`data-line-offset="${highlight.lineOffset}"`); 62 | } 63 | } 64 | 65 | if (additionalAttributes.length) { 66 | codeContainer.codes[codeContainer.counter] = code; 67 | additionalAttributes.push(`data-code-counter="${codeContainer.counter}"`); 68 | codeContainer.counter += 1; 69 | 70 | return originalResult.replace(/^
 {
83 |       const env = {
84 |         code: codeContainer.codes[pre.dataset.codeCounter],
85 |         element: pre.querySelector(':scope > code')
86 |       };
87 | 
88 |       window.Prism.hooks.run('complete', env);
89 |     });
90 |   });
91 | }
92 | 
93 | window.$docsify.plugins = [].concat(install, window.$docsify.plugins);
94 | 


--------------------------------------------------------------------------------
/docs/docsify/replace.js:
--------------------------------------------------------------------------------
 1 | function install (hook) {
 2 |   let replacements;
 3 | 
 4 |   hook.mounted(function () {
 5 |     replacements = window.$docsify.replacements || [];
 6 |   });
 7 | 
 8 |   hook.beforeEach(function (content) {
 9 |     let modifiedContent = content;
10 |     replacements.forEach(({ search, replace }) => {
11 |       modifiedContent = modifiedContent.replace(search, replace);
12 |     });
13 |     return modifiedContent;
14 |   });
15 | }
16 | 
17 | window.$docsify.plugins = [].concat(install, window.$docsify.plugins);
18 | 


--------------------------------------------------------------------------------
/docs/examples/_sidebar.md:
--------------------------------------------------------------------------------
 1 | * [Home](/)
 2 | * [API](/api.md)
 3 | * [Custom Methods](/custom-methods.md)
 4 | * [Examples](/examples/index.md)
 5 | * * [Basic Example](/examples/basic.md)
 6 | * * [Adding docs to a generated service (v4)](/examples/generated_app_v4.md)
 7 | * * [SwaggerUI](/examples/ui.md)
 8 | * * [Prefixed Routes](/examples/prefixed_routes.md)
 9 | * * [Custom Methods](/examples/custom_methods.md)
10 | * * [Using Schemas of Feathersjs v5 (dove)](/examples/generated_service_v5.md)
11 | * * [Authentication with Feathersjs v5 (dove)](/examples/authentication_v5.md)
12 | 


--------------------------------------------------------------------------------
/docs/examples/authentication_v5.md:
--------------------------------------------------------------------------------
  1 | ### Example with feathers generated authentication on feathersjs v5 (dove) 
  2 | 
  3 | 1. When you have generated the app with authentication you have to add some things to the initial
  4 |    specs when registering feathers-swagger.
  5 | 
  6 |    ```typescript
  7 |    app.configure(
  8 |      swagger({
  9 |        specs: {
 10 |          info: {
 11 |            title: 'Feathers app with swagger with authentication',
 12 |            version: '1.0.0',
 13 |            description: '...',
 14 |          },
 15 |          components: {
 16 |            securitySchemes: {
 17 |              BearerAuth: {
 18 |                type: 'http',
 19 |                scheme: 'bearer',
 20 |              },
 21 |            },
 22 |          },
 23 |          security: [{ BearerAuth: [] }],
 24 |        },
 25 |        // other options ...
 26 |      }),
 27 |    );
 28 |    ```
 29 | 
 30 | 2. Add documentation to the authentication service (`src/authentication.ts`).
 31 |    This example shows local authentication.
 32 | 
 33 |    ```typescript {"highlight": "12-16, 21-71", "lineNumbers": true}
 34 |    import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication';
 35 |    import { LocalStrategy } from '@feathersjs/authentication-local';
 36 |    import type { Application } from './declarations';
 37 |    import { ServiceSwaggerOptions } from '../../feathers-swagger';
 38 |    
 39 |    declare module './declarations' {
 40 |      interface ServiceTypes {
 41 |        authentication: AuthenticationService;
 42 |      }
 43 |    }
 44 |    
 45 |    declare module '@feathersjs/authentication' {
 46 |      class AuthenticationService {
 47 |        docs: ServiceSwaggerOptions;
 48 |      }
 49 |    }
 50 |    
 51 |    export const authentication = (app: Application) => {
 52 |      const authentication = new AuthenticationService(app);
 53 |      
 54 |      authentication.docs = {
 55 |        idNames: {
 56 |          remove: 'accessToken',
 57 |        },
 58 |        idType: 'string',
 59 |        securities: ['remove', 'removeMulti'],
 60 |        multi: ['remove'],
 61 |        schemas: {
 62 |          authRequest: {
 63 |            type: 'object',
 64 |            properties: {
 65 |              strategy: { type: 'string' },
 66 |              email: { type: 'string' },
 67 |              password: { type: 'string' },
 68 |            },
 69 |          },
 70 |          authResult: {
 71 |            type: 'object',
 72 |            properties: {
 73 |              accessToken: { type: 'string' },
 74 |              authentication: {
 75 |                type: 'object',
 76 |                properties: {
 77 |                  strategy: { type: 'string' },
 78 |                },
 79 |              },
 80 |              payload: {
 81 |                type: 'object',
 82 |                properties: {}, // TODO
 83 |              },
 84 |              user: { $ref: '#/components/schemas/User' },
 85 |            },
 86 |          },
 87 |        },
 88 |        refs: {
 89 |          createRequest: 'authRequest',
 90 |          createResponse: 'authResult',
 91 |          removeResponse: 'authResult',
 92 |          removeMultiResponse: 'authResult',
 93 |        },
 94 |        operations: {
 95 |          remove: {
 96 |            description: 'Logout the currently logged in user',
 97 |            'parameters[0].description': 'accessToken of the currently logged in user',
 98 |          },
 99 |          removeMulti: {
100 |            description: 'Logout the currently logged in user',
101 |            parameters: [],
102 |          },
103 |        },
104 |      };
105 |        
106 |      authentication.register('jwt', new JWTStrategy());
107 |      authentication.register('local', new LocalStrategy());
108 |      
109 |      app.use('authentication', authentication);
110 |    };
111 |    ```
112 | 
113 | 3. Set the `security` option for `service.docs` like shown in
114 |    [Using Schemas of Feathersjs v5 (dove)](/examples/generated_service_v5.md) for methods that are protected. 
115 |    If all methods are protected `all` can be used.
116 | 
117 | 4. If you want to provide simple authentication usage on the SwaggerUI using Email/Username and Password,
118 |    you can use the [Swagger UI Plugin ApiKeyAuthForm](https://github.com/Mairu/swagger-ui-apikey-auth-form).
119 | 
120 |    Here is an example of an `openapi.ts` swagger configuration file, that can used with `api.configure();`
121 | 
122 |    ```typescript
123 |    import swagger from 'feathers-swagger';
124 |    import type { FnSwaggerUiGetInitializerScript } from 'feathers-swagger';
125 |    import type { Application } from './declarations';
126 |    
127 |    const getSwaggerInitializerScript: FnSwaggerUiGetInitializerScript = ({ docsJsonPath, ctx }) => {
128 |      const headers = ctx && ctx.headers;
129 |      const basePath = headers!['x-forwarded-prefix'] ?? '';
130 |    
131 |      // language=JavaScript
132 |      return `
133 |        window.onload = function () {
134 |          var script = document.createElement('script');
135 |          script.onload = function () {
136 |            window.ui = SwaggerUIBundle({
137 |              url: "${basePath}${docsJsonPath}",
138 |              dom_id: '#swagger-ui',
139 |              deepLinking: true,
140 |              presets: [
141 |                SwaggerUIBundle.presets.apis,
142 |                SwaggerUIStandalonePreset,
143 |                SwaggerUIApiKeyAuthFormPlugin,
144 |              ],
145 |              plugins: [
146 |                SwaggerUIBundle.plugins.DownloadUrl
147 |              ],
148 |              layout: "StandaloneLayout",
149 |              configs: {
150 |                apiKeyAuthFormPlugin: {
151 |                  forms: {
152 |                    BearerAuth: {
153 |                      fields: {
154 |                        email: {
155 |                          type: 'text',
156 |                          label: 'E-Mail-Address',
157 |                        },
158 |                        password: {
159 |                          type: 'password',
160 |                          label: 'Password',
161 |                        },
162 |                      },
163 |                      authCallback(values, callback) {
164 |                        window.ui.fn.fetch({
165 |                          url: '/authentication',
166 |                          method: 'post',
167 |                          headers: {
168 |                            Accept: 'application/json',
169 |                            'Content-Type': 'application/json'
170 |                          },
171 |                          body: JSON.stringify({
172 |                            strategy: 'local',
173 |                            ...values,
174 |                          }),
175 |                        }).then(function (response) {
176 |                          const json = JSON.parse(response.data);
177 |                          if (json.accessToken) {
178 |                            callback(null, json.accessToken);
179 |                          } else {
180 |                            callback('error while login');
181 |                          }
182 |                        }).catch(function (err) {
183 |                          console.log(err, Object.entries(err));
184 |                          callback('error while login');
185 |                        });
186 |                      },
187 |                    }
188 |                  },
189 |                  localStorage: {
190 |                    BearerAuth: {}
191 |                  }
192 |                }
193 |              }
194 |            });
195 |          };
196 |    
197 |          script.src = '//cdn.jsdelivr.net/npm/@mairu/swagger-ui-apikey-auth-form@1/dist/swagger-ui-apikey-auth-form.js';
198 |          document.head.appendChild(script)
199 |        };
200 |      `;
201 |    };
202 |    
203 |    export default (app: Application) => {
204 |      // If you don't use custom methods this line can be removed
205 |      app.configure(swagger.customMethodsHandler);
206 |    
207 |      app.configure(
208 |        swagger({
209 |          specs: {
210 |            info: {
211 |              title: 'Example with Authentication',
212 |              version: '1.0.0',
213 |              description: 'Example with Authentication and SwaggerUI ApiKeyAuthForm plugin',
214 |            },
215 |            components: {
216 |              securitySchemes: {
217 |                BearerAuth: {
218 |                  type: 'http',
219 |                  scheme: 'bearer',
220 |                },
221 |              },
222 |            },
223 |            security: [{ BearerAuth: [] }],
224 |          },
225 |          ui: swagger.swaggerUI({ getSwaggerInitializerScript }),
226 |        }),
227 |      );
228 |    };
229 |    ```
230 | 
231 |    Here is a preview together with a user service from [Using Schemas of Feathersjs v5 (dove)](/examples/generated_service_v5.md):
232 | 
233 | 
234 | [filename](../swagger-ui/index.html?url=../examples/authentication_v5_plugin.json ':include class=swui-preview')
235 | 


--------------------------------------------------------------------------------
/docs/examples/basic.md:
--------------------------------------------------------------------------------
 1 | ### Basic example 
 2 | 
 3 | Here's an example of a Feathers server that uses `feathers-swagger`.
 4 | 
 5 | > npm install @feathersjs/feathers @feathersjs/express feathers-memory feathers-swagger
 6 | 
 7 | ```js
 8 | const feathers = require('@feathersjs/feathers');
 9 | const express = require('@feathersjs/express');
10 | const memory = require('feathers-memory');
11 | const swagger = require('feathers-swagger');
12 | 
13 | const messageService = memory();
14 | 
15 | // swagger spec for this service, see http://swagger.io/specification/
16 | messageService.docs = {
17 |   description: 'A service to send and receive messages',
18 |   definitions: {
19 |     messages: {
20 |       "type": "object",
21 |       "required": [
22 |         "text"
23 |       ],
24 |       "properties": {
25 |         "text": {
26 |           "type": "string",
27 |           "description": "The message text"
28 |         },
29 |         "userId": {
30 |           "type": "string",
31 |           "description": "The id of the user that sent the message"
32 |         }
33 |       }
34 |     }
35 |   }
36 | };
37 | 
38 | const app = express(feathers())
39 |   .use(express.json())
40 |   .use(express.urlencoded({ extended: true }))
41 |   .configure(express.rest())
42 |   .configure(swagger({
43 |     specs: {
44 |       info: {
45 |         title: 'A test',
46 |         description: 'A description',
47 |         version: '1.0.0',
48 |       },
49 |     },
50 |   }))
51 |   .use('/messages', messageService);
52 | 
53 | app.listen(3030);
54 | ```
55 | 
56 | Visit  to see the Swagger JSON documentation.
57 | 


--------------------------------------------------------------------------------
/docs/examples/custom_methods.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "info": {
  3 |     "title": "A test",
  4 |     "description": "An example using custom methods",
  5 |     "version": "1.0.0"
  6 |   },
  7 |   "components": {
  8 |     "securitySchemes": {
  9 |       "BasicAuth": {
 10 |         "type": "http",
 11 |         "scheme": "basic"
 12 |       },
 13 |       "BearerAuth": {
 14 |         "type": "http",
 15 |         "scheme": "bearer"
 16 |       }
 17 |     },
 18 |     "schemas": {
 19 |       "custom_response": {
 20 |         "title": "Custom service response",
 21 |         "type": "object",
 22 |         "required": [
 23 |           "method"
 24 |         ],
 25 |         "properties": {
 26 |           "method": {
 27 |             "type": "string",
 28 |             "description": "name of the called method"
 29 |           },
 30 |           "data": {
 31 |             "type": "object",
 32 |             "description": "POST data provided to the service method"
 33 |           },
 34 |           "params": {
 35 |             "type": "object",
 36 |             "description": "params provided to the service method"
 37 |           },
 38 |           "id": {
 39 |             "type": "integer",
 40 |             "description": "id provided to the service method"
 41 |           }
 42 |         }
 43 |       },
 44 |       "custom_request": {
 45 |         "title": "Custom service requestBody",
 46 |         "type": "object",
 47 |         "required": [
 48 |           "method"
 49 |         ],
 50 |         "properties": {
 51 |           "string": {
 52 |             "type": "string",
 53 |             "description": "some string data"
 54 |           },
 55 |           "number": {
 56 |             "type": "number",
 57 |             "format": "int32",
 58 |             "description": "a number"
 59 |           }
 60 |         }
 61 |       }
 62 |     }
 63 |   },
 64 |   "security": [
 65 |     {
 66 |       "BearerAuth": []
 67 |     }
 68 |   ],
 69 |   "paths": {
 70 |     "/v3/custom-methods/{pathParamName}/service": {
 71 |       "get": {
 72 |         "parameters": [
 73 |           {
 74 |             "description": "A global path param",
 75 |             "in": "path",
 76 |             "name": "pathParamName",
 77 |             "schema": {
 78 |               "type": "string"
 79 |             },
 80 |             "required": true
 81 |           },
 82 |           {
 83 |             "description": "A defined param",
 84 |             "in": "query",
 85 |             "name": "test",
 86 |             "schema": {
 87 |               "type": "string"
 88 |             }
 89 |           }
 90 |         ],
 91 |         "responses": {
 92 |           "200": {
 93 |             "description": "Use find to do a normal GET",
 94 |             "content": {
 95 |               "application/json": {
 96 |                 "schema": {
 97 |                   "$ref": "#/components/schemas/custom_response"
 98 |                 }
 99 |               }
100 |             }
101 |           },
102 |           "401": {
103 |             "description": "not authenticated"
104 |           },
105 |           "500": {
106 |             "description": "general error"
107 |           }
108 |         },
109 |         "description": "Do something with a GET method",
110 |         "summary": "",
111 |         "tags": [
112 |           "custom method service"
113 |         ],
114 |         "security": []
115 |       }
116 |     },
117 |     "/v3/custom-methods/{pathParamName}/service/do-post": {
118 |       "post": {
119 |         "parameters": [
120 |           {
121 |             "description": "A global path param",
122 |             "in": "path",
123 |             "name": "pathParamName",
124 |             "schema": {
125 |               "type": "string"
126 |             },
127 |             "required": true
128 |           },
129 |           {
130 |             "description": "A defined param",
131 |             "in": "query",
132 |             "name": "test",
133 |             "schema": {
134 |               "type": "string"
135 |             }
136 |           }
137 |         ],
138 |         "responses": {
139 |           "200": {
140 |             "description": "success",
141 |             "content": {
142 |               "application/json": {
143 |                 "schema": {
144 |                   "$ref": "#/components/schemas/custom_response"
145 |                 }
146 |               }
147 |             }
148 |           },
149 |           "401": {
150 |             "description": "not authenticated"
151 |           },
152 |           "500": {
153 |             "description": "general error"
154 |           }
155 |         },
156 |         "description": "A custom POST method",
157 |         "summary": "",
158 |         "tags": [
159 |           "custom method service"
160 |         ],
161 |         "security": [
162 |           {
163 |             "BearerAuth": []
164 |           }
165 |         ],
166 |         "requestBody": {
167 |           "required": true,
168 |           "content": {
169 |             "application/json": {
170 |               "schema": {
171 |                 "$ref": "#/components/schemas/custom_request"
172 |               }
173 |             }
174 |           }
175 |         }
176 |       }
177 |     },
178 |     "/v3/custom-methods/{pathParamName}/service/do-patch-with/{id}": {
179 |       "put": {
180 |         "parameters": [
181 |           {
182 |             "description": "A global path param",
183 |             "in": "path",
184 |             "name": "pathParamName",
185 |             "schema": {
186 |               "type": "string"
187 |             },
188 |             "required": true
189 |           },
190 |           {
191 |             "in": "path",
192 |             "name": "id",
193 |             "description": "ID of service",
194 |             "schema": {
195 |               "type": "integer"
196 |             },
197 |             "required": true
198 |           }
199 |         ],
200 |         "responses": {
201 |           "200": {
202 |             "description": "success",
203 |             "content": {
204 |               "application/json": {
205 |                 "schema": {
206 |                   "$ref": "#/components/schemas/custom_response"
207 |                 }
208 |               }
209 |             }
210 |           },
211 |           "401": {
212 |             "description": "not authenticated"
213 |           },
214 |           "500": {
215 |             "description": "general error"
216 |           }
217 |         },
218 |         "description": "A custom customPatchWithId method.",
219 |         "summary": "",
220 |         "tags": [
221 |           "custom method service"
222 |         ],
223 |         "security": [],
224 |         "requestBody": {
225 |           "required": true,
226 |           "content": {
227 |             "application/json": {
228 |               "schema": {
229 |                 "$ref": "#/components/schemas/custom_request"
230 |               }
231 |             }
232 |           }
233 |         }
234 |       }
235 |     },
236 |     "/v3/custom-methods/{pathParamName}/service/do-patch-with/{customId}/{customId2}": {
237 |       "get": {
238 |         "parameters": [
239 |           {
240 |             "description": "A global path param",
241 |             "in": "path",
242 |             "name": "pathParamName",
243 |             "schema": {
244 |               "type": "string"
245 |             },
246 |             "required": true
247 |           },
248 |           {
249 |             "description": "Custom integer path id",
250 |             "in": "path",
251 |             "required": true,
252 |             "name": "customId",
253 |             "schema": {
254 |               "type": "integer"
255 |             }
256 |           },
257 |           {
258 |             "description": "Custom string path id",
259 |             "in": "path",
260 |             "required": true,
261 |             "name": "customId2",
262 |             "schema": {
263 |               "type": "string"
264 |             }
265 |           }
266 |         ],
267 |         "responses": {
268 |           "200": {
269 |             "description": "success"
270 |           },
271 |           "401": {
272 |             "description": "not authenticated"
273 |           },
274 |           "500": {
275 |             "description": "general error"
276 |           }
277 |         },
278 |         "description": "A custom GET method",
279 |         "summary": "",
280 |         "tags": [
281 |           "custom method service"
282 |         ],
283 |         "security": []
284 |       }
285 |     }
286 |   },
287 |   "openapi": "3.0.3",
288 |   "tags": [
289 |     {
290 |       "name": "custom method service",
291 |       "description": "A service with custom methods"
292 |     }
293 |   ]
294 | }
295 | 


--------------------------------------------------------------------------------
/docs/examples/custom_methods.md:
--------------------------------------------------------------------------------
1 | ## Custom Methods 
2 | 
3 | ### Code (from example app) 
4 | [filename](https://raw.githubusercontent.com/feathersjs-ecosystem/feathers-swagger/{GITHUB_BRANCH}/example/openapi-v3/customMethods.js ':include')
5 | 
6 | ### Preview in SwaggerUI 
7 | 
8 | [filename](../swagger-ui/index.html?url=../examples/custom_methods.json ':include class=swui-preview')
9 | 


--------------------------------------------------------------------------------
/docs/examples/generated_app_v4.md:
--------------------------------------------------------------------------------
 1 | ### Example with feathers generated app 
 2 | 
 3 | 1. Go into your `src/services/` folder, and open the service you want to edit `PATH.service.js`
 4 | 2. Change from this:
 5 | ```js
 6 | // Initialize our service with any options it requires
 7 | app.use('/events', createService(options));
 8 | ```
 9 | to this:
10 | ```js
11 | const events = createService(options);
12 | events.docs = {
13 |   //overwrite things here.
14 |   //if we want to add a mongoose style $search hook to find, we can write this:
15 |   operations: {
16 |     find: {
17 |       'parameters[]': {
18 |         description: 'Property to query results',
19 |         in: 'query',
20 |         name: '$search',
21 |         type: 'string'
22 |       },
23 |     },
24 |   },
25 |   //if we want to add the mongoose model to the 'definitions' so it is a named model in the swagger ui:
26 |   definitions: {
27 |     event: mongooseToJsonLibraryYouImport(Model), //import your own library, use the 'Model' object in this file.
28 |     'event_list': { //this library currently configures the return documentation to look for ``${tag} list`
29 |        type: 'array',
30 |        items: { $ref: '#/definitions/event' }
31 |      }
32 |    }
33 | };
34 | app.use('/events', events);
35 | ```
36 | 
37 | The overrides work at a property level - if you pass in `find.parameters`, that whole object will be used, it is not merged in.
38 | If you want to update only parts there is [support of path keys to update specific nested structures](/api.md#path-support-to-update-nested-structures).
39 | You can find more information in the utils.js file to get an idea of what is passed in.
40 | 


--------------------------------------------------------------------------------
/docs/examples/generated_service_v5.md:
--------------------------------------------------------------------------------
 1 | ### Example with feathers generated service based on feathersjs v5 (dove) 
 2 | 
 3 | 1. Go into your `src/services/{$name}` folder, and open the service you want to edit `${name}.[tj]s`
 4 | 2. Import the helper function and import the schemas (example for user service):
 5 | ```js  {"highlight": "11-14", "lineNumbers": true}
 6 |   import { createSwaggerServiceOptions } from 'feathers-swagger';
 7 |   import {
 8 |     userDataValidator,
 9 |     userPatchValidator,
10 |     userQueryValidator,
11 |     userResolver,
12 |     userExternalResolver,
13 |     userDataResolver,
14 |     userPatchResolver,
15 |     userQueryResolver,
16 |     userSchema,
17 |     userDataSchema,
18 |     userPatchSchema,
19 |     userQuerySchema
20 |   } from './users.schema';
21 | ```
22 | adjust the options when the service is generated
23 | ```js {"highlight": "6-12", "lineNumbers": true}
24 |   app.use('users', new UserService(getOptions(app)), {
25 |       // A list of all methods this service exposes externally
26 |       methods: userMethods,
27 |       // You can add additional custom events to be sent to clients here
28 |       events: [],
29 |       docs: createSwaggerServiceOptions({
30 |           schemas: { userSchema, userDataSchema, userPatchSchema, userQuerySchema },
31 |           docs: {
32 |               // any options for service.docs can be added here
33 |               securities: ['find', 'get', 'patch', 'remove'],
34 |           }
35 |       }),
36 |   });
37 | ```
38 | 
39 | If you are using Typescript don't forget to add the docs property to the ServiceOptions interface
40 | which is described on the
41 | [Getting Started - Configure the documentation for a feathers service](/#/?id=configure-the-documentation-for-a-feathers-service)  
42 | 


--------------------------------------------------------------------------------
/docs/examples/index.md:
--------------------------------------------------------------------------------
 1 | ## Examples 
 2 | 
 3 | In addition to the examples you can access from the navigation, you can check out the [example application](https://github.com/feathersjs-ecosystem/feathers-swagger/tree/{GITHUB_BRANCH}/example) of the git repository.
 4 | It contains many configurations and can be start with `npm start` when feathers-swagger have been checked out.
 5 | 
 6 | There is also a [repository with examples of integrations of feathers-swagger](https://github.com/Mairu/feathersjs-swagger-tests). It contains multiple branches for different versions and configurations.  
 7 | 
 8 | ### List of examples 
 9 | * [Basic Example](/examples/basic.md)
10 | * [Adding docs to a generated service (v4)](/examples/generated_app_v4.md)
11 | * [SwaggerUI](/examples/ui.md)
12 | * [Prefixed Routes](/examples/prefixed_routes.md)
13 | * [Custom Methods](/examples/custom_methods.md)
14 | * [Using Schemas of Feathersjs v5 (dove)](/examples/generated_service_v5.md)
15 | * [Authentication with Feathersjs v5 (dove)](/examples/authentication_v5.md)
16 | 


--------------------------------------------------------------------------------
/docs/examples/prefixed_routes.md:
--------------------------------------------------------------------------------
 1 | ### Prefixed routes 
 2 | 
 3 | If you are using versioned or prefixed routes for your API like `/api//users`, you can configure it using the
 4 | `prefix` property so all your services don't end up in the same group. The value of the `prefix` property can be either
 5 | a string or a RegEx.
 6 | 
 7 | ```js
 8 | const app = express(feathers())
 9 |   // ... configure app
10 |   .configure(swagger({
11 |     prefix: /api\/v\d\//,
12 |     docsPath: '/docs',
13 |     specs: {
14 |       info: {
15 |         title: 'A test',
16 |         description: 'A description',
17 |         version: '1.0.0',
18 |       },
19 |     },
20 |   }))
21 |   .use('/api/v1/messages', messageService);
22 | 
23 | app.listen(3030);
24 | ```
25 | 
26 | To display your API version alongside the service name, you can also define a `versionPrefix` to be extracted:
27 | ```js
28 | const app = express(feathers())
29 |   // ... configure app
30 |   .configure(swagger({
31 |     prefix: /api\/v\d\//,
32 |     versionPrefix: /v\d/,
33 |     specs: {
34 |       info: {
35 |         title: 'A test',
36 |         description: 'A description',
37 |         version: '1.0.0',
38 |       },
39 |     },
40 |   }))
41 |   .use('/api/v1/messages', messageService);
42 | 
43 | app.listen(3030);
44 | ```
45 | 


--------------------------------------------------------------------------------
/docs/examples/ui.md:
--------------------------------------------------------------------------------
 1 | ### Example with UI 
 2 | 
 3 | The `ui` option allows to set up a UI for visualizing the documentation.
 4 | Feather-swagger provides direct support for the usage of [Swagger UI](http://swagger.io/swagger-ui/) which will host the UI at `docsPath`.
 5 | The `sagger-ui-dist` package has to be installed manually.
 6 | 
 7 | ```js
 8 | const path = require('path');
 9 | const feathers = require('@feathersjs/feathers');
10 | const express = require('@feathersjs/express');
11 | const memory = require('feathers-memory');
12 | const swagger = require('feathers-swagger');
13 | 
14 | const messageService = memory();
15 | 
16 | messageService.docs = {
17 |   description: 'A service to send and receive messages',
18 |   definitions: {
19 |     messages: {
20 |       "type": "object",
21 |       "required": [
22 |         "text"
23 |       ],
24 |       "properties": {
25 |         "text": {
26 |           "type": "string",
27 |           "description": "The message text"
28 |         },
29 |         "useId": {
30 |           "type": "string",
31 |           "description": "The id of the user that send the message"
32 |         }
33 |       }
34 |     }
35 |   }
36 | };
37 | 
38 | const app = express(feathers())
39 |   .use(express.json())
40 |   .use(express.urlencoded({ extended: true }))
41 |   .configure(express.rest())
42 |   .configure(swagger({
43 |     ui: swagger.swaggerUI({ docsPath: '/docs' }),
44 |     specs: {
45 |       info: {
46 |         title: 'A test',
47 |         description: 'A description',
48 |         version: '1.0.0',
49 |       },
50 |     },
51 |   }))
52 |   .use('/messages', messageService);
53 | 
54 | app.listen(3030);
55 | ```
56 | 
57 | Now  will show the documentation in the browser using the Swagger UI.
58 | 


--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
  1 | # feathers-swagger
  2 | 
  3 | [![CI](https://github.com/feathersjs-ecosystem/feathers-swagger/actions/workflows/ci.yaml/badge.svg)](https://github.com/feathersjs-ecosystem/feathers-swagger/actions/workflows/ci.yaml)
  4 | [![Download Status](https://img.shields.io/npm/dm/feathers-swagger.svg?style=flat-square)](https://www.npmjs.com/package/feathers-swagger)
  5 | 
  6 | Add [OpenAPI](https://swagger.io/resources/open-api/) documentation to your Feathers services and optionally show them in the [Swagger UI](https://swagger.io/tools/swagger-ui/).
  7 | 
  8 | You can also add custom methods to feathers services that will be available as rest routes but not via feathers client. 
  9 | 
 10 | This version is prepared to work with Swagger UI >= 4.9
 11 | 
 12 | ## Installation
 13 | 
 14 | 
 15 | 
 16 | #### **Default (express)**
 17 | 
 18 | ```shell
 19 | npm install feathers-swagger swagger-ui-dist
 20 | ```
 21 | 
 22 | #### **Koa**
 23 | 
 24 | ```shell
 25 | npm install feathers-swagger swagger-ui-dist koa-mount koa-static
 26 | ```
 27 | 
 28 | #### **Koa with custom methods**
 29 | 
 30 | ```shell
 31 | npm install feathers-swagger swagger-ui-dist @koa/router koa-mount koa-static
 32 | ```
 33 | 
 34 | #### **Minimum**
 35 | 
 36 | ```shell
 37 | npm install feathers-swagger
 38 | ```
 39 | 
 40 | 
 41 | 
 42 | ## Usage
 43 | 
 44 | ### Initialize feathers swagger
 45 | 
 46 | It has to initialized before the services are registered.
 47 | Only for services registered after feather-swagger documentation will be generated.
 48 | There are many options, please check out the [API documentation](api.md#swaggeroptions) to get more details.
 49 | 
 50 | 
 51 | 
 52 | #### **Simple**
 53 | 
 54 | ```js
 55 | // ... imports
 56 | const swagger = require('feathers-swagger');
 57 | 
 58 | const app = express(feathers())
 59 |   // ...
 60 |   .configure(swagger({
 61 |     specs: {
 62 |       info: {
 63 |         title: 'A test',
 64 |         description: 'A description',
 65 |         version: '1.0.0',
 66 |       },
 67 |     },
 68 |   }))
 69 |   // later services can be registered
 70 | ```
 71 | 
 72 | #### **With SwaggerUI**
 73 | 
 74 | To enable the usage of SwaggerUI the `ui` option has to be set with the result of `swagger.swaggerUI()`. Checkout the [available options](api.md#swaggerswaggeruioptions) for `swaggerUI`. 
 75 | 
 76 | ```js
 77 | // ... imports
 78 | const swagger = require('feathers-swagger');
 79 | 
 80 | const app = express(feathers())
 81 |   // ...
 82 |   .configure(swagger({
 83 |     specs: {
 84 |       info: {
 85 |         title: 'A test',
 86 |         description: 'A description',
 87 |         version: '1.0.0',
 88 |       },
 89 |     },
 90 |     ui: swagger.swaggerUI(),
 91 |   }))
 92 |   // later services can be registered
 93 | ```
 94 | 
 95 | #### **Custom Methods support with feathers 5**
 96 | 
 97 | To support the usage of custom methods in feathers 5 the `customMethodsHandler` middleware has to be registered before the rest middleware of express or koa.
 98 | 
 99 | ```js
100 | // ... imports
101 | const swagger = require('feathers-swagger');
102 | 
103 | const app = express(feathers())
104 |   // ...
105 |   .configure(swagger.customMethodsHandler)
106 |   .configure(express.rest()) // or rest() of koa
107 |   .configure(swagger({
108 |     specs: {
109 |       info: {
110 |         title: 'A test',
111 |         description: 'A description',
112 |         version: '1.0.0',
113 |       },
114 |     },
115 |   }))
116 |   // later services can be registered
117 | ```
118 | 
119 | 
120 | 
121 | ### Configure the documentation for a feathers service
122 | 
123 | To customize the documentation of feathers service a docs property has to be added. This can be done as property before the service is registered or as service path option.
124 | There are many options please check out the [examples](examples/index.md) and the [API documentation](api.md#servicedocs) to get more details.
125 | 
126 | 
127 | 
128 | #### **Service path option (TS)**
129 | The docs property can be set as [path service options](https://dove.feathersjs.com/api/application.html#use-path-service-options) introduced with feathers dove. 
130 | 
131 | ```typescript
132 | import type { createSwaggerServiceOptions } from 'feathers-swagger';
133 | import {
134 |   // ...
135 |   messageSchema,
136 |   messageDataSchema,
137 |   messagePatchSchema,
138 |   messageQuerySchema
139 | } from './messages.schema';
140 | 
141 | // ...
142 | 
143 | app.use('message', new MessageService(), {
144 |   methods: messageMethods,
145 |   events: [],
146 |   docs: createSwaggerServiceOptions({
147 |     schemas: { messageDataSchema, messageQuerySchema, messageSchema },
148 |     docs: { 
149 |       description: 'My custom service description',
150 |       securities: ['all'],
151 |     }
152 |   })
153 | });
154 | ```
155 | 
156 | To be able to set the docs property on the service options, the interface has to be adjusted in the `declarations.ts` of the project.
157 | 
158 | ```typescript
159 | import { ServiceSwaggerOptions } from 'feathers-swagger';
160 | 
161 | // ...
162 | 
163 | declare module '@feathersjs/feathers' {
164 |   interface ServiceOptions {
165 |     docs?: ServiceSwaggerOptions;
166 |   }
167 | }
168 | ```
169 | 
170 | #### **Simple**
171 | The docs property can be set as property of a service instance before it is registered.
172 | 
173 | 
174 | ```js
175 | // definition of service
176 | service.docs = {
177 |   description: 'A description for the service',
178 |   schema: { /* definition of the openapi schema for the service */ }
179 | };
180 | app.use('pathForService', service); // service is registered after docs property has been set
181 | ```
182 | 
183 | #### **Class Property (TS)**
184 | The docs property can also be directly set as property of a service class that later is registered.
185 | 
186 | ```typescript
187 | import type { ServiceSwaggerOptions } from 'feathers-swagger';
188 | 
189 | // ...
190 | 
191 | class SomeService extends AnyAdapter {
192 |   docs: ServiceSwaggerOptions = {
193 |     description: 'Description of the service'
194 |     // ...
195 |   };
196 | }
197 | ```
198 | 
199 | 
200 | 
201 | ## Model Schemas
202 | 
203 | Please note that feathers-swagger does not generate model schemas, so you might
204 | encounter errors similar to the one below when initially using the Swagger UI.
205 | 
206 | > Could not resolve reference: Could not resolve pointer: /components/schemas/ does not exist in document
207 | 
208 | To resolve, either manually define your model schemas or consider automated alternatives like:
209 | 
210 | - [sequelize-to-json-schemas](https://github.com/alt3/sequelize-to-json-schemas)
211 | 
212 | ## Migration
213 | 
214 | * [Migrations for version 1](migrations/MIGRATIONS_v1.md)
215 | * [Migrations for version 2](migrations/MIGRATIONS_v2.md)
216 | * [Migrations for version 3](migrations/MIGRATIONS_v3.md)
217 | 
218 | ## License
219 | 
220 | Copyright (c) 2016 - 2023
221 | 
222 | Licensed under the [MIT license](LICENSE).
223 | 


--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 5 |   Feathers Swagger Documentation
 6 |   
 7 |   
 8 |   
 9 |   
10 |   
11 |   
12 |   
13 |   
14 |   
43 | 
44 | 
45 |   
46 |   
47 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /docs/migrations/MIGRATIONS_v1.md: -------------------------------------------------------------------------------- 1 | ## Migrations for Version 1 2 | 3 | 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. 4 | 5 | ### Introduction of specs option 6 | 7 | To not mix up options and specification as before all specifications go into the new specs option. 8 | 9 | #### Before 10 | ```js 11 | swagger({ 12 | prefix: /api\/v\d\//, 13 | versionPrefix: /v\d/, 14 | docsPath: '/docs', 15 | info: { 16 | title: 'A test', 17 | description: 'A description', 18 | version: '1.0.0' 19 | }, 20 | definitions: { 21 | // ... 22 | }, 23 | }) 24 | ``` 25 | 26 | #### After 27 | ```js 28 | swagger({ 29 | prefix: /api\/v\d\//, 30 | versionPrefix: /v\d/, 31 | docsPath: '/docs', 32 | specs: { 33 | info: { 34 | title: 'A test', 35 | description: 'A description', 36 | version: '1.0.0' 37 | }, 38 | definitions: { 39 | // ... 40 | }, 41 | }, 42 | }) 43 | 44 | ``` 45 | ### Introduction of operations option of the service.doc config 46 | 47 | With introduction of custom methods support it made more sense (also because of TypeScript) to nest operations specifications. 48 | 49 | #### Before 50 | ```js 51 | messageService.docs = { 52 | find: { 53 | description: 'My description', 54 | }, 55 | }; 56 | ``` 57 | 58 | #### After 59 | ```js 60 | messageService.docs = { 61 | operations: { 62 | find: { 63 | description: 'My description', 64 | }, 65 | }, 66 | }; 67 | ``` 68 | 69 | ### Remove of findQueryParameters option 70 | 71 | This option was very specific to add (prepend) parameters to all find operations. 72 | With the introduced option to customize defaults you can also add more default find parameters. 73 | 74 | #### Before 75 | ```js 76 | swagger({ 77 | // ... 78 | findQueryParameters: [ 79 | { 80 | description: 'My custom query parameter', 81 | in: 'query', 82 | name: '$custom', 83 | type: 'string' 84 | }, 85 | ], 86 | }) 87 | ``` 88 | 89 | #### After 90 | ```js 91 | swagger({ 92 | // ... 93 | defaults: { 94 | operations: { 95 | find: { 96 | 'parameters[-]': { 97 | description: 'My custom query parameter', 98 | in: 'query', 99 | name: '$custom', 100 | type: 'string' 101 | } 102 | } 103 | } 104 | } 105 | }) 106 | ``` 107 | 108 | ### Generate RFC3986-compliant percent-encoded URIs 109 | 110 | To generate valid specifications there may not be spaces in $ref links. 111 | Therefore concatenation is done with _ by default now. This applies to list and version refs of the previous version. 112 | 113 | #### Before 114 | ```js 115 | messageService.docs = { 116 | definitions: { 117 | message: { /* definition */ }, 118 | 'message list': { 119 | type: 'array', 120 | items: { $ref: '#/definitions/message' } 121 | } 122 | } 123 | }; 124 | // Versioned service 125 | messageV1Service.docs = { 126 | definitions: { 127 | 'message v1': { /* definition */ }, 128 | 'message v1 list': { 129 | type: 'array', 130 | items: { $ref: '#/definitions/message v1' } 131 | } 132 | } 133 | }; 134 | ``` 135 | 136 | #### After 137 | ```js 138 | messageService.docs = { 139 | definitions: { 140 | message: { /* definition */ }, 141 | message_list: { 142 | type: 'array', 143 | items: { $ref: '#/definitions/message' } 144 | } 145 | } 146 | }; 147 | // Versioned service 148 | messageV1Service.docs = { 149 | definitions: { 150 | message_v1: { /* definition */ }, 151 | message_v1_list: { 152 | type: 'array', 153 | items: { $ref: '#/definitions/message_v1' } 154 | } 155 | } 156 | }; 157 | ``` 158 | 159 | ### Removal of sequelize related utils 160 | 161 | Because sequelize is not in the scope of this package the utils getType, getFormat, property and definition were removed. 162 | If you used them extract them from an old version or use other packages which provide this functionality. 163 | 164 | #### Before 165 | ```js 166 | const { definition } = 'feathers-swagger'; 167 | 168 | messageService.docs = { 169 | definition: definition(Model), 170 | }; 171 | ``` 172 | 173 | #### After 174 | ```js 175 | const sequelizeJsonSchema = require('sequelize-json-schema'); 176 | 177 | messageService.docs = { 178 | definition: sequelizeJsonSchema(Model), 179 | }; 180 | ``` 181 | 182 | ### Overwriting of already defined tags specifications is now opt-in 183 | 184 | Introduced with [PR: Fix: docs ignored when path already exists \#69](https://github.com/feathersjs-ecosystem/feathers-swagger/pull/69) 185 | the last registered service will always overwrite previously defined tags. To be able to handle it by config 186 | the `overwriteTagSpec` was introduced. It defaults to false, which is a breaking change. 187 | 188 | #### Before 189 | ```js 190 | // ... 191 | app.use('/projects/:projectId/sync', projectsSyncService); 192 | // use tag from second service 193 | const docs = { description: 'My Project Service' }; 194 | app.use('/projects', Object.assign(service(options), { docs }) ); 195 | ``` 196 | 197 | #### After 198 | ```js 199 | // ... 200 | app.use('/projects/:projectId/sync', projectsSyncService); 201 | // use tag from second service 202 | const docs = { description: 'My Project Service', overwriteTagSpec: true }; 203 | app.use('/projects', Object.assign(service(options), { docs }) ); 204 | ``` 205 | -------------------------------------------------------------------------------- /docs/migrations/MIGRATIONS_v2.md: -------------------------------------------------------------------------------- 1 | ## Migrations for Version 2 (from 1) 2 | 3 | Version 2.0.0 introduces some breaking changes to previous 1.x.x versions. These changes and ways to migrate to the new release will be described here. 4 | 5 | ### Removing swagger-ui-dist dependency and the uiIndex option and replaces it by the ui option 6 | 7 | The direct dependency to **swagger-ui-dist** was removed, so the installation and versioning is not in the hand of the library anymore. 8 | The **uiIndex** option has been removed and is being replaced by the new **ui** option. 9 | The **ui** option takes a function that gets 2 parameters -> (app, {docsJsonPath, specs, openApiVersion}) and should initialize the UI. 10 | An initializer function (creator) for Swagger UI is included with feathers-swagger. 11 | 12 | **_Hint_:** Now it is simple to use another UI for OpenAPI documentation (like [Redoc](https://github.com/Redocly/redoc) or [RapiDoc](https://github.com/rapi-doc/RapiDoc)) by using your own ui initializer function. 13 | 14 | #### Before 15 | ```js 16 | swagger({ 17 | docsPath: '/docs', 18 | info: { 19 | title: 'A test', 20 | description: 'A description', 21 | version: '1.0.0' 22 | }, 23 | specs: { 24 | info: { 25 | title: 'A test', 26 | description: 'A description', 27 | version: '1.0.0' 28 | }, 29 | schemas: { 30 | // ... 31 | }, 32 | }, 33 | }) 34 | ``` 35 | 36 | #### After 37 | ```js 38 | swagger({ 39 | ui: swagger.swaggerUI({ docsPath: '/docs' }), 40 | specs: { 41 | info: { 42 | title: 'A test', 43 | description: 'A description', 44 | version: '1.0.0' 45 | }, 46 | schemas: { 47 | // ... 48 | }, 49 | }, 50 | }) 51 | ``` 52 | 53 | ### docsPath option is removed and a default value for docsJsonPath has been added 54 | 55 | As the uiIndex has been removed and the initialization of the ui has been extracted (to the swaggerUI initializer), the docsPath option has been removed / moved to the swaggerUI initializer. 56 | Therefore the docsJsonPath now has the default value of `/swagger.json`. 57 | 58 | ### Default of openApiVersion option was changed from 2 to 3 59 | 60 | As the OpenApi 3 standard has more features and is the de facto standard nowadays it is now the default. 61 | 62 | #### Before 63 | ```js 64 | swagger({ 65 | // no openApiVersion option 66 | specs: { 67 | info: { 68 | title: 'A test', 69 | description: 'A description', 70 | version: '1.0.0' 71 | }, 72 | definitions: { 73 | // ... 74 | }, 75 | }, 76 | }) 77 | ``` 78 | 79 | #### After 80 | ```js 81 | swagger({ 82 | openApiVersion: 2, 83 | specs: { 84 | info: { 85 | title: 'A test', 86 | description: 'A description', 87 | version: '1.0.0' 88 | }, 89 | definitions: { 90 | // ... 91 | }, 92 | }, 93 | }) 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/migrations/MIGRATIONS_v3.md: -------------------------------------------------------------------------------- 1 | ## Migrations for Version 3 (from 2) 2 | 3 | Version 3.0.0 introduces some breaking changes to previous 2.x.x versions. These changes and ways to migrate to the new release will be described here. 4 | 5 | ### Adjust the default schema / definition name of generated lists and pagination schemas 6 | 7 | #### Before 8 | Generated names were `${model}_list` and `${model}_pagination` and could not be adjusted. 9 | 10 | #### After 11 | Generated names will by default be `${model}List` and `${model}Pagination` but can be adjusted. 12 | 13 | To get the old behavior you can override the schemaNames generation (on service level) or as defaults. 14 | ```js 15 | swagger({ 16 | specs: { 17 | info: { 18 | title: 'A test', 19 | description: 'A description', 20 | version: '1.0.0' 21 | }, 22 | defaults: { 23 | schemaNames: { 24 | list: name => `${name}_list`, 25 | pagination: name => `${name}_pagination`, 26 | } 27 | } 28 | }, 29 | }) 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/swagger-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swagger UI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/swagger-ui/swagger-initializer.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | const searchParams = new URLSearchParams(window.location.search); 3 | 4 | let url = 'https://petstore3.swagger.io/api/v3/openapi.json'; 5 | if (searchParams.has('url')) { 6 | url = searchParams.get('url'); 7 | } 8 | 9 | /* global SwaggerUIBundle, SwaggerUIStandalonePreset */ 10 | window.ui = SwaggerUIBundle({ 11 | url, 12 | dom_id: '#swagger-ui', 13 | deepLinking: true, 14 | presets: [ 15 | SwaggerUIBundle.presets.apis, 16 | SwaggerUIStandalonePreset 17 | ], 18 | plugins: [ 19 | SwaggerUIBundle.plugins.DownloadUrl 20 | ], 21 | layout: 'StandaloneLayout' 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | const feathers = require('@feathersjs/feathers'); 2 | const express = require('@feathersjs/express'); 3 | const path = require('path'); 4 | const swagger = require('../lib'); 5 | 6 | const serveStatic = require('serve-static'); 7 | const distPath = require.resolve('swagger-ui-dist'); 8 | 9 | const swaggerV2Definitions = require('./swagger-v2/definitions'); 10 | const swaggerV2DefinitionWithCustomizedSpec = require('./swagger-v2/definitionWithCustomizedSpec'); 11 | const swaggerV2CustomTags = require('./swagger-v2/customTags'); 12 | const swaggerV2Security = require('./swagger-v2/security'); 13 | const swaggerV2CustomMethods = require('./swagger-v2/customMethods'); 14 | const openApiV2Multi = require('./swagger-v2/multi'); 15 | 16 | const openApiV3Definitions = require('./openapi-v3/definitions'); 17 | const openApiV3DefinitionWithCustomizedSpec = require('./openapi-v3/definitionWithCustomizedSpec'); 18 | const openApiV3CustomTags = require('./openapi-v3/customTags'); 19 | const openApiV3Security = require('./openapi-v3/security'); 20 | const openApiV3CustomMethods = require('./openapi-v3/customMethods'); 21 | const openApiV3Multi = require('./openapi-v3/multi'); 22 | const openApiV3IdNames = require('./openapi-v3/idNames'); 23 | const openApiV3DoveSchemas = require('./openapi-v3/doveSchemas'); 24 | 25 | const app = express(feathers()) 26 | .use(express.json()) 27 | .use(express.urlencoded({ 28 | extended: true 29 | })) 30 | .use(serveStatic(distPath)) 31 | .configure(swagger.customMethodsHandler) 32 | .configure(express.rest()) 33 | 34 | .get('/', (req, res) => { 35 | res.sendFile(path.join(__dirname, 'index.html')); 36 | }) 37 | .get('/docs', (req, res) => { 38 | res.sendFile(path.join(__dirname, 'docs.html')); 39 | }) 40 | .use(serveStatic(path.dirname(require.resolve('swagger-ui-dist')))) 41 | 42 | .configure(swaggerV2Definitions) 43 | .configure(swaggerV2DefinitionWithCustomizedSpec) 44 | .configure(swaggerV2CustomTags) 45 | .configure(swaggerV2Security) 46 | .configure(swaggerV2CustomMethods) 47 | .configure(openApiV2Multi) 48 | 49 | .configure(openApiV3Definitions) 50 | .configure(openApiV3DefinitionWithCustomizedSpec) 51 | .configure(openApiV3CustomTags) 52 | .configure(openApiV3Security) 53 | .configure(openApiV3CustomMethods) 54 | .configure(openApiV3Multi) 55 | .configure(openApiV3IdNames) 56 | .configure(openApiV3DoveSchemas) 57 | 58 | ; 59 | 60 | const port = process.env.PORT || 3030; 61 | console.log(`Simple app with multiple feathers-swagger examples running on http://localhost:${port}/`); 62 | 63 | app.listen(port); 64 | -------------------------------------------------------------------------------- /example/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 | 71 | 72 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Feathers Swagger Examples 6 | 7 | 8 | 9 |

Different Examples using feathers-swagger

10 | 11 | Example of multiple combined specifications 12 | 13 |

Swagger v2 / OpenApi 2.0

14 | 22 |

OpenApi 3.0

23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/openapi-v3/customMethods.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for openapi v3 3 | * - using parameters in path to service 4 | * - using custom methods introduced with @feathersjs/express v4 5 | * - with security 6 | * - with refs for request and response 7 | */ 8 | const swagger = require('../../lib'); 9 | 10 | const contentSchema = (schema) => ({ 11 | content: { 12 | 'application/json': { 13 | schema 14 | } 15 | } 16 | }); 17 | 18 | module.exports = (app) => { 19 | const service = { 20 | find (params) { 21 | return Promise.resolve({ method: 'find', params }); 22 | }, 23 | customPost: swagger.customMethod('POST', '/do-post')((data, params) => { 24 | return Promise.resolve({ method: 'customPost', data, params }); 25 | }), 26 | customUpdateWithId: swagger.customMethod('PUT', '/do-patch-with/:__feathersId')( 27 | (data, params, id) => { 28 | return Promise.resolve({ method: 'customPatchWithId', id, data, params }); 29 | } 30 | ), 31 | customGetWithCustomIds: swagger.customMethod('GET', '/do-patch-with/:customId/:customId2')( 32 | (data, params) => { 33 | return Promise.resolve({ method: 'customGetWithCustomIds', data, params }); 34 | } 35 | ) 36 | }; 37 | 38 | service.docs = { 39 | description: 'A service with custom methods', 40 | pathParams: { 41 | pathParamName: { 42 | description: 'A global path param', 43 | in: 'path', 44 | name: 'pathParamName', 45 | schema: { 46 | type: 'string' 47 | }, 48 | required: true 49 | } 50 | }, 51 | tag: 'custom method service', 52 | definitions: { 53 | custom_response: { 54 | title: 'Custom service response', 55 | type: 'object', 56 | required: [ 57 | 'method' 58 | ], 59 | properties: { 60 | method: { 61 | type: 'string', 62 | description: 'name of the called method' 63 | }, 64 | data: { 65 | type: 'object', 66 | description: 'POST data provided to the service method' 67 | }, 68 | params: { 69 | type: 'object', 70 | description: 'params provided to the service method' 71 | }, 72 | id: { 73 | type: 'integer', 74 | description: 'id provided to the service method' 75 | } 76 | } 77 | }, 78 | custom_request: { 79 | title: 'Custom service requestBody', 80 | type: 'object', 81 | required: [ 82 | 'method' 83 | ], 84 | properties: { 85 | string: { 86 | type: 'string', 87 | description: 'some string data' 88 | }, 89 | number: { 90 | type: 'number', 91 | format: 'int32', 92 | description: 'a number' 93 | } 94 | } 95 | } 96 | }, 97 | securities: ['customPost'], 98 | refs: { 99 | customPostRequest: 'custom_request', 100 | customPostResponse: 'custom_response', 101 | customUpdateWithIdRequest: 'custom_request', 102 | customUpdateWithIdResponse: 'custom_response' 103 | }, 104 | operations: { 105 | find: { 106 | description: 'Do something with a GET method', 107 | // remove default parameters with nested path 'logic' 108 | 'parameters[4]': undefined, 109 | 'parameters[3]': undefined, 110 | 'parameters[2]': undefined, 111 | 'parameters[1]': { 112 | description: 'A defined param', 113 | in: 'query', 114 | name: 'test', 115 | schema: { 116 | type: 'string' 117 | } 118 | }, 119 | 'responses.200': { 120 | description: 'Use find to do a normal GET', 121 | ...contentSchema({ $ref: '#/components/schemas/custom_response' }) 122 | } 123 | }, 124 | customPost: { 125 | description: 'A custom POST method', 126 | 'parameters[]': { 127 | description: 'A defined param', 128 | in: 'query', 129 | name: 'test', 130 | schema: { 131 | type: 'string' 132 | } 133 | } 134 | }, 135 | customGetWithCustomIds: { 136 | description: 'A custom GET method', 137 | 'parameters[1]': { 138 | description: 'Custom integer path id', 139 | in: 'path', 140 | required: true, 141 | name: 'customId', 142 | schema: { 143 | type: 'integer' 144 | } 145 | }, 146 | 'parameters[2]': { 147 | description: 'Custom string path id', 148 | in: 'path', 149 | required: true, 150 | name: 'customId2', 151 | schema: { 152 | type: 'string' 153 | } 154 | } 155 | } 156 | } 157 | }; 158 | 159 | app.configure(swagger({ 160 | openApiVersion: 3, 161 | prefix: 'v3/custom-methods/', 162 | docsJsonPath: '/v3/custom-methods.json', 163 | ui: swagger.swaggerUI({ docsPath: '/v3/custom-methods' }), 164 | specs: { 165 | info: { 166 | title: 'A test', 167 | description: 'An example using custom methods', 168 | version: '1.0.0' 169 | }, 170 | components: { 171 | securitySchemes: { 172 | BasicAuth: { 173 | type: 'http', 174 | scheme: 'basic' 175 | }, 176 | BearerAuth: { 177 | type: 'http', 178 | scheme: 'bearer' 179 | } 180 | } 181 | }, 182 | security: [ 183 | { BearerAuth: [] } 184 | ] 185 | }, 186 | include: { 187 | paths: ['v3/custom-methods/:pathParamName/service'] 188 | } 189 | })) 190 | .use( 191 | '/v3/custom-methods/:pathParamName/service', 192 | service, 193 | { methods: ['find', 'customPost', 'customUpdateWithId', 'customGetWithCustomIds'] } 194 | ); 195 | }; 196 | -------------------------------------------------------------------------------- /example/openapi-v3/customTags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for openapi v3 3 | * - using definitions option of service.docs to define all needed definitions 4 | * - using custom tag 5 | * - using custom tags, with one being ignored 6 | * - using custom model 7 | * - using externalDocs 8 | * - using custom list schemaName 9 | */ 10 | 11 | const memory = require('feathers-memory'); 12 | const swagger = require('../../lib'); 13 | 14 | module.exports = (app) => { 15 | const messageService = memory(); 16 | 17 | messageService.docs = { 18 | tag: 'message', 19 | description: 'A service to send and receive messages', 20 | externalDocs: { 21 | description: 'find more info here', 22 | url: 'https://swagger.io/about' 23 | }, 24 | tags: ['message', 'additional', 'ignored'], 25 | model: 'custom_message', 26 | schemaNames: { 27 | list: () => 'custom_message_list' 28 | }, 29 | definitions: { 30 | custom_message: { 31 | title: 'Message', 32 | type: 'object', 33 | required: [ 34 | 'text' 35 | ], 36 | properties: { 37 | text: { 38 | type: 'string', 39 | description: 'The message text' 40 | }, 41 | userId: { 42 | type: 'string', 43 | description: 'The id of the user that send the message' 44 | } 45 | } 46 | }, 47 | custom_message_list: { 48 | title: 'List of Messages', 49 | type: 'array', 50 | items: { 51 | $ref: '#/components/schemas/custom_message' 52 | } 53 | } 54 | } 55 | }; 56 | 57 | app.configure(swagger({ 58 | openApiVersion: 3, 59 | prefix: 'v3/custom-tags/', 60 | docsJsonPath: '/v3/custom-tags.json', 61 | ui: swagger.swaggerUI({ docsPath: '/v3/custom-tags' }), 62 | specs: { 63 | info: { 64 | title: 'A test', 65 | description: 'An example using custom tags and model', 66 | version: '1.0.0' 67 | }, 68 | tags: [swagger.tag('additional', { 69 | description: 'An additional tag, that can be used' 70 | })] 71 | }, 72 | include: { 73 | paths: ['v3/custom-tags/messages'] 74 | }, 75 | ignore: { 76 | tags: ['ignored'] 77 | } 78 | })) 79 | .use('/v3/custom-tags/messages', messageService); 80 | }; 81 | -------------------------------------------------------------------------------- /example/openapi-v3/definitionWithCustomizedSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for openapi v3 3 | * - using default swagger ui with ui option 4 | * - just define the model with definition option of service.docs 5 | * - use a customized default generator for update 6 | * - use a customized default (object) for get 7 | */ 8 | 9 | const memory = require('feathers-memory'); 10 | const swagger = require('../../lib'); 11 | 12 | module.exports = (app) => { 13 | const messageService = memory(); 14 | 15 | messageService.docs = { 16 | description: 'A service to send and receive messages', 17 | definition: { 18 | type: 'object', 19 | required: [ 20 | 'text' 21 | ], 22 | properties: { 23 | text: { 24 | type: 'string', 25 | description: 'The message text' 26 | }, 27 | userId: { 28 | type: 'string', 29 | description: 'The id of the user that send the message' 30 | } 31 | } 32 | } 33 | }; 34 | 35 | app.configure(swagger({ 36 | openApiVersion: 3, 37 | prefix: 'v3/definition-with-customized-update/', 38 | docsJsonPath: '/v3/definition-with-customized-update.json', 39 | ui: swagger.swaggerUI({ docsPath: '/v3/definition-with-customized-update' }), 40 | specs: { 41 | info: { 42 | title: 'A test', 43 | description: 'A description', 44 | version: '1.0.0' 45 | } 46 | }, 47 | include: { 48 | paths: ['v3/definition-with-customized-update/messages'] 49 | }, 50 | defaults: { 51 | operationGenerators: { 52 | update ({ tag, modelName, idName, idType, security, securities, refs }) { 53 | return { 54 | tags: [tag, 'update'], 55 | description: 'Changed stuff', 56 | parameters: [{ 57 | description: `Some custom text still with ${modelName}`, 58 | in: 'path', 59 | required: true, 60 | name: idName, 61 | schema: { 62 | type: idType 63 | } 64 | }], 65 | requestBody: { 66 | required: true, 67 | content: { 68 | 'application/json': { 69 | schema: { 70 | $ref: `#/components/schemas/${refs.updateRequest}` 71 | } 72 | } 73 | } 74 | }, 75 | responses: { 76 | 500: { 77 | description: 'will always fail :D' 78 | } 79 | }, 80 | security: securities.indexOf('update') > -1 ? security : [] 81 | }; 82 | } 83 | }, 84 | operations: { 85 | get: { 86 | description: 'Overwrite just one property', 87 | 'responses.500.description': 'Oops, something went wrong' 88 | } 89 | } 90 | } 91 | })) 92 | .use('/v3/definition-with-customized-update/messages', messageService); 93 | }; 94 | -------------------------------------------------------------------------------- /example/openapi-v3/definitions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for openapi v3 3 | * - using definitions option of service.docs to define all needed definitions 4 | * - service with pagination 5 | * - using swagger ui with a custom indexFile 6 | * - add parameter to find (globally) 7 | * - set specific values and sub values for a operation 8 | * - set/remove a value from all methods 9 | */ 10 | 11 | const path = require('path'); 12 | const memory = require('feathers-memory'); 13 | const swagger = require('../../lib'); 14 | 15 | module.exports = (app) => { 16 | const messageService = memory({ paginate: { default: 10, max: 50 } }); 17 | const uiIndexFile = path.join(__dirname, 'docs.html'); 18 | 19 | messageService.docs = { 20 | description: 'A service to send and receive messages', 21 | definitions: { 22 | messages: { 23 | type: 'object', 24 | required: [ 25 | 'text' 26 | ], 27 | properties: { 28 | text: { 29 | type: 'string', 30 | description: 'The message text' 31 | }, 32 | userId: { 33 | type: 'string', 34 | description: 'The id of the user that send the message' 35 | } 36 | } 37 | }, 38 | messagesList: { 39 | type: 'array', 40 | items: { 41 | $ref: '#/components/schemas/messages' 42 | } 43 | } 44 | }, 45 | operations: { 46 | get: { 47 | description: 'This is my custom get description', 48 | 'responses.200.description': 'Change just the description' 49 | }, 50 | all: { 51 | 'parameters[-]': { $ref: '#/components/parameters/customHeaderBefore' }, 52 | 'parameters[]': { $ref: '#/components/parameters/customHeaderAfter' }, 53 | 'responses.401': undefined 54 | } 55 | } 56 | }; 57 | 58 | app.configure(swagger({ 59 | openApiVersion: 3, 60 | prefix: 'v3/definitions/', 61 | docsJsonPath: '/v3/definitions.json', 62 | ui: swagger.swaggerUI({ docsPath: '/v3/definitions', indexFile: uiIndexFile }), 63 | defaults: { 64 | operations: { 65 | find: { 66 | 'parameters[]': { 67 | description: 'My custom query parameter', 68 | in: 'query', 69 | name: '$custom', 70 | schema: { 71 | type: 'string' 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | specs: { 78 | info: { 79 | title: 'A test', 80 | description: 'A description', 81 | version: '1.0.0' 82 | }, 83 | components: { 84 | parameters: { 85 | customHeaderBefore: { 86 | description: 'My custom header before all other parameters', 87 | in: 'header', 88 | required: false, 89 | name: 'X-Custom-Header-Before', 90 | schema: { 91 | type: 'string' 92 | } 93 | }, 94 | customHeaderAfter: { 95 | description: 'My custom header after all other parameters', 96 | in: 'header', 97 | required: true, 98 | name: 'X-Custom-Header-After', 99 | schema: { 100 | type: 'string' 101 | } 102 | } 103 | } 104 | } 105 | }, 106 | include: { 107 | paths: ['v3/definitions/messages'] 108 | } 109 | })) 110 | .use('/v3/definitions/messages', messageService); 111 | }; 112 | -------------------------------------------------------------------------------- /example/openapi-v3/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom Swagger UI IndexFile 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /example/openapi-v3/doveSchemas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for openapi v3 3 | * - using schemas introduced with dove (feathers v5) with createSwaggerServiceOptions 4 | * - service with pagination 5 | * - use include.filter option 6 | */ 7 | 8 | const memory = require('feathers-memory'); 9 | const { Type, querySyntax } = require('@feathersjs/typebox'); 10 | const swagger = require('../../lib'); 11 | 12 | module.exports = (app) => { 13 | app.configure(swagger({ 14 | openApiVersion: 3, 15 | prefix: 'v3/dove-schemas/', 16 | docsJsonPath: '/v3/dove-schemas.json', 17 | ui: swagger.swaggerUI({ docsPath: '/v3/dove-schemas' }), 18 | specs: { 19 | info: { 20 | title: 'A test', 21 | description: 'Show the result when using schema from feathers dove', 22 | version: '1.0.0' 23 | } 24 | }, 25 | include: { 26 | filter (service, path) { 27 | return service.includeThisServiceProp === true; 28 | } 29 | } 30 | })); 31 | 32 | const messageDataSchema = Type.Object( 33 | { message: Type.String() }, { $id: 'MessageData', additionalProperties: false } 34 | ); 35 | const messageSchema = Type.Intersect( 36 | [ 37 | Type.Object({ 38 | id: Type.Number() 39 | }), 40 | messageDataSchema 41 | ], 42 | { $id: 'Message' } 43 | ); 44 | const messageQuerySchema = Type.Intersect([ 45 | querySyntax(messageSchema), 46 | Type.Object({}) 47 | ]); 48 | 49 | const messageService = memory({ paginate: { default: 10, max: 50 } }); 50 | messageService.includeThisServiceProp = true; 51 | 52 | app.use('/v3/dove-schemas/messages', messageService, { 53 | // A list of all methods this service exposes externally 54 | methods: ['find', 'get', 'create', 'update', 'patch', 'remove'], 55 | // You can add additional custom events to be sent to clients here 56 | events: [], 57 | docs: swagger.createSwaggerServiceOptions({ 58 | schemas: { messageSchema, messageDataSchema, messageQuerySchema }, 59 | docs: { 60 | description: 'A custom description' 61 | } 62 | }) 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /example/openapi-v3/idNames.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for openapi v3 3 | * - using default swagger ui with ui option 4 | * - just define the model with definition option of service.docs 5 | * - use custom idNames for the different operations 6 | * - change description of id parameter 7 | */ 8 | 9 | const memory = require('feathers-memory'); 10 | const swagger = require('../../lib'); 11 | 12 | module.exports = (app) => { 13 | const messageService = memory(); 14 | 15 | messageService.docs = { 16 | description: 'A service to send and receive messages', 17 | definition: { 18 | type: 'object', 19 | required: [ 20 | 'text' 21 | ], 22 | properties: { 23 | text: { 24 | type: 'string', 25 | description: 'The message text' 26 | }, 27 | userId: { 28 | type: 'string', 29 | description: 'The id of the user that send the message' 30 | } 31 | } 32 | }, 33 | idNames: { 34 | get: 'slug', 35 | update: 'uid', 36 | patch: 'pid', 37 | remove: 'rid' 38 | }, 39 | operations: { 40 | get: { 41 | 'parameters[0].description': 'Slug for the message' 42 | } 43 | } 44 | }; 45 | 46 | app.configure(swagger({ 47 | openApiVersion: 3, 48 | prefix: 'v3/id-names/', 49 | docsJsonPath: '/v3/id-names.json', 50 | ui: swagger.swaggerUI({ docsPath: '/v3/id-names' }), 51 | specs: { 52 | info: { 53 | title: 'A test', 54 | description: 'An example with different id names', 55 | version: '1.0.0' 56 | } 57 | }, 58 | include: { 59 | paths: ['v3/id-names/messages'] 60 | } 61 | })) 62 | .use('/v3/id-names/messages', messageService); 63 | }; 64 | -------------------------------------------------------------------------------- /example/openapi-v3/multi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for openapi v3 3 | * - using default swagger ui with ui option 4 | * - just define the model with definition option of service.docs 5 | * - use service with multi: true and define multi option 6 | * - use refs.sortParameter 7 | * - use schemasGenerator 8 | * - use custom default schemaName for list 9 | */ 10 | 11 | const memory = require('feathers-memory'); 12 | const swagger = require('../../lib'); 13 | 14 | module.exports = (app) => { 15 | const messageService = memory({ multi: true }); 16 | 17 | messageService.model = { 18 | type: 'object', 19 | required: [ 20 | 'text' 21 | ], 22 | properties: { 23 | text: { 24 | type: 'string', 25 | description: 'The message text' 26 | }, 27 | userId: { 28 | type: 'string', 29 | description: 'The id of the user that send the message' 30 | } 31 | } 32 | }; 33 | 34 | messageService.docs = { 35 | description: 'A service to send and receive messages', 36 | multi: ['patch', 'remove', 'create'], 37 | refs: { 38 | sortParameter: 'messages_sort_filter' 39 | } 40 | }; 41 | 42 | app.configure(swagger({ 43 | openApiVersion: 3, 44 | prefix: 'v3/multi/', 45 | docsJsonPath: '/v3/multi.json', 46 | ui: swagger.swaggerUI({ docsPath: '/v3/multi' }), 47 | specs: { 48 | info: { 49 | title: 'A test', 50 | description: 'An example with "multi mode" service', 51 | version: '1.0.0' 52 | } 53 | }, 54 | include: { 55 | paths: ['v3/multi/messages'] 56 | }, 57 | defaults: { 58 | schemaNames: { 59 | list: (model) => `${model}_list` 60 | }, 61 | schemasGenerator (service, model, modelName) { 62 | // here you could transform an orm model to json schema for example 63 | // we simply reuse the schema 64 | 65 | return { 66 | [model]: service.model, 67 | [`${model}_list`]: { 68 | title: `${modelName} list`, 69 | type: 'array', 70 | items: { $ref: `#/components/schemas/${model}` } 71 | }, 72 | [`${model}_sort_filter`]: { 73 | title: `${modelName} sorting parameter`, 74 | type: 'object', 75 | properties: { 76 | text: { 77 | type: 'integer', 78 | description: 'sort by text, -1 for descending' 79 | }, 80 | userId: { 81 | type: 'integer', 82 | description: 'sort by userId, -1 for descending' 83 | } 84 | } 85 | } 86 | }; 87 | } 88 | } 89 | })) 90 | .use('/v3/multi/messages', messageService); 91 | }; 92 | -------------------------------------------------------------------------------- /example/openapi-v3/security.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for openapi v3 3 | * - using definitions option of service.docs to define all needed definitions 4 | * - add security definitions 5 | * - configure security for service operations 6 | * - use custom security definitions for specific operation 7 | * - use Swagger UI plugin @mairu/swagger-ui-apikey-auth-form for convenient bearer token usage 8 | */ 9 | 10 | const memory = require('feathers-memory'); 11 | const swagger = require('../../lib'); 12 | 13 | module.exports = (app) => { 14 | const messageService = memory(); 15 | 16 | messageService.docs = { 17 | description: 'A service to send and receive messages', 18 | definitions: { 19 | messages: { 20 | title: 'Message', 21 | type: 'object', 22 | required: [ 23 | 'text' 24 | ], 25 | properties: { 26 | text: { 27 | type: 'string', 28 | description: 'The message text' 29 | }, 30 | userId: { 31 | type: 'string', 32 | description: 'The id of the user that send the message' 33 | } 34 | } 35 | }, 36 | messagesList: { 37 | title: 'List of Messages', 38 | type: 'array', 39 | items: { 40 | $ref: '#/components/schemas/messages' 41 | } 42 | } 43 | }, 44 | securities: ['create', 'update', 'patch', 'remove'], 45 | operations: { 46 | find: { 47 | security: [ 48 | { BasicAuth: [] } 49 | ] 50 | } 51 | } 52 | }; 53 | 54 | app.get('/v3/security/swagger-ui-apikey-auth-form.js', function (req, res) { 55 | res.sendFile(require.resolve('@mairu/swagger-ui-apikey-auth-form/dist/swagger-ui-apikey-auth-form')); 56 | }); 57 | app.configure(swagger({ 58 | openApiVersion: 3, 59 | prefix: 'v3/security/', 60 | docsJsonPath: '/v3/security.json', 61 | ui: swagger.swaggerUI({ 62 | docsPath: '/v3/security', 63 | getSwaggerInitializerScript: ({ docsJsonPath }) => { 64 | // language=JavaScript 65 | return ` 66 | window.onload = function() { 67 | var script = document.createElement('script'); 68 | script.onload = function () { 69 | window.ui = SwaggerUIBundle({ 70 | url: "${docsJsonPath}", 71 | dom_id: '#swagger-ui', 72 | deepLinking: true, 73 | presets: [ 74 | SwaggerUIBundle.presets.apis, 75 | SwaggerUIStandalonePreset, 76 | SwaggerUIApiKeyAuthFormPlugin, 77 | ], 78 | plugins: [ 79 | SwaggerUIBundle.plugins.DownloadUrl 80 | ], 81 | layout: "StandaloneLayout", 82 | configs: { 83 | apiKeyAuthFormPlugin: { 84 | forms: { 85 | BearerAuth: { 86 | authCallback(values, callback) { 87 | // mimic logic process 88 | if (values.username === 'user' && values.password === 'secret') { 89 | callback(null, 'secret-key'); 90 | } else { 91 | callback('invalid credentials'); 92 | } 93 | }, 94 | } 95 | }, 96 | localStorage: { 97 | BearerAuth: {} 98 | } 99 | } 100 | } 101 | }); 102 | }; 103 | 104 | script.src = '/v3/security/swagger-ui-apikey-auth-form.js'; 105 | document.head.appendChild(script) 106 | }; 107 | `; 108 | } 109 | }), 110 | specs: { 111 | info: { 112 | title: 'A test', 113 | description: 'An example using security definitions and swagger ui plugin.' + 114 | ' The valid credentials for BearerAuth in this example are `user` with password `secret`.', 115 | version: '1.0.0' 116 | }, 117 | components: { 118 | securitySchemes: { 119 | BasicAuth: { 120 | type: 'http', 121 | scheme: 'basic' 122 | }, 123 | BearerAuth: { 124 | type: 'http', 125 | scheme: 'bearer' 126 | } 127 | } 128 | }, 129 | security: [ 130 | { BearerAuth: [] } 131 | ] 132 | }, 133 | include: { 134 | paths: ['v3/security/messages'] 135 | } 136 | })) 137 | .use('/v3/security/messages', messageService); 138 | }; 139 | -------------------------------------------------------------------------------- /example/swagger-v2/customMethods.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for swagger v2 3 | * - using parameters in path to service 4 | * - using custom methods introduced with @feathersjs/express v4 5 | * - with security 6 | * - with refs for request and response 7 | */ 8 | const swagger = require('../../lib'); 9 | 10 | module.exports = (app) => { 11 | const service = { 12 | find (params) { 13 | return Promise.resolve({ method: 'find', params }); 14 | }, 15 | customPost: swagger.customMethod('POST', '/do-post')((data, params) => { 16 | return Promise.resolve({ method: 'customPost', data, params }); 17 | }), 18 | customPatchWithId: swagger.customMethod('PUT', '/do-patch-with/:__feathersId')( 19 | (data, params, id) => { 20 | return Promise.resolve({ method: 'customPatchWithId', id, data, params }); 21 | } 22 | ), 23 | customGetWithCustomIds: swagger.customMethod('GET', '/do-patch-with/:customId/:customId2')( 24 | (data, params) => { 25 | return Promise.resolve({ method: 'customGetWithCustomIds', data, params }); 26 | } 27 | ) 28 | }; 29 | 30 | service.docs = { 31 | description: 'A service with custom methods', 32 | pathParams: { 33 | pathParamName: { 34 | description: 'A global path param', 35 | in: 'path', 36 | name: 'pathParamName', 37 | type: 'string', 38 | required: true 39 | } 40 | }, 41 | tag: 'custom method service', 42 | definitions: { 43 | custom_response: { 44 | title: 'Custom service response', 45 | type: 'object', 46 | required: [ 47 | 'method' 48 | ], 49 | properties: { 50 | method: { 51 | type: 'string', 52 | description: 'name of the called method' 53 | }, 54 | data: { 55 | type: 'object', 56 | description: 'POST data provided to the service method' 57 | }, 58 | params: { 59 | type: 'object', 60 | description: 'params provided to the service method' 61 | }, 62 | id: { 63 | type: 'integer', 64 | description: 'id provided to the service method' 65 | } 66 | } 67 | }, 68 | custom_request: { 69 | title: 'Custom service requestBody', 70 | type: 'object', 71 | required: [ 72 | 'method' 73 | ], 74 | properties: { 75 | string: { 76 | type: 'string', 77 | description: 'some string data' 78 | }, 79 | number: { 80 | type: 'number', 81 | format: 'int32', 82 | description: 'a number' 83 | } 84 | } 85 | } 86 | }, 87 | securities: ['customPost'], 88 | refs: { 89 | customPostRequest: 'custom_request', 90 | customPostResponse: 'custom_response', 91 | customPatchWithIdRequest: 'custom_request', 92 | customPatchWithIdResponse: 'custom_response' 93 | }, 94 | operations: { 95 | find: { 96 | description: 'Do something with a GET method', 97 | // remove default parameters with nested path 'logic' 98 | 'parameters[3]': undefined, 99 | 'parameters[2]': undefined, 100 | 'parameters[1]': { 101 | description: 'A defined param', 102 | in: 'query', 103 | name: 'test', 104 | type: 'string' 105 | }, 106 | 'responses.200': { 107 | description: 'Use find to do a normal GET', 108 | schema: { $ref: '#/definitions/custom_response' } 109 | } 110 | }, 111 | customPost: { 112 | description: 'A custom POST method', 113 | 'parameters[]': { 114 | description: 'A defined param', 115 | in: 'query', 116 | name: 'test', 117 | type: 'string' 118 | } 119 | }, 120 | customGetWithCustomIds: { 121 | description: 'A custom GET method', 122 | 'parameters[1]': { 123 | description: 'Custom integer path id', 124 | in: 'path', 125 | required: true, 126 | name: 'customId', 127 | type: 'integer' 128 | }, 129 | 'parameters[2]': { 130 | description: 'Custom string path id', 131 | in: 'path', 132 | required: true, 133 | name: 'customId2', 134 | type: 'string' 135 | } 136 | } 137 | } 138 | }; 139 | 140 | app.configure(swagger({ 141 | openApiVersion: 2, 142 | prefix: 'v2/custom-methods/', 143 | docsJsonPath: '/v2/custom-methods.json', 144 | ui: swagger.swaggerUI({ docsPath: '/v2/custom-methods' }), 145 | specs: { 146 | info: { 147 | title: 'A test', 148 | description: 'An example using custom methods', 149 | version: '1.0.0' 150 | }, 151 | securityDefinitions: { 152 | BasicAuth: { 153 | type: 'basic' 154 | }, 155 | ApiKeyAuth: { 156 | type: 'apiKey', 157 | in: 'header', 158 | name: 'X-API-Key' 159 | } 160 | }, 161 | security: [ 162 | { ApiKeyAuth: [] } 163 | ] 164 | }, 165 | include: { 166 | paths: ['v2/custom-methods/:pathParamName/service'] 167 | } 168 | })) 169 | .use( 170 | '/v2/custom-methods/:pathParamName/service', 171 | service, 172 | { methods: ['find', 'customPost', 'customPatchWithId', 'customGetWithCustomIds'] } 173 | ); 174 | }; 175 | -------------------------------------------------------------------------------- /example/swagger-v2/customTags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for swagger v2 3 | * - using definitions option of service.docs to define all needed definitions 4 | * - using custom tag 5 | * - using custom tags, with one being ignored 6 | * - using custom model 7 | * - using externalDocs 8 | * - using custom list schemaName 9 | */ 10 | 11 | const memory = require('feathers-memory'); 12 | const swagger = require('../../lib'); 13 | 14 | module.exports = (app) => { 15 | const messageService = memory(); 16 | 17 | messageService.docs = { 18 | tag: 'message', 19 | description: 'A service to send and receive messages', 20 | externalDocs: { 21 | description: 'find more info here', 22 | url: 'https://swagger.io/about' 23 | }, 24 | tags: ['message', 'additional', 'ignored'], 25 | model: 'custom_message', 26 | definitions: { 27 | custom_message: { 28 | title: 'Message', 29 | type: 'object', 30 | required: [ 31 | 'text' 32 | ], 33 | properties: { 34 | text: { 35 | type: 'string', 36 | description: 'The message text' 37 | }, 38 | userId: { 39 | type: 'string', 40 | description: 'The id of the user that send the message' 41 | } 42 | } 43 | }, 44 | custom_message_list: { 45 | title: 'List of Messages', 46 | type: 'array', 47 | items: { 48 | $ref: '#/definitions/custom_message' 49 | } 50 | } 51 | }, 52 | schemaNames: { 53 | list: () => 'custom_message_list' 54 | } 55 | }; 56 | 57 | app.configure(swagger({ 58 | openApiVersion: 2, 59 | prefix: 'v2/custom-tags/', 60 | docsJsonPath: '/v2/custom-tags.json', 61 | ui: swagger.swaggerUI({ docsPath: '/v2/custom-tags' }), 62 | specs: { 63 | info: { 64 | title: 'A test', 65 | description: 'An example using custom tags and model', 66 | version: '1.0.0' 67 | }, 68 | tags: [swagger.tag('additional', { 69 | description: 'An additional tag, that can be used' 70 | })] 71 | }, 72 | include: { 73 | paths: ['v2/custom-tags/messages'] 74 | }, 75 | ignore: { 76 | tags: ['ignored'] 77 | } 78 | })) 79 | .use('/v2/custom-tags/messages', messageService); 80 | }; 81 | -------------------------------------------------------------------------------- /example/swagger-v2/definitionWithCustomizedSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for swagger v2 3 | * - using default swagger ui with ui option 4 | * - just define the model with definition option of service.docs 5 | * - use a customized default generator for update 6 | * - use a customized default (object) for get 7 | */ 8 | 9 | const memory = require('feathers-memory'); 10 | const swagger = require('../../lib'); 11 | 12 | module.exports = (app) => { 13 | const messageService = memory(); 14 | 15 | messageService.docs = { 16 | description: 'A service to send and receive messages', 17 | definition: { 18 | type: 'object', 19 | required: [ 20 | 'text' 21 | ], 22 | properties: { 23 | text: { 24 | type: 'string', 25 | description: 'The message text' 26 | }, 27 | userId: { 28 | type: 'string', 29 | description: 'The id of the user that send the message' 30 | } 31 | } 32 | } 33 | }; 34 | 35 | app.configure(swagger({ 36 | openApiVersion: 2, 37 | prefix: 'v2/definition-with-customized-update/', 38 | docsJsonPath: '/v2/definition-with-customized-update.json', 39 | ui: swagger.swaggerUI({ docsPath: '/v2/definition-with-customized-update' }), 40 | specs: { 41 | info: { 42 | title: 'A test', 43 | description: 'A description', 44 | version: '1.0.0' 45 | } 46 | }, 47 | include: { 48 | paths: ['v2/definition-with-customized-update/messages'] 49 | }, 50 | defaults: { 51 | operationGenerators: { 52 | update ({ tag, modelName, idName, idType, security, securities, specs, refs }) { 53 | return { 54 | tags: [tag, 'update'], 55 | description: 'Changed stuff', 56 | parameters: [{ 57 | description: `Some custom text still with ${modelName}`, 58 | in: 'path', 59 | required: true, 60 | name: idName, 61 | type: idType 62 | }, { 63 | in: 'body', 64 | name: 'body', 65 | required: true, 66 | schema: { 67 | $ref: `#/definitions/${refs.updateRequest}` 68 | } 69 | }], 70 | responses: { 71 | 500: { 72 | description: 'will always fail :D' 73 | } 74 | }, 75 | produces: specs.produces, 76 | consumes: specs.consumes, 77 | security: securities.indexOf('update') > -1 ? security : [] 78 | }; 79 | } 80 | }, 81 | operations: { 82 | get: { 83 | description: 'Overwrite just one property', 84 | 'responses.500.description': 'Oops, something went wrong' 85 | } 86 | } 87 | } 88 | })) 89 | .use('/v2/definition-with-customized-update/messages', messageService); 90 | }; 91 | -------------------------------------------------------------------------------- /example/swagger-v2/definitions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for swagger v2 3 | * - using definitions option of service.docs to define all needed definitions 4 | * - service with pagination 5 | * - using swagger ui with a custom indexFile 6 | * - add parameter to find (globally) 7 | * - set specific values and sub values for a operation 8 | */ 9 | 10 | const path = require('path'); 11 | const memory = require('feathers-memory'); 12 | const swagger = require('../../lib'); 13 | 14 | module.exports = (app) => { 15 | const messageService = memory({ paginate: { default: 10 } }); 16 | const uiIndexFile = path.join(__dirname, 'docs.html'); 17 | 18 | messageService.docs = { 19 | description: 'A service to send and receive messages', 20 | definitions: { 21 | messages: { 22 | type: 'object', 23 | required: [ 24 | 'text' 25 | ], 26 | properties: { 27 | text: { 28 | type: 'string', 29 | description: 'The message text' 30 | }, 31 | userId: { 32 | type: 'string', 33 | description: 'The id of the user that send the message' 34 | } 35 | } 36 | }, 37 | messagesList: { 38 | type: 'array', 39 | items: { 40 | $ref: '#/definitions/messages' 41 | } 42 | } 43 | }, 44 | operations: { 45 | get: { 46 | description: 'This is my custom get description', 47 | 'responses.200.description': 'Change just the description' 48 | }, 49 | all: { 50 | 'parameters[-]': { $ref: '#/definitions/customHeaderBefore' }, 51 | 'parameters[]': { $ref: '#/definitions/customHeaderAfter' }, 52 | 'responses.401': undefined 53 | } 54 | } 55 | }; 56 | 57 | app.configure(swagger({ 58 | openApiVersion: 2, 59 | prefix: 'v2/definitions/', 60 | docsJsonPath: '/v2/definitions.json', 61 | ui: swagger.swaggerUI({ docsPath: '/v2/definitions', indexFile: uiIndexFile }), 62 | defaults: { 63 | operations: { 64 | find: { 65 | 'parameters[]': { 66 | description: 'My custom query parameter', 67 | in: 'query', 68 | name: '$custom', 69 | type: 'string' 70 | } 71 | } 72 | } 73 | }, 74 | specs: { 75 | info: { 76 | title: 'A test', 77 | description: 'A description', 78 | version: '1.0.0' 79 | }, 80 | definitions: { 81 | customHeaderBefore: { 82 | description: 'My custom header before all other parameters', 83 | in: 'header', 84 | required: false, 85 | name: 'X-Custom-Header-Before', 86 | type: 'string' 87 | }, 88 | customHeaderAfter: { 89 | description: 'My custom header after all other parameters', 90 | in: 'header', 91 | required: true, 92 | name: 'X-Custom-Header-After', 93 | type: 'string' 94 | } 95 | } 96 | }, 97 | include: { 98 | paths: ['v2/definitions/messages'] 99 | } 100 | })) 101 | .use('/v2/definitions/messages', messageService); 102 | }; 103 | -------------------------------------------------------------------------------- /example/swagger-v2/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom Swagger UI IndexFile 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /example/swagger-v2/multi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for swagger v2 3 | * - using default swagger ui with ui option 4 | * - just define the model with definition option of service.docs 5 | * - use service with multi: true and define multi option 6 | * - for swagger v2 filter parameters have to be defined manually 7 | */ 8 | 9 | const memory = require('feathers-memory'); 10 | const swagger = require('../../lib'); 11 | 12 | module.exports = (app) => { 13 | const messageService = memory({ multi: true }); 14 | 15 | messageService.docs = { 16 | description: 'A service to send and receive messages', 17 | definition: { 18 | type: 'object', 19 | required: [ 20 | 'text' 21 | ], 22 | properties: { 23 | text: { 24 | type: 'string', 25 | description: 'The message text' 26 | }, 27 | userId: { 28 | type: 'string', 29 | description: 'The id of the user that send the message' 30 | } 31 | } 32 | }, 33 | multi: ['patch', 'remove'], 34 | operations: { 35 | find: { 36 | 'parameters[+]': { 37 | description: 'UserId to find messages for', 38 | in: 'query', 39 | required: false, 40 | name: 'userId', 41 | type: 'string' 42 | } 43 | }, 44 | patchMulti: { 45 | 'parameters[-]': { 46 | description: 'UserId to update messages for', 47 | in: 'query', 48 | required: true, 49 | name: 'userId', 50 | type: 'string' 51 | } 52 | }, 53 | removeMulti: { 54 | 'parameters[-]': { 55 | description: 'UserId to delete messages for', 56 | in: 'query', 57 | required: true, 58 | name: 'userId', 59 | type: 'string' 60 | } 61 | } 62 | } 63 | }; 64 | 65 | app.configure(swagger({ 66 | openApiVersion: 2, 67 | prefix: 'v2/multi/', 68 | docsJsonPath: '/v2/multi.json', 69 | ui: swagger.swaggerUI({ docsPath: '/v2/multi' }), 70 | specs: { 71 | info: { 72 | title: 'A test', 73 | description: 'An example with "multi mode" service', 74 | version: '1.0.0' 75 | } 76 | }, 77 | include: { 78 | paths: ['v2/multi/messages'] 79 | } 80 | })) 81 | .use('/v2/multi/messages', messageService); 82 | }; 83 | -------------------------------------------------------------------------------- /example/swagger-v2/security.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example for swagger v2 3 | * - using definitions option of service.docs to define all needed definitions 4 | * - add security definitions 5 | * - configure security for service operations 6 | * - use custom security definitions for specific operation 7 | */ 8 | 9 | const memory = require('feathers-memory'); 10 | const swagger = require('../../lib'); 11 | 12 | module.exports = (app) => { 13 | const messageService = memory(); 14 | 15 | messageService.docs = { 16 | description: 'A service to send and receive messages', 17 | definitions: { 18 | messages: { 19 | title: 'Message', 20 | type: 'object', 21 | required: [ 22 | 'text' 23 | ], 24 | properties: { 25 | text: { 26 | type: 'string', 27 | description: 'The message text' 28 | }, 29 | userId: { 30 | type: 'string', 31 | description: 'The id of the user that send the message' 32 | } 33 | } 34 | }, 35 | messagesList: { 36 | title: 'List of Messages', 37 | type: 'array', 38 | items: { 39 | $ref: '#/definitions/messages' 40 | } 41 | } 42 | }, 43 | securities: ['create', 'update', 'patch', 'remove'], 44 | operations: { 45 | find: { 46 | security: [ 47 | { BasicAuth: [] } 48 | ] 49 | } 50 | } 51 | }; 52 | 53 | app.configure(swagger({ 54 | openApiVersion: 3, 55 | prefix: 'v2/security/', 56 | docsJsonPath: '/v2/security.json', 57 | ui: swagger.swaggerUI({ docsPath: '/v2/security' }), 58 | specs: { 59 | info: { 60 | title: 'A test', 61 | description: 'An example using security definitions', 62 | version: '1.0.0' 63 | }, 64 | securityDefinitions: { 65 | BasicAuth: { 66 | type: 'basic' 67 | }, 68 | ApiKeyAuth: { 69 | type: 'apiKey', 70 | in: 'header', 71 | name: 'X-API-Key' 72 | } 73 | }, 74 | security: [ 75 | { ApiKeyAuth: [] } 76 | ] 77 | }, 78 | include: { 79 | paths: ['v2/security/messages'] 80 | } 81 | })) 82 | .use('/v2/security/messages', messageService); 83 | }; 84 | -------------------------------------------------------------------------------- /lib/custom-methods.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const { checkImport, isExpressApp, isKoaApp, requireOrFail } = require('./helpers'); 4 | 5 | const CUSTOM_METHOD = Symbol('feathers-swagger/CUSTOM_METHOD'); 6 | 7 | let registerCustomMethod = _.identity; 8 | let getCustomMethods = () => []; 9 | let customMethodsHandler = _.noop; 10 | 11 | if (checkImport('@feathersjs/express/rest', 'HTTP_METHOD')) { 12 | const { activateHooks } = require('@feathersjs/feathers'); 13 | const { HTTP_METHOD, httpMethod } = require('@feathersjs/express/rest'); 14 | 15 | getCustomMethods = (app, service, defaultMethods, basePath, doc) => { 16 | const customMethods = []; 17 | if (!service.methods) { 18 | return customMethods; 19 | } 20 | Object.keys(_.omit(service.methods, Object.keys(defaultMethods))).forEach((method) => { 21 | const serviceMethod = service[method]; 22 | if (typeof serviceMethod !== 'function' || doc.operations[method] === false) { 23 | return; 24 | } 25 | const httpMethod = serviceMethod[HTTP_METHOD]; 26 | if (!httpMethod) { 27 | return; 28 | } 29 | httpMethod.forEach(({ verb, uri }) => { 30 | // add support for id 31 | const path = `${basePath}${uri}`; 32 | customMethods.push({ path, method, httpMethod: verb.toLowerCase() }); 33 | }); 34 | }); 35 | return customMethods; 36 | }; 37 | 38 | registerCustomMethod = (verb, path) => { 39 | const hooksParams = ['data', 'params']; 40 | if (path.includes(':__feathersId')) { 41 | hooksParams.push('id'); 42 | } 43 | return function (method) { 44 | return httpMethod(verb, path)(activateHooks(hooksParams)(method)); 45 | }; 46 | }; 47 | } else if (checkImport('@feathersjs/feathers', 'getServiceOptions')) { 48 | const { getServiceOptions, defaultServiceMethods } = require('@feathersjs/feathers'); 49 | const { http } = require('@feathersjs/transport-commons'); 50 | 51 | const CUSTOM_METHOD_HANDLER = '_feathers_swagger_custom_method_handler_'; 52 | 53 | getCustomMethods = (app, service, defaultMethods, basePath, doc, serviceOptions) => { 54 | const serviceCustomMethods = []; 55 | const localServiceOptions = (serviceOptions || getServiceOptions(service)); 56 | if (!localServiceOptions || !localServiceOptions.methods) { 57 | return serviceCustomMethods; 58 | } 59 | localServiceOptions.methods.forEach((method) => { 60 | if (defaultServiceMethods.includes(method)) { 61 | return; 62 | } 63 | const serviceMethod = service[method]; 64 | if (typeof serviceMethod !== 'function' || doc.operations[method] === false) { 65 | return; 66 | } 67 | const customMethods = serviceMethod[CUSTOM_METHOD]; 68 | if (!customMethods || !app[CUSTOM_METHOD_HANDLER]) { 69 | return; 70 | } 71 | 72 | customMethods.forEach(({ verb, path: uri }) => { 73 | // add support for id 74 | const httpMethod = verb.toLowerCase(); 75 | const path = `${basePath}${uri}`; 76 | app[CUSTOM_METHOD_HANDLER](basePath, path, method, httpMethod); 77 | 78 | serviceCustomMethods.push({ path, method, httpMethod }); 79 | }); 80 | }); 81 | return serviceCustomMethods; 82 | }; 83 | 84 | registerCustomMethod = (verb, path) => { 85 | return function (method) { 86 | if (!method[CUSTOM_METHOD]) { 87 | method[CUSTOM_METHOD] = []; 88 | } 89 | method[CUSTOM_METHOD].push({ verb, path }); 90 | return method; 91 | }; 92 | }; 93 | 94 | customMethodsHandler = (app) => { 95 | if (isExpressApp(app)) { 96 | const { Router } = require('express'); 97 | const router = Router(); 98 | app.use(router); 99 | app[CUSTOM_METHOD_HANDLER] = (basePath, path, method, httpMethod) => { 100 | router[httpMethod](path, (req, res, next) => { 101 | req.url = basePath; 102 | req.headers[http.METHOD_HEADER] = method; 103 | req.method = 'POST'; 104 | req.feathers = { ...req.feathers, params: req.params }; 105 | next(); 106 | }); 107 | }; 108 | } else if (isKoaApp(app)) { 109 | const Router = requireOrFail('@koa/router', 'to use the customMethodsHandler'); 110 | const router = new Router(); 111 | app 112 | .use(router.routes()) 113 | .use(router.allowedMethods()); 114 | app[CUSTOM_METHOD_HANDLER] = (basePath, path, method, httpMethod) => { 115 | router[httpMethod](path, async (ctx, next) => { 116 | ctx.request.path = basePath; 117 | ctx.request.headers[http.METHOD_HEADER] = method; 118 | ctx.request.method = 'POST'; 119 | ctx.feathers = { ...ctx.feathers, params: ctx.params }; 120 | await next(); 121 | }); 122 | }; 123 | } 124 | }; 125 | } 126 | 127 | function customMethodDecorator (verb, path) { 128 | return function (target, propertyKey) { 129 | target[propertyKey] = customMethod(verb, path)(target[propertyKey]); 130 | }; 131 | } 132 | 133 | function customMethod (verb, path) { 134 | return function () { 135 | if (arguments.length === 1) { 136 | return registerCustomMethod(verb, path)(arguments[0]); 137 | } 138 | return customMethodDecorator(verb, path)(...arguments); 139 | }; 140 | } 141 | 142 | module.exports.registerCustomMethod = registerCustomMethod; 143 | module.exports.customMethodDecorator = customMethodDecorator; 144 | module.exports.getCustomMethods = getCustomMethods; 145 | module.exports.customMethodsHandler = customMethodsHandler; 146 | module.exports.customMethod = customMethod; 147 | module.exports.CUSTOM_METHOD = CUSTOM_METHOD; 148 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const keyWithArrayLogic = /(.+)\[(?:([+-])?|([+-])\d+)]$/; 4 | 5 | const unset = (object, path) => { 6 | const pathArray = _.toPath(path); 7 | const leavePath = pathArray.pop(); 8 | 9 | // remove from array 10 | if (pathArray.length) { 11 | const parent = _.get(object, pathArray); 12 | if (_.isArray(parent)) { 13 | return _.pullAt(parent, leavePath); 14 | } 15 | } 16 | 17 | return _.unset(object, path); 18 | }; 19 | 20 | exports.assignWithSet = (object, ...sources) => { 21 | while (sources.length) { 22 | const source = _.cloneDeep(sources.shift()); 23 | Object.entries(source).forEach(([key, value]) => { 24 | if (value === undefined) { 25 | return unset(object, key); 26 | } 27 | const regExpResult = keyWithArrayLogic.exec(key); 28 | 29 | if (regExpResult === null) { 30 | return _.set(object, key, value); 31 | } 32 | 33 | const keyWithoutArrayLogic = regExpResult[1]; 34 | if (regExpResult[2] === '-' || regExpResult[3] === '-') { 35 | // unshift 36 | const array = _.get(object, keyWithoutArrayLogic); 37 | _.set(object, keyWithoutArrayLogic, array ? [value, ...array] : [value]); 38 | } else { 39 | // push 40 | const array = _.get(object, keyWithoutArrayLogic); 41 | _.set(object, `${keyWithoutArrayLogic}[${array ? array.length : 0}]`, value); 42 | } 43 | }); 44 | } 45 | return object; 46 | }; 47 | 48 | exports.isPaginationEnabled = function (service) { 49 | return service.options && service.options.paginate && service.options.paginate.default; 50 | }; 51 | 52 | exports.checkImport = function checkImport (packageName, child) { 53 | try { 54 | const rest = require(packageName); 55 | /* __istanbul ignore else: for @feathers/express versions < 4 */ 56 | if (rest[child]) { 57 | return true; 58 | } 59 | } catch (e) {} 60 | /* __istanbul ignore next: for @feathers/express versions < 4 */ 61 | return false; 62 | }; 63 | 64 | exports.requireOrFail = function (packageName, reason = '') { 65 | let result; 66 | try { 67 | result = require(packageName); 68 | } catch (e) { 69 | throw new Error(`Package ${packageName} has to be installed ${reason}`); 70 | } 71 | return result; 72 | }; 73 | 74 | exports.isKoaApp = function (app) { 75 | return typeof app.request === 'object' && typeof app.request.query === 'object'; 76 | }; 77 | 78 | exports.isExpressApp = function (app) { 79 | return typeof app.request === 'object' && typeof app.request.app === 'function'; 80 | }; 81 | 82 | exports.versionCompare = (v1, v2) => v1.localeCompare(v2, undefined, { numeric: true, sensitivity: 'base' }); 83 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const OpenApiV2Generator = require('./v2/generator'); 2 | const OpenApiV3Generator = require('./v3/generator'); 3 | const utils = require('./utils'); 4 | const swaggerUI = require('./swagger-ui-dist'); 5 | const { customMethod, customMethodsHandler } = require('./custom-methods'); 6 | const { isExpressApp, isKoaApp } = require('./helpers'); 7 | 8 | const init = module.exports = function (config) { 9 | return function () { 10 | const app = this; 11 | 12 | const { 13 | ui, 14 | docsJsonPath = '/swagger.json', 15 | appProperty = 'docs', 16 | openApiVersion = 3 17 | } = config; 18 | 19 | const specs = {}; 20 | if (appProperty) { 21 | app[appProperty] = specs; 22 | } 23 | 24 | if (isExpressApp(app)) { 25 | app.get(docsJsonPath, (req, res) => { 26 | res.json(specs); 27 | }); 28 | } else if (isKoaApp(app)) { 29 | app.use(async (ctx, next) => { 30 | if (ctx.url === docsJsonPath) { 31 | ctx.body = specs; 32 | } 33 | return next(); 34 | }); 35 | } 36 | 37 | let specGenerator; 38 | if (openApiVersion === 2) { 39 | specGenerator = new OpenApiV2Generator(app, specs, config); 40 | } else if (openApiVersion === 3) { 41 | specGenerator = new OpenApiV3Generator(app, specs, config); 42 | } else { 43 | throw new Error(`Unsupported openApiVersion ${openApiVersion}! Allowed: 2, 3`); 44 | } 45 | 46 | // Register this plugin 47 | /* istanbul ignore else for feathers versions < 3 */ 48 | if ((app.version && parseInt(app.version, 10) >= 3) || Array.isArray(app.mixins)) { 49 | app.mixins.push(specGenerator.addService); 50 | } else { 51 | app.providers.push((path, service) => specGenerator.addService(service, path)); 52 | } 53 | 54 | // init UI module 55 | if (ui) { 56 | ui(app, { docsJsonPath, specs, openApiVersion }); 57 | } 58 | }; 59 | }; 60 | 61 | Object.assign(init, utils); 62 | init.swaggerUI = swaggerUI; 63 | init.customMethod = customMethod; 64 | init.customMethodsHandler = customMethodsHandler; 65 | -------------------------------------------------------------------------------- /lib/swagger-ui-dist.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { isKoaApp, isExpressApp, requireOrFail, versionCompare } = require('./helpers'); 4 | 5 | function getRedirectPrefix ({ req, ctx }) { 6 | const headers = (req && req.headers) || (ctx && ctx.headers) || {}; 7 | return headers['x-forwarded-prefix'] ? headers['x-forwarded-prefix'] : ''; 8 | } 9 | 10 | function generateSwaggerUIInitializerScript ({ docsJsonPath, ctx, req }) { 11 | const basePath = getRedirectPrefix({ req, ctx }); 12 | 13 | return ` 14 | window.onload = function() { 15 | window.ui = SwaggerUIBundle({ 16 | url: "${basePath}${docsJsonPath}", 17 | dom_id: '#swagger-ui', 18 | deepLinking: true, 19 | presets: [ 20 | SwaggerUIBundle.presets.apis, 21 | SwaggerUIStandalonePreset 22 | ], 23 | plugins: [ 24 | SwaggerUIBundle.plugins.DownloadUrl 25 | ], 26 | layout: "StandaloneLayout" 27 | }); 28 | }; 29 | `; 30 | } 31 | 32 | module.exports = function (options) { 33 | let swaggerUI, swaggerUIVersion; 34 | try { 35 | swaggerUI = require('swagger-ui-dist'); 36 | swaggerUIVersion = require('swagger-ui-dist/package.json').version; 37 | } catch (e) {} 38 | if (!swaggerUI || !swaggerUIVersion || versionCompare(swaggerUIVersion, '4.9.0') === -1) { 39 | throw new Error('swagger-ui-dist has to be installed in a version >= 4.9.0 when using feathers-swagger.swaggerUI function'); 40 | } 41 | 42 | const { docsPath = '/docs', indexFile, getSwaggerInitializerScript } = options || {}; 43 | if (docsPath[docsPath.length - 1] === '/') { 44 | throw new Error('swaggerUI.docsPath must not contain a trailing slash'); 45 | } 46 | 47 | let absoluteIndexFile = indexFile; 48 | if (indexFile && !path.isAbsolute(indexFile)) { 49 | absoluteIndexFile = path.join(process.cwd(), indexFile); 50 | } 51 | 52 | const swaggerInitializerPath = `${docsPath}/swagger-initializer.js`; 53 | 54 | return function initSwaggerUI (app, swaggerConfig) { 55 | const { docsJsonPath, specs } = swaggerConfig; 56 | 57 | if (isExpressApp(app)) { 58 | const express = require('express'); 59 | 60 | if (indexFile) { 61 | app.get(docsPath, function (req, res) { 62 | res.sendFile(absoluteIndexFile); 63 | }); 64 | } 65 | 66 | app.get(swaggerInitializerPath, function (req, res) { 67 | res.type('application/javascript'); 68 | res.send((getSwaggerInitializerScript || generateSwaggerUIInitializerScript)( 69 | { docsPath, docsJsonPath, specs, req, app }) 70 | ); 71 | }); 72 | 73 | app.get(docsPath, function (req, res, next) { 74 | if (req.url === docsPath) { 75 | res.redirect(getRedirectPrefix({ req }) + docsPath + '/'); 76 | } else { 77 | next(); 78 | } 79 | }); 80 | 81 | app.use(docsPath, express.static(swaggerUI.getAbsoluteFSPath())); 82 | } else if (isKoaApp(app)) { 83 | const serveStatic = requireOrFail('koa-static', 'to use Swagger UI with koa'); 84 | const koaMount = requireOrFail('koa-mount', 'to use Swagger UI with koa'); 85 | 86 | if (indexFile) { 87 | const koaSend = requireOrFail('koa-send', 'to use Swagger UI with koa'); 88 | const paths = [docsPath, `${docsPath}/`, `${docsPath}/index.html`]; 89 | let relativeFilePath = indexFile; 90 | const sendOptions = {}; 91 | relativeFilePath = path.basename(absoluteIndexFile); 92 | sendOptions.root = path.dirname(absoluteIndexFile); 93 | 94 | app.use(async (ctx, next) => { 95 | if (paths.includes(ctx.url)) { 96 | await koaSend(ctx, relativeFilePath, sendOptions); 97 | } else { 98 | await next(); 99 | } 100 | }); 101 | } 102 | 103 | app.use(async (ctx, next) => { 104 | if (ctx.url === swaggerInitializerPath) { 105 | ctx.type = 'application/javascript'; 106 | ctx.body = (getSwaggerInitializerScript || generateSwaggerUIInitializerScript)( 107 | { docsPath, docsJsonPath, specs, ctx, app } 108 | ); 109 | } else { 110 | return next(); 111 | } 112 | }); 113 | 114 | app.use(async (ctx, next) => { 115 | if (ctx.url === docsPath) { 116 | ctx.redirect(getRedirectPrefix({ ctx }) + docsPath + '/'); 117 | } else { 118 | return next(); 119 | } 120 | }); 121 | 122 | app.use(koaMount(docsPath, serveStatic(swaggerUI.getAbsoluteFSPath()))); 123 | } 124 | }; 125 | }; 126 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const { assignWithSet } = require('./helpers'); 2 | const { omit, pick } = require('lodash'); 3 | 4 | exports.tag = function tag (name, options = {}) { 5 | const result = { 6 | name, 7 | description: options.description || `A ${name} service` 8 | }; 9 | 10 | if (options.externalDocs) { 11 | result.externalDocs = options.externalDocs; 12 | } 13 | 14 | return result; 15 | }; 16 | 17 | const v2OperationDefaults = { 18 | parameters: [], 19 | responses: {}, 20 | description: '', 21 | summary: '', 22 | tags: [], 23 | consumes: [], 24 | produces: [], 25 | security: [] 26 | }; 27 | 28 | exports.operation = function operation (method, service, defaults = {}, specDefaults = v2OperationDefaults) { 29 | const operation = {}; 30 | if (service.docs && service.docs.operations) { 31 | Object.assign(operation, service.docs.operations.all || {}, service.docs.operations[method] || {}); 32 | } 33 | 34 | if (service[method] && service[method].docs) { 35 | Object.assign(operation, service[method].docs); 36 | } 37 | 38 | return assignWithSet({}, specDefaults, defaults, operation); 39 | }; 40 | 41 | exports.security = function security (method, securities, security) { 42 | if (securities.includes(method) || securities.includes('all')) { 43 | return security; 44 | } 45 | 46 | return []; 47 | }; 48 | 49 | exports.idPathParameters = function idPathParameters (idName, idSeparator) { 50 | return `{${Array.isArray(idName) ? idName.join(`}${idSeparator}{`) : idName}}`; 51 | }; 52 | 53 | const allowedDefaultRefs = [ 54 | 'findResponse', 55 | 'getResponse', 56 | 'createRequest', 57 | 'createResponse', 58 | 'createMultiRequest', 59 | 'createMultiResponse', 60 | 'updateRequest', 61 | 'updateResponse', 62 | 'updateMultiRequest', 63 | 'updateMultiResponse', 64 | 'patchRequest', 65 | 'patchResponse', 66 | 'patchMultiRequest', 67 | 'patchMultiResponse', 68 | 'removeResponse', 69 | 'removeMultiResponse', 70 | 'filterParameter', 71 | 'sortParameter', 72 | 'queryParameters' 73 | ]; 74 | 75 | exports.defaultTransformSchema = function defaultTransformSchema (schema) { 76 | const allowedProperties = pick(schema, [ 77 | 'title', 78 | 'multipleOf', 79 | 'maximum', 80 | 'exclusiveMaximum', 81 | 'minimum', 82 | 'exclusiveMinimum', 83 | 'maxLength', 84 | 'minLength', 85 | 'pattern', 86 | 'maxItems', 87 | 'minItems', 88 | 'uniqueItems', 89 | 'maxProperties', 90 | 'minProperties', 91 | 'required', 92 | 'dependentRequired', 93 | 'const', 94 | 'enum', 95 | 'type', 96 | 'allOf', 97 | 'oneOf', 98 | 'anyOf', 99 | 'not', 100 | 'items', 101 | 'properties', 102 | 'additionalProperties', 103 | 'description', 104 | 'format', 105 | 'default', 106 | 'nullable', 107 | 'discriminator', 108 | 'readOnly', 109 | 'writeOnly', 110 | 'xml', 111 | 'externalDocs', 112 | 'example', 113 | 'deprecated', 114 | '$ref', 115 | '$dynamicRef', 116 | '$anchor', 117 | '$dynamicAnchor', 118 | '$recursiveRef', 119 | '$recursiveAnchor', 120 | '$defs', 121 | '$comment' 122 | ]); 123 | 124 | if (allowedProperties.$ref && !allowedProperties.$ref.includes('#')) { 125 | allowedProperties.$ref = '#/components/schemas/' + allowedProperties.$ref; 126 | } 127 | 128 | if (allowedProperties.items) { 129 | allowedProperties.items = defaultTransformSchema(allowedProperties.items); 130 | } 131 | 132 | if (allowedProperties.properties) { 133 | allowedProperties.properties = Object.entries(allowedProperties.properties).reduce( 134 | (previousValue, [key, value]) => { 135 | return { 136 | ...previousValue, 137 | [key]: defaultTransformSchema(value) 138 | }; 139 | }, 140 | {} 141 | ); 142 | } 143 | 144 | return allowedProperties; 145 | }; 146 | 147 | function determineSchemaPrefix (schemas) { 148 | const dataSchemaKey = Object.keys(schemas).find(value => value.endsWith('DataSchema')); 149 | if (dataSchemaKey) { 150 | return dataSchemaKey.replace(/DataSchema$/, ''); 151 | } 152 | const querySchemaKey = Object.keys(schemas).find(value => value.endsWith('QuerySchema')); 153 | if (querySchemaKey) { 154 | return querySchemaKey.replace(/QuerySchema$/, ''); 155 | } 156 | const patchSchemaKey = Object.keys(schemas).find(value => value.endsWith('PatchSchema')); 157 | if (patchSchemaKey) { 158 | return patchSchemaKey.replace(/PatchSchema$/, ''); 159 | } 160 | const schemaKey = Object.keys(schemas).find(value => value.endsWith('Schema')); 161 | if (schemaKey) { 162 | return schemaKey.replace(/Schema$/, ''); 163 | } 164 | 165 | return undefined; 166 | } 167 | 168 | exports.createSwaggerServiceOptions = function createSwaggerServiceOptions ({ schemas, docs, transformSchema }) { 169 | const serviceDocs = { schemas: {}, refs: {} }; 170 | const transformSchemaFn = transformSchema || exports.defaultTransformSchema; 171 | 172 | let unspecificSchemas; 173 | const prefix = determineSchemaPrefix(schemas); 174 | if (prefix) { 175 | const resultSchemaKey = `${prefix}Schema`; 176 | const dataSchemaKey = `${prefix}DataSchema`; 177 | const querySchemaKey = `${prefix}QuerySchema`; 178 | const patchSchemaKey = `${prefix}PatchSchema`; 179 | if (schemas[resultSchemaKey]) { 180 | const { 181 | [dataSchemaKey]: dataSchema, 182 | [querySchemaKey]: baseQuerySchema, 183 | [resultSchemaKey]: resultSchema, 184 | [patchSchemaKey]: patchSchema, 185 | ...otherSchemas 186 | } = schemas; 187 | unspecificSchemas = otherSchemas; 188 | 189 | const baseSchemeName = resultSchema.$id || prefix; 190 | const listSchemaName = `${baseSchemeName}List`; 191 | 192 | serviceDocs.schemas = { 193 | [baseSchemeName]: transformSchemaFn(resultSchema), 194 | [listSchemaName]: { 195 | type: 'array', 196 | items: { $ref: `#/components/schemas/${baseSchemeName}` } 197 | } 198 | }; 199 | 200 | serviceDocs.refs = {}; 201 | 202 | if (dataSchema) { 203 | const dataSchemeName = dataSchema.$id || `${baseSchemeName}Data`; 204 | serviceDocs.schemas[dataSchemeName] = transformSchemaFn(dataSchema); 205 | serviceDocs.refs.createRequest = dataSchemeName; 206 | serviceDocs.refs.updateRequest = dataSchemeName; 207 | } 208 | 209 | if (baseQuerySchema) { 210 | const querySchema = { 211 | ...baseQuerySchema, 212 | properties: omit(baseQuerySchema.properties, ['$limit', '$skip']) 213 | }; 214 | const querySchemaName = querySchema.$id || `${baseSchemeName}Query`; 215 | serviceDocs.schemas[querySchemaName] = transformSchemaFn(querySchema); 216 | serviceDocs.refs.queryParameters = querySchemaName; 217 | } 218 | 219 | if (patchSchema) { 220 | const patchSchemaName = patchSchema.$id || `${baseSchemeName}PatchData`; 221 | serviceDocs.schemas[patchSchemaName] = transformSchemaFn(patchSchema); 222 | serviceDocs.refs.patchRequest = patchSchemaName; 223 | } 224 | 225 | if (!docs || !docs.model) { 226 | serviceDocs.model = baseSchemeName; 227 | } 228 | } 229 | } 230 | if (unspecificSchemas === undefined) { 231 | unspecificSchemas = schemas; 232 | } 233 | 234 | Object.entries(unspecificSchemas).forEach(([key, schema]) => { 235 | const schemaName = schema.$id; 236 | if (schemaName) { 237 | serviceDocs.schemas[schemaName] = transformSchemaFn(schema); 238 | if (allowedDefaultRefs.includes(key)) { 239 | serviceDocs.refs[key] = schemaName; 240 | } 241 | } 242 | }); 243 | 244 | if (docs) { 245 | const { schemas, definitions, refs, ...rest } = docs; 246 | 247 | if (schemas || definitions) { 248 | Object.assign(serviceDocs.schemas, schemas || definitions); 249 | } 250 | 251 | if (refs) { 252 | Object.assign(serviceDocs.refs, refs); 253 | } 254 | 255 | Object.assign(serviceDocs, rest); 256 | } 257 | 258 | return serviceDocs; 259 | }; 260 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-swagger", 3 | "description": "Add documentation to your Feathers services and feed them to Swagger UI.", 4 | "version": "3.0.0", 5 | "homepage": "https://feathersjs-ecosystem.github.io/feathers-swagger", 6 | "main": "lib/", 7 | "keywords": [ 8 | "feathers", 9 | "feathers-plugin" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/feathersjs-ecosystem/feathers-swagger.git" 15 | }, 16 | "author": { 17 | "name": "Feathers contributors", 18 | "email": "hello@feathersjs.com", 19 | "url": "https://feathersjs.com" 20 | }, 21 | "contributors": [], 22 | "bugs": { 23 | "url": "https://github.com/feathersjs-ecosystem/feathers-swagger/issues" 24 | }, 25 | "engines": { 26 | "node": ">= 14" 27 | }, 28 | "scripts": { 29 | "publish": "git push origin --tags", 30 | "release:patch": "npm version patch && npm publish", 31 | "release:minor": "npm version minor && npm publish", 32 | "release:major": "npm version major && npm publish", 33 | "release:prerelease": "npm version prerelease && npm publish --tag pre", 34 | "release:premajor": "npm version premajor && npm publish --tag pre", 35 | "changelog": "github_changelog_generator && git add docs/CHANGELOG.md && git commit -am \"chore: Update changelog\"", 36 | "lint": "semistandard --fix", 37 | "test:types": "tsd", 38 | "mocha": "mocha --reporter-option maxDiffSize=0", 39 | "coverage": "nyc --check-coverage npm run mocha", 40 | "test": "npm run lint && npm run coverage && npm run test:types", 41 | "test:ci": "npm run lint && npm run test:types && npm run coverage:ci", 42 | "test:ci:cleanup": "git reset HEAD package.json package-lock.json && npm ci", 43 | "coverage:ci": "npm run coverage:ci:v5 && npm run coverage:ci:v4 && nyc report", 44 | "coverage:ci:v5": "nyc --silent npm run mocha", 45 | "coverage:ci:v4": "npm run coverage:ci:v4:install && npm run coverage:ci:v4:nyc", 46 | "coverage:ci:v4:install": "npm rm @feathersjs/koa @koa/router koa-mount koa-static && npm install @feathersjs/feathers@^4 @feathersjs/express@^4", 47 | "coverage:ci:v4:nyc": "nyc --silent --no-clean npm run mocha", 48 | "start": "node example/app", 49 | "dev:docs": "docsify serve docs" 50 | }, 51 | "semistandard": { 52 | "sourceType": "module", 53 | "env": [ 54 | "mocha" 55 | ] 56 | }, 57 | "directories": { 58 | "lib": "lib" 59 | }, 60 | "types": "types/index.d.ts", 61 | "dependencies": { 62 | "lodash": "^4.17.21" 63 | }, 64 | "peerDependencies": { 65 | "swagger-ui-dist": ">=4.9.0", 66 | "koa-static": "^5.0.0", 67 | "koa-mount": "^4.0.0" 68 | }, 69 | "peerDependenciesMeta": { 70 | "swagger-ui-dist": { 71 | "optional": true 72 | }, 73 | "koa-static": { 74 | "optional": true 75 | }, 76 | "koa-mount": { 77 | "optional": true 78 | } 79 | }, 80 | "devDependencies": { 81 | "@feathersjs/express": "^5.0.12", 82 | "@feathersjs/feathers": "^5.0.12", 83 | "@feathersjs/koa": "^5.0.12", 84 | "@feathersjs/schema": "^5.0.12", 85 | "@feathersjs/typebox": "^5.0.12", 86 | "@koa/router": "^12.0.1", 87 | "@mairu/swagger-ui-apikey-auth-form": "^1.2.1", 88 | "@types/serve-static": "^1.13.10", 89 | "axios": "^1.6.2", 90 | "chai": "^4.3.10", 91 | "cors": "^2.8.5", 92 | "feathers-memory": "^4.1.0", 93 | "koa-mount": "^4.0.0", 94 | "koa-static": "^5.0.0", 95 | "mocha": "^10.2.0", 96 | "nyc": "^15.1.0", 97 | "proxyquire": "^2.1.3", 98 | "semistandard": "^17.0.0", 99 | "swagger-parser": "^10.0.3", 100 | "swagger-ui-dist": "^5.10.5", 101 | "tsd": "^0.30.0" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/custom-methods-v4.test.js: -------------------------------------------------------------------------------- 1 | const express = require('@feathersjs/express'); 2 | const axios = require('axios').default; 3 | const { expect } = require('chai'); 4 | 5 | const { feathers, startFeathersApp, isFeathers4 } = require('./helper'); 6 | const { customMethod } = require('../lib'); 7 | 8 | describe('feathers 4 custom http methods', () => { 9 | if (!isFeathers4) { 10 | return; // only valid with feathers 4 11 | } 12 | 13 | let appTeardown; 14 | 15 | const startFeathersWithService = (service) => { 16 | const app = express(feathers()) 17 | .use(express.json()) 18 | .use(express.urlencoded({ 19 | extended: true 20 | })) 21 | .configure(express.rest()) 22 | .use('/service', service); 23 | 24 | return startFeathersApp(app, 6776).then((res) => { appTeardown = res; }); 25 | }; 26 | 27 | afterEach(done => appTeardown(() => done())); 28 | 29 | it('handle simple post method', async () => { 30 | const customService = { 31 | async find (params) { 32 | return { queryParams: params.query }; 33 | }, 34 | getVersion: customMethod('POST', '/getVersion')(async (data, params) => { 35 | return { method: 'getVersion', data, queryParams: params.query, routeParams: params.route }; 36 | }) 37 | }; 38 | 39 | await startFeathersWithService(customService); 40 | 41 | const { data: responseContent } = await axios.post( 42 | 'http://localhost:6776/service/getVersion?param=abc', 43 | { 44 | example: 'super' 45 | } 46 | ); 47 | 48 | expect(responseContent).to.deep.equal({ 49 | method: 'getVersion', 50 | data: { example: 'super' }, 51 | queryParams: { param: 'abc' }, 52 | routeParams: {} 53 | }); 54 | }); 55 | 56 | it('handle get method with id', async () => { 57 | const customService = { 58 | async find (params) { 59 | return { queryParams: params.query }; 60 | }, 61 | getVersion: customMethod('GET', '/:customId/version')(async (data, params) => { 62 | return { method: 'getVersion', data, queryParams: params.query, routeParams: params.route }; 63 | }) 64 | }; 65 | 66 | await startFeathersWithService(customService); 67 | 68 | const { data: responseContent } = await axios.get('http://localhost:6776/service/5/version?param=abc'); 69 | 70 | expect(responseContent).to.deep.equal({ 71 | method: 'getVersion', 72 | data: {}, 73 | queryParams: { param: 'abc' }, 74 | routeParams: { customId: '5' } 75 | }); 76 | }); 77 | 78 | it('handle get method with feathers id', async () => { 79 | const customService = { 80 | async find (params) { 81 | return { queryParams: params.query }; 82 | }, 83 | getVersion: customMethod('GET', '/:__feathersId/version')(async (data, params, id) => { 84 | return { method: 'getVersion', data, id, queryParams: params.query, routeParams: params.route }; 85 | }) 86 | }; 87 | 88 | await startFeathersWithService(customService); 89 | 90 | const { data: responseContent } = await axios.get('http://localhost:6776/service/5/version?param=abc'); 91 | 92 | expect(responseContent).to.deep.equal({ 93 | method: 'getVersion', 94 | data: {}, 95 | id: '5', 96 | queryParams: { param: 'abc' }, 97 | routeParams: {} 98 | }); 99 | }); 100 | 101 | it('handle put method with id', async () => { 102 | const customService = { 103 | async find (params) { 104 | return { queryParams: params.query }; 105 | }, 106 | setVersion: customMethod('PUT', '/:customId/version')(async (data, params) => { 107 | return { method: 'setVersion', data, queryParams: params.query, routeParams: params.route }; 108 | }) 109 | }; 110 | 111 | await startFeathersWithService(customService); 112 | 113 | const { data: responseContent } = await axios.put( 114 | 'http://localhost:6776/service/5/version?param=abc', 115 | { example: 'put' } 116 | ); 117 | 118 | expect(responseContent).to.deep.equal({ 119 | method: 'setVersion', 120 | data: { example: 'put' }, 121 | queryParams: { param: 'abc' }, 122 | routeParams: { customId: '5' } 123 | }); 124 | }); 125 | 126 | it('handle patch method with id', async () => { 127 | const customService = { 128 | async find (params) { 129 | return { queryParams: params.query }; 130 | }, 131 | setVersion: customMethod('PATCH', '/:customId/version')(async (data, params) => { 132 | return { method: 'setVersion', data, queryParams: params.query, routeParams: params.route }; 133 | }) 134 | }; 135 | 136 | await startFeathersWithService(customService); 137 | 138 | const { data: responseContent } = await axios.patch( 139 | 'http://localhost:6776/service/5/version?param=abc', 140 | { example: 'patch' } 141 | ); 142 | 143 | expect(responseContent).to.deep.equal({ 144 | method: 'setVersion', 145 | data: { example: 'patch' }, 146 | queryParams: { param: 'abc' }, 147 | routeParams: { customId: '5' } 148 | }); 149 | }); 150 | 151 | it('handle delete method with id', async () => { 152 | const customService = { 153 | async find (params) { 154 | return { queryParams: params.query }; 155 | }, 156 | removeVersion: customMethod('DELETE', '/:customId/version')(async (data, params) => { 157 | return { method: 'removeVersion', data, queryParams: params.query, routeParams: params.route }; 158 | }) 159 | }; 160 | 161 | await startFeathersWithService(customService); 162 | 163 | const { data: responseContent } = await axios.delete( 164 | 'http://localhost:6776/service/5/version?param=abc' 165 | ); 166 | 167 | expect(responseContent).to.deep.equal({ 168 | method: 'removeVersion', 169 | data: {}, 170 | queryParams: { param: 'abc' }, 171 | routeParams: { customId: '5' } 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/custom-methods-v5.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const express = require('@feathersjs/express'); 3 | const axios = require('axios').default; 4 | const { expect } = require('chai'); 5 | 6 | let bodyParser; 7 | const { startFeathersApp, koaApp, expressApp, isFeathers4 } = require('./helper'); 8 | const swagger = require('../lib'); 9 | const proxyquire = require('proxyquire'); 10 | 11 | if (!isFeathers4) { 12 | bodyParser = require('@feathersjs/koa').bodyParser; 13 | } 14 | 15 | const { customMethodsHandler, customMethod } = swagger; 16 | 17 | const startFeathersWithService = (initApp, service, methods) => { 18 | const app = initApp() 19 | .configure(swagger({ 20 | specs: { 21 | info: { 22 | title: 'A test', 23 | description: 'A description', 24 | version: '1.0.0' 25 | } 26 | } 27 | })) 28 | .use('/service', service, { methods }); 29 | 30 | return startFeathersApp(app, 6776); 31 | }; 32 | 33 | describe('feathers 5 custom methods', () => { 34 | if (isFeathers4) { 35 | return; 36 | } 37 | 38 | Object.entries({ 39 | koa: { 40 | initApp: () => { 41 | return koaApp((app) => { 42 | app.use(bodyParser()) 43 | .configure(customMethodsHandler); 44 | }); 45 | } 46 | }, 47 | express: { 48 | initApp: () => { 49 | return expressApp((app) => { 50 | app.use(express.json()) 51 | .use(express.urlencoded({ 52 | extended: true 53 | })) 54 | .configure(customMethodsHandler) 55 | ; 56 | }); 57 | } 58 | } 59 | }).forEach(([type, options]) => { 60 | describe(`using customMethodsHandler with ${type}`, () => { 61 | const { initApp } = options; 62 | 63 | let appTeardown; 64 | 65 | afterEach(done => appTeardown(() => done())); 66 | 67 | it('should handle simple post method', async () => { 68 | const customService = { 69 | async find (params) { 70 | return { queryParams: params.query }; 71 | }, 72 | getVersion: customMethod('POST', '/getVersion')(async (data, params) => { 73 | return { method: 'getVersion', data, queryParams: params.query, routeParams: params.params }; 74 | }) 75 | }; 76 | 77 | appTeardown = await startFeathersWithService(initApp, customService, ['find', 'getVersion']); 78 | 79 | const { data: responseContent } = await axios.post( 80 | 'http://localhost:6776/service/getVersion?param=abc', 81 | { 82 | example: 'super' 83 | } 84 | ); 85 | 86 | expect(responseContent).to.deep.equal({ 87 | method: 'getVersion', 88 | data: { example: 'super' }, 89 | queryParams: { param: 'abc' }, 90 | routeParams: {} 91 | }); 92 | }); 93 | 94 | it('should handle get method with id', async () => { 95 | const customService = { 96 | async find (params) { 97 | return { queryParams: params.query }; 98 | }, 99 | getVersion: customMethod('GET', '/:customId/version')(async (data, params) => { 100 | return { method: 'getVersion', data, queryParams: params.query, routeParams: params.params }; 101 | }) 102 | }; 103 | 104 | appTeardown = await startFeathersWithService(initApp, customService, ['find', 'getVersion']); 105 | 106 | const { data: responseContent } = await axios.get('http://localhost:6776/service/5/version?param=abc'); 107 | 108 | expect(responseContent).to.deep.equal({ 109 | method: 'getVersion', 110 | data: {}, 111 | queryParams: { param: 'abc' }, 112 | routeParams: { customId: '5' } 113 | }); 114 | }); 115 | 116 | it('should handle get method with feathers id', async () => { 117 | const customService = { 118 | async find (params) { 119 | return { queryParams: params.query }; 120 | }, 121 | 122 | getVersion: customMethod('GET', '/:__id/version')(async (data, params) => { 123 | return { method: 'getVersion', data, queryParams: params.query, routeParams: params.params }; 124 | }) 125 | }; 126 | 127 | appTeardown = await startFeathersWithService(initApp, customService, ['find', 'getVersion']); 128 | 129 | const { data: responseContent } = await axios.get('http://localhost:6776/service/5/version?param=abc'); 130 | 131 | expect(responseContent).to.deep.equal({ 132 | method: 'getVersion', 133 | data: {}, 134 | queryParams: { param: 'abc' }, 135 | routeParams: { __id: '5' } 136 | }); 137 | }); 138 | 139 | it('should handle put method with id', async () => { 140 | const customService = { 141 | async find (params) { 142 | return { queryParams: params.query }; 143 | }, 144 | setVersion: customMethod('PUT', '/:customId/version')(async (data, params) => { 145 | return { method: 'setVersion', data, queryParams: params.query, routeParams: params.params }; 146 | }) 147 | }; 148 | 149 | appTeardown = await startFeathersWithService(initApp, customService, ['find', 'setVersion']); 150 | 151 | const { data: responseContent } = await axios.put( 152 | 'http://localhost:6776/service/5/version?param=abc', 153 | { example: 'put' } 154 | ); 155 | 156 | expect(responseContent).to.deep.equal({ 157 | method: 'setVersion', 158 | data: { example: 'put' }, 159 | queryParams: { param: 'abc' }, 160 | routeParams: { customId: '5' } 161 | }); 162 | }); 163 | 164 | it('should handle patch method with id', async () => { 165 | const customService = { 166 | async find (params) { 167 | return { queryParams: params.query }; 168 | }, 169 | setVersion: customMethod('PATCH', '/:customId/version')(async (data, params) => { 170 | return { method: 'setVersion', data, queryParams: params.query, routeParams: params.params }; 171 | }) 172 | }; 173 | 174 | appTeardown = await startFeathersWithService(initApp, customService, ['find', 'setVersion']); 175 | 176 | const { data: responseContent } = await axios.patch( 177 | 'http://localhost:6776/service/5/version?param=abc', 178 | { example: 'patch' } 179 | ); 180 | 181 | expect(responseContent).to.deep.equal({ 182 | method: 'setVersion', 183 | data: { example: 'patch' }, 184 | queryParams: { param: 'abc' }, 185 | routeParams: { customId: '5' } 186 | }); 187 | }); 188 | 189 | it('should handle delete method with id', async () => { 190 | const customService = { 191 | async find (params) { 192 | return { queryParams: params.query }; 193 | }, 194 | removeVersion: customMethod('DELETE', '/:customId/version')(async (data, params) => { 195 | return { method: 'removeVersion', data, queryParams: params.query, routeParams: params.params }; 196 | }) 197 | }; 198 | 199 | appTeardown = await startFeathersWithService(initApp, customService, ['find', 'removeVersion']); 200 | 201 | const { data: responseContent } = await axios.delete( 202 | 'http://localhost:6776/service/5/version?param=abc' 203 | ); 204 | 205 | expect(responseContent).to.deep.equal({ 206 | method: 'removeVersion', 207 | data: {}, 208 | queryParams: { param: 'abc' }, 209 | routeParams: { customId: '5' } 210 | }); 211 | }); 212 | }); 213 | }); 214 | 215 | describe('with missing dependencies', () => { 216 | it('customMethodsHandler with koa should fail', () => { 217 | const { customMethodsHandler } = proxyquire('../lib/custom-methods', { 218 | './helpers': proxyquire('../lib/helpers', { '@koa/router': null }) 219 | }); 220 | 221 | const app = koaApp(); 222 | expect(() => { 223 | app.configure(customMethodsHandler); 224 | }).to.throw(Error, 'Package @koa/router has to be installed to use the customMethodsHandler'); 225 | }); 226 | }); 227 | 228 | describe('without customMethodsHandler', () => { 229 | let appTeardown; 230 | let app; 231 | 232 | const initApp = () => { 233 | return koaApp((appArg) => { 234 | app = appArg; 235 | app.use(bodyParser()); 236 | }); 237 | }; 238 | 239 | afterEach(done => { appTeardown(() => { done(); app = undefined; }); }); 240 | 241 | it('should not fail and don\'t add custom methods to open api specs', async () => { 242 | const customService = { 243 | async find (params) { 244 | return { queryParams: params.query }; 245 | }, 246 | getVersion: customMethod('POST', '/getVersion')(async (data, params) => { 247 | return { method: 'getVersion', data, queryParams: params.query, routeParams: params.params }; 248 | }) 249 | }; 250 | 251 | appTeardown = await startFeathersWithService(initApp, customService, ['find', 'getVersion']); 252 | 253 | try { 254 | await axios.post( 255 | 'http://localhost:6776/service/getVersion?param=abc', 256 | { 257 | example: 'super' 258 | } 259 | ); 260 | expect.fail('an error response is expected'); 261 | } catch (e) { 262 | expect(e).to.be.instanceof(Error); 263 | } 264 | 265 | expect(app.docs.paths['/service/getVersion']).to.not.exist; 266 | }); 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /test/custom-methods.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const { expect } = require('chai'); 3 | 4 | const { customMethod } = require('../lib'); 5 | const { isFeathers4 } = require('./helper'); 6 | 7 | const isCustomMethod = (() => { 8 | if (isFeathers4) { 9 | const { HTTP_METHOD } = require('@feathersjs/express/rest'); 10 | 11 | return (method) => method[HTTP_METHOD] !== undefined; 12 | } 13 | 14 | const { CUSTOM_METHOD } = require('../lib/custom-methods'); 15 | 16 | return (method) => method[CUSTOM_METHOD] !== undefined; 17 | })(); 18 | 19 | describe('customMethod', () => { 20 | it('should work as wrapper function', () => { 21 | const fn = () => {}; 22 | const result = customMethod('POST', '/path')(fn); 23 | expect(result).to.eq(fn); 24 | expect(isCustomMethod(result)).to.be.true; 25 | }); 26 | 27 | it('should work as typescript decorator', () => { 28 | const service = { 29 | custom: () => {} 30 | }; 31 | 32 | expect(customMethod('POST', '/path')(service, 'custom', {})).to.not.exist; 33 | expect(isCustomMethod(service.custom)).to.be.true; 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const feathers = require('@feathersjs/feathers'); 3 | const express = require('@feathersjs/express'); 4 | const { customMethod } = require('../lib'); 5 | const { versionCompare } = require('../lib/helpers'); 6 | 7 | if (feathers.version && feathers.version[0] >= '5') { 8 | // feathers v5 9 | const { koa, rest } = require('@feathersjs/koa'); 10 | 11 | exports.feathers = feathers.feathers; 12 | 13 | /** 14 | * Start feathers app (compatibility layer to support feathers 4 and 5) 15 | * @param {object} app 16 | * @param {number} port 17 | * @return {Promise} appShutdown 18 | */ 19 | exports.startFeathersApp = async function (app, port) { 20 | await app.listen(port); 21 | return (done) => { 22 | app.teardown().then(done); 23 | }; 24 | }; 25 | 26 | const { SERVICE } = feathers; 27 | 28 | /** 29 | * Add a custom method and emulate registration by feathers 30 | * @param {object} service 31 | * @param {object} [options] 32 | * @param {string} [options.name] 33 | * @param {string} [options.httpMethod] 34 | * @param {string} [options.path] 35 | * @param {string[]} [options.hookParams] 36 | */ 37 | exports.addCustomMethod = (service, options = {}) => { 38 | _.defaults(options, { name: 'customMethod', httpMethod: 'POST', path: '/custom' }); 39 | const { name, path, httpMethod } = options; 40 | 41 | service[name] = customMethod(httpMethod, path)(function () {}); 42 | service[SERVICE] = { methods: [name] }; 43 | }; 44 | 45 | exports.koaApp = (modifierFn) => { 46 | const app = koa(exports.feathers()); 47 | if (typeof modifierFn === 'function') { 48 | modifierFn(app); 49 | } 50 | app.configure(rest()); 51 | return app; 52 | }; 53 | } else { 54 | // feathers v4 55 | 56 | exports.feathers = feathers; 57 | 58 | /** 59 | * Start feathers app (compatibility layer to support feathers 4 and 5) 60 | * @param {object} app 61 | * @param {number} port 62 | * @return {Promise} server 63 | */ 64 | exports.startFeathersApp = async function (app, port) { 65 | return new Promise(resolve => { 66 | const server = app.listen(port, () => resolve((done) => { 67 | server.close(() => { done(); }); 68 | })); 69 | }); 70 | }; 71 | 72 | const { activateHooks } = feathers; 73 | const { httpMethod } = require('@feathersjs/express/rest'); 74 | 75 | /** 76 | * Add a custom method and emulate registration by feathers 77 | * @param {object} service 78 | * @param {object} [options] 79 | * @param {string} [options.name] 80 | * @param {string} [options.httpMethod] 81 | * @param {string} [options.path] 82 | * @param {string[]} [options.hookParams] 83 | */ 84 | exports.addCustomMethod = (service, options = {}) => { 85 | _.defaults(options, { name: 'customMethod', httpMethod: 'POST', path: '/custom', hookParams: ['data', 'params'] }); 86 | const { name, hookParams, path, httpMethod: method } = options; 87 | 88 | service[name] = httpMethod(method, path)(activateHooks(hookParams)(function () {})); 89 | service.methods = Object.assign(service.methods || {}, { 90 | [name]: hookParams 91 | }); 92 | }; 93 | } 94 | 95 | exports.expressApp = (modifierFn) => { 96 | const app = express(exports.feathers()); 97 | if (typeof modifierFn === 'function') { 98 | modifierFn(app); 99 | } 100 | app.configure(express.rest()); 101 | return app; 102 | }; 103 | 104 | exports.isFeathers4 = versionCompare(exports.feathers.version, '4.99') === -1; 105 | -------------------------------------------------------------------------------- /test/helpers.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { assignWithSet } = require('../lib/helpers'); 3 | 4 | describe('helper tests', () => { 5 | describe('assignWithSet', () => { 6 | it('works with multiple sources', () => { 7 | const object = { 8 | a: 1, 9 | b: { 10 | complete: 'replacement' 11 | }, 12 | c: { 13 | partial: 'replacement', 14 | of: 'subkey', 15 | remove: 'please' 16 | }, 17 | d: ['also', 'for', 'arrays'], 18 | e: 'untouched', 19 | f: 'to be removed' 20 | }; 21 | const source = { 22 | a: 2, 23 | b: { now: 'different' }, 24 | 'c.of': 'subkey replaced', 25 | 'c.added': 'subkey added', 26 | 'c.remove': undefined, 27 | 'd[2]': 'Arrays', 28 | 'd[3].test': 'nested object value', 29 | 'g[]': 'appended to new' 30 | }; 31 | const source2 = { 32 | 'd[3].test': 'second is the winner', 33 | 'd[3].test2': 'nested object value', 34 | 'd[]': 'appended to existing', 35 | f: undefined, 36 | 'd[-]': 'prepended' 37 | }; 38 | const expectedResult = { 39 | a: 2, 40 | b: { 41 | now: 'different' 42 | }, 43 | c: { 44 | partial: 'replacement', 45 | of: 'subkey replaced', 46 | added: 'subkey added' 47 | }, 48 | d: ['prepended', 'also', 'for', 'Arrays', { test: 'second is the winner', test2: 'nested object value' }, 'appended to existing'], 49 | e: 'untouched', 50 | g: ['appended to new'] 51 | }; 52 | 53 | const result = assignWithSet(object, source, source2); 54 | 55 | expect(result).to.equal(object); 56 | expect(result).to.deep.equal(expectedResult); 57 | }); 58 | 59 | it('can unset object paths', () => { 60 | const object = { 61 | a: { 62 | b: '1', 63 | c: '2' 64 | } 65 | }; 66 | 67 | const source = { 68 | 'a.b': undefined 69 | }; 70 | 71 | const expectedResult = { 72 | a: { 73 | c: '2' 74 | } 75 | }; 76 | 77 | const result = assignWithSet(object, source); 78 | 79 | expect(result).to.equal(object); 80 | expect(result).to.deep.equal(expectedResult); 81 | }); 82 | 83 | it('can unset array paths', () => { 84 | const object = { 85 | a: [ 86 | '1', 87 | '2', 88 | '3' 89 | ] 90 | }; 91 | 92 | const source = { 93 | 'a[1]': undefined 94 | }; 95 | 96 | const expectedResult = { 97 | a: [ 98 | '1', 99 | '3' 100 | ] 101 | }; 102 | 103 | const result = assignWithSet(object, source); 104 | 105 | expect(result).to.equal(object); 106 | expect(result).to.deep.equal(expectedResult); 107 | }); 108 | 109 | it('can push to arrays', () => { 110 | const object = { 111 | a: [ 112 | '1', 113 | '2', 114 | '3' 115 | ] 116 | }; 117 | 118 | const source = { 119 | 'a[]': '4', 120 | 'a[+]': '5', 121 | 'a[+1]': '6', 122 | 'a[+2]': '7' 123 | }; 124 | 125 | const expectedResult = { 126 | a: [ 127 | '1', 128 | '2', 129 | '3', 130 | '4', 131 | '5', 132 | '6', 133 | '7' 134 | ] 135 | }; 136 | 137 | const result = assignWithSet(object, source); 138 | 139 | expect(result).to.equal(object); 140 | expect(result).to.deep.equal(expectedResult); 141 | }); 142 | 143 | it('can unshift to arrays', () => { 144 | const object = { 145 | a: [ 146 | '4', 147 | '5' 148 | ] 149 | }; 150 | 151 | const source = { 152 | 'a[-]': '3', 153 | 'a[-1]': '2', 154 | 'a[-2]': '1', 155 | 'b[-]': 4 156 | }; 157 | 158 | const expectedResult = { 159 | a: [ 160 | '1', 161 | '2', 162 | '3', 163 | '4', 164 | '5' 165 | ], 166 | b: [4] 167 | }; 168 | 169 | const result = assignWithSet(object, source); 170 | 171 | expect(result).to.equal(object); 172 | expect(result).to.deep.equal(expectedResult); 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | const { expect } = require('chai'); 3 | 4 | const axios = require('axios').default; 5 | const memory = require('feathers-memory'); 6 | const SwaggerParser = require('swagger-parser'); 7 | const swagger = require('../lib'); 8 | const { feathers, startFeathersApp, koaApp, expressApp, isFeathers4 } = require('./helper'); 9 | 10 | describe('feathers-swagger', () => { 11 | Object.entries({ 12 | koa: { initApp: koaApp }, 13 | express: { initApp: expressApp } 14 | }).forEach(([type, options]) => { 15 | if (type === 'koa' && isFeathers4) { 16 | return; 17 | } 18 | 19 | const { initApp } = options; 20 | 21 | describe(`should serve openapi json file with ${type}`, () => { 22 | let appTeardown; 23 | let app; 24 | 25 | const startApp = (swaggerConfig) => { 26 | app = initApp(); 27 | 28 | app.configure(swagger({ 29 | specs: { 30 | info: { 31 | title: 'A test', 32 | description: 'A description', 33 | version: '1.0.0' 34 | } 35 | }, 36 | ...swaggerConfig 37 | })); 38 | 39 | return startFeathersApp(app, 6776).then((res) => { appTeardown = res; }); 40 | }; 41 | 42 | afterEach(done => appTeardown(() => { done(); appTeardown = undefined; })); 43 | 44 | it('default as /swagger.json', async () => { 45 | await startApp({}); 46 | 47 | expect((await axios.get('http://localhost:6776/swagger.json')).data).to.deep.equal(app.docs); 48 | }); 49 | 50 | it('use `docsJsonPath` as /docs.json', async () => { 51 | await startApp({ docsJsonPath: '/docs.json' }); 52 | 53 | expect((await axios.get('http://localhost:6776/docs.json')).data).to.deep.equal(app.docs); 54 | await axios.get('http://localhost:6776/swagger.json') 55 | .catch(error => expect(error.response).to.have.property('status', 404)); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('basic functionality with openapi v2', () => { 61 | let app; 62 | 63 | before(() => { 64 | const messageService = memory(); 65 | messageService.docs = { 66 | description: 'A service to send and receive messages', 67 | definition: { 68 | type: 'object', 69 | required: [ 70 | 'text' 71 | ], 72 | properties: { 73 | text: { 74 | type: 'string', 75 | description: 'The message text' 76 | }, 77 | userId: { 78 | type: 'string', 79 | description: 'The id of the user that sent the message' 80 | } 81 | } 82 | } 83 | }; 84 | 85 | app = feathers() 86 | .configure( 87 | swagger({ 88 | openApiVersion: 2, 89 | idType: 'string', 90 | specs: { 91 | info: { 92 | title: 'A test', 93 | description: 'A description', 94 | version: '1.0.0' 95 | } 96 | } 97 | }) 98 | ) 99 | .use('/messages', messageService); 100 | }); 101 | 102 | it('supports basic functionality with a simple app', () => { 103 | expect(app.docs.swagger).to.equal('2.0'); 104 | expect(app.docs.info.title).to.equal('A test'); 105 | expect(app.docs.info.description).to.equal('A description'); 106 | expect(app.docs.paths['/messages']).to.exist; 107 | }); 108 | 109 | it('supports id types in config', () => { 110 | const messagesIdParam = app.docs.paths['/messages/{id}'].get.parameters[0]; 111 | expect(messagesIdParam.type).to.equal('string'); 112 | }); 113 | 114 | it('check swagger document validity', () => { 115 | SwaggerParser.validate(app.docs) 116 | .then(api => { 117 | expect(api).to.exist; 118 | }) 119 | .catch(error => { 120 | expect.fail(`${error.message}\n\nJSON:\n${JSON.stringify(app.docs, undefined, 2)}`); 121 | }); 122 | }); 123 | }); 124 | 125 | describe('basic functionality with openapi v3', () => { 126 | let app; 127 | 128 | before(() => { 129 | const messageService = memory(); 130 | messageService.docs = { 131 | description: 'A service to send and receive messages', 132 | definition: { 133 | type: 'object', 134 | required: [ 135 | 'text' 136 | ], 137 | properties: { 138 | text: { 139 | type: 'string', 140 | description: 'The message text' 141 | }, 142 | userId: { 143 | type: 'string', 144 | description: 'The id of the user that sent the message' 145 | } 146 | } 147 | } 148 | }; 149 | 150 | app = feathers() 151 | .configure( 152 | swagger({ 153 | idType: 'string', 154 | specs: { 155 | info: { 156 | title: 'A test', 157 | description: 'A description', 158 | version: '1.0.0' 159 | } 160 | } 161 | }) 162 | ) 163 | .use('/messages', messageService); 164 | }); 165 | 166 | it('serves json specification at docJsonPath', () => { 167 | expect(app.docs.openapi).to.equal('3.0.3'); 168 | expect(app.docs.info.title).to.equal('A test'); 169 | expect(app.docs.info.description).to.equal('A description'); 170 | expect(app.docs.paths['/messages']).to.exist; 171 | }); 172 | 173 | it('supports id types in config', () => { 174 | const messagesIdParam = app.docs.paths['/messages/{id}'].get.parameters[0]; 175 | expect(messagesIdParam.schema.type).to.equal('string'); 176 | }); 177 | 178 | it('check swagger document validity', () => { 179 | return SwaggerParser.validate(app.docs) 180 | .then(api => { 181 | expect(api).to.exist; 182 | }) 183 | .catch(error => { 184 | expect.fail(`${error.message}\n\nJSON:\n${JSON.stringify(app.docs, undefined, 2)}`); 185 | }); 186 | }); 187 | }); 188 | 189 | it('fails with invalid openApiVersion', () => { 190 | const app = feathers(); 191 | expect(() => app.configure(swagger({ openApiVersion: 1 }))) 192 | .to.throw(Error, 'Unsupported openApiVersion 1! Allowed: 2, 3'); 193 | }); 194 | 195 | describe('appProperty', () => { 196 | const specs = { 197 | info: { 198 | title: 'A test', 199 | description: 'A description', 200 | version: '1.0.0' 201 | } 202 | }; 203 | 204 | it('should be docs by default', () => { 205 | const app = feathers() 206 | .configure(swagger({ specs })); 207 | 208 | expect(app.docs.info).to.deep.equal(specs.info); 209 | }); 210 | 211 | it('can be specified', () => { 212 | const app = feathers() 213 | .configure(swagger({ specs, appProperty: 'swaggerSpecs' })); 214 | 215 | expect(app.swaggerSpecs.info).to.deep.equal(specs.info); 216 | expect(app.docs).to.not.exist; 217 | }); 218 | 219 | it('can be disabled', () => { 220 | const app = feathers() 221 | .configure(swagger({ specs, appProperty: '' })); 222 | 223 | expect(app.docs).to.not.exist; 224 | }); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /test/v2/expected-memory-spec-multi-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "title": "swagger generator v2 tests", 4 | "version": "1.0.0" 5 | }, 6 | "paths": { 7 | "/message": { 8 | "put": { 9 | "parameters": [ 10 | { 11 | "in": "body", 12 | "name": "body", 13 | "required": true, 14 | "schema": { 15 | "$ref": "#/definitions/message_list" 16 | } 17 | } 18 | ], 19 | "responses": { 20 | "200": { 21 | "description": "success", 22 | "schema": { 23 | "$ref": "#/definitions/message_list" 24 | } 25 | }, 26 | "401": { 27 | "description": "not authenticated" 28 | }, 29 | "500": { 30 | "description": "general error" 31 | } 32 | }, 33 | "description": "Updates multiple resources.", 34 | "summary": "", 35 | "tags": [ 36 | "message" 37 | ], 38 | "consumes": [ 39 | "application/json" 40 | ], 41 | "produces": [ 42 | "application/json" 43 | ], 44 | "security": [] 45 | }, 46 | "patch": { 47 | "parameters": [ 48 | { 49 | "in": "body", 50 | "name": "body", 51 | "required": true, 52 | "schema": { 53 | "$ref": "#/definitions/message" 54 | } 55 | } 56 | ], 57 | "responses": { 58 | "200": { 59 | "description": "success", 60 | "schema": { 61 | "$ref": "#/definitions/message_list" 62 | } 63 | }, 64 | "401": { 65 | "description": "not authenticated" 66 | }, 67 | "500": { 68 | "description": "general error" 69 | } 70 | }, 71 | "description": "Updates multiple resources queried by given filters.", 72 | "summary": "", 73 | "tags": [ 74 | "message" 75 | ], 76 | "consumes": [ 77 | "application/json" 78 | ], 79 | "produces": [ 80 | "application/json" 81 | ], 82 | "security": [] 83 | }, 84 | "delete": { 85 | "parameters": [], 86 | "responses": { 87 | "200": { 88 | "description": "success", 89 | "schema": { 90 | "$ref": "#/definitions/message_list" 91 | } 92 | }, 93 | "401": { 94 | "description": "not authenticated" 95 | }, 96 | "500": { 97 | "description": "general error" 98 | } 99 | }, 100 | "description": "Removes multiple resources queried by given filters.", 101 | "summary": "", 102 | "tags": [ 103 | "message" 104 | ], 105 | "consumes": [ 106 | "application/json" 107 | ], 108 | "produces": [ 109 | "application/json" 110 | ], 111 | "security": [] 112 | } 113 | } 114 | }, 115 | "definitions": { 116 | "message": { 117 | "type": "object", 118 | "properties": { 119 | "content": { 120 | "type": "string" 121 | } 122 | } 123 | }, 124 | "message_list": { 125 | "title": "message list", 126 | "type": "array", 127 | "items": { 128 | "$ref": "#/definitions/message" 129 | } 130 | } 131 | }, 132 | "swagger": "2.0", 133 | "schemes": [ 134 | "http" 135 | ], 136 | "tags": [ 137 | { 138 | "name": "message", 139 | "description": "A message service" 140 | } 141 | ], 142 | "basePath": "/", 143 | "consumes": [ 144 | "application/json" 145 | ], 146 | "produces": [ 147 | "application/json" 148 | ] 149 | } 150 | -------------------------------------------------------------------------------- /test/v2/expected-memory-spec-pagination-find.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "title": "swagger generator v2 tests", 4 | "version": "1.0.0" 5 | }, 6 | "paths": { 7 | "/message": { 8 | "get": { 9 | "parameters": [ 10 | { 11 | "description": "Number of results to return", 12 | "in": "query", 13 | "name": "$limit", 14 | "type": "integer" 15 | }, 16 | { 17 | "description": "Number of results to skip", 18 | "in": "query", 19 | "name": "$skip", 20 | "type": "integer" 21 | }, 22 | { 23 | "description": "Property to sort results", 24 | "in": "query", 25 | "name": "$sort", 26 | "type": "string" 27 | } 28 | ], 29 | "responses": { 30 | "200": { 31 | "description": "success", 32 | "schema": { 33 | "$ref": "#/definitions/message_pagination" 34 | } 35 | }, 36 | "401": { 37 | "description": "not authenticated" 38 | }, 39 | "500": { 40 | "description": "general error" 41 | } 42 | }, 43 | "description": "Retrieves a list of all resources from the service.", 44 | "summary": "", 45 | "tags": [ 46 | "message" 47 | ], 48 | "consumes": [ 49 | "application/json" 50 | ], 51 | "produces": [ 52 | "application/json" 53 | ], 54 | "security": [] 55 | } 56 | } 57 | }, 58 | "definitions": { 59 | "message": { 60 | "type": "object", 61 | "properties": { 62 | "content": { 63 | "type": "string" 64 | } 65 | } 66 | }, 67 | "message_list": { 68 | "title": "message list", 69 | "type": "array", 70 | "items": { 71 | "$ref": "#/definitions/message" 72 | } 73 | }, 74 | "message_pagination": { 75 | "title": "message pagination result", 76 | "type": "object", 77 | "properties": { 78 | "total": { 79 | "type": "integer" 80 | }, 81 | "limit": { 82 | "type": "integer" 83 | }, 84 | "skip": { 85 | "type": "integer" 86 | }, 87 | "data": { 88 | "$ref": "#/definitions/message_list" 89 | } 90 | } 91 | } 92 | }, 93 | "swagger": "2.0", 94 | "schemes": [ 95 | "http" 96 | ], 97 | "tags": [ 98 | { 99 | "name": "message", 100 | "description": "A message service" 101 | } 102 | ], 103 | "basePath": "/", 104 | "consumes": [ 105 | "application/json" 106 | ], 107 | "produces": [ 108 | "application/json" 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /test/v2/expected-memory-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "title": "swagger generator v2 tests", 4 | "version": "1.0.0" 5 | }, 6 | "paths": { 7 | "/message": { 8 | "get": { 9 | "parameters": [ 10 | { 11 | "description": "Number of results to return", 12 | "in": "query", 13 | "name": "$limit", 14 | "type": "integer" 15 | }, 16 | { 17 | "description": "Number of results to skip", 18 | "in": "query", 19 | "name": "$skip", 20 | "type": "integer" 21 | }, 22 | { 23 | "description": "Property to sort results", 24 | "in": "query", 25 | "name": "$sort", 26 | "type": "string" 27 | } 28 | ], 29 | "responses": { 30 | "200": { 31 | "description": "success", 32 | "schema": { 33 | "$ref": "#/definitions/message_list" 34 | } 35 | }, 36 | "401": { 37 | "description": "not authenticated" 38 | }, 39 | "500": { 40 | "description": "general error" 41 | } 42 | }, 43 | "description": "Retrieves a list of all resources from the service.", 44 | "summary": "", 45 | "tags": [ 46 | "message" 47 | ], 48 | "consumes": [ 49 | "application/json" 50 | ], 51 | "produces": [ 52 | "application/json" 53 | ], 54 | "security": [] 55 | }, 56 | "post": { 57 | "parameters": [ 58 | { 59 | "in": "body", 60 | "name": "body", 61 | "required": true, 62 | "schema": { 63 | "$ref": "#/definitions/message" 64 | } 65 | } 66 | ], 67 | "responses": { 68 | "201": { 69 | "description": "created", 70 | "schema": { 71 | "$ref": "#/definitions/message" 72 | } 73 | }, 74 | "401": { 75 | "description": "not authenticated" 76 | }, 77 | "500": { 78 | "description": "general error" 79 | } 80 | }, 81 | "description": "Creates a new resource with data.", 82 | "summary": "", 83 | "tags": [ 84 | "message" 85 | ], 86 | "consumes": [ 87 | "application/json" 88 | ], 89 | "produces": [ 90 | "application/json" 91 | ], 92 | "security": [] 93 | } 94 | }, 95 | "/message/{id}": { 96 | "get": { 97 | "parameters": [ 98 | { 99 | "description": "ID of message to return", 100 | "in": "path", 101 | "required": true, 102 | "name": "id", 103 | "type": "integer" 104 | } 105 | ], 106 | "responses": { 107 | "200": { 108 | "description": "success", 109 | "schema": { 110 | "$ref": "#/definitions/message" 111 | } 112 | }, 113 | "401": { 114 | "description": "not authenticated" 115 | }, 116 | "404": { 117 | "description": "not found" 118 | }, 119 | "500": { 120 | "description": "general error" 121 | } 122 | }, 123 | "description": "Retrieves a single resource with the given id from the service.", 124 | "summary": "", 125 | "tags": [ 126 | "message" 127 | ], 128 | "consumes": [ 129 | "application/json" 130 | ], 131 | "produces": [ 132 | "application/json" 133 | ], 134 | "security": [] 135 | }, 136 | "put": { 137 | "parameters": [ 138 | { 139 | "description": "ID of message to return", 140 | "in": "path", 141 | "required": true, 142 | "name": "id", 143 | "type": "integer" 144 | }, 145 | { 146 | "in": "body", 147 | "name": "body", 148 | "required": true, 149 | "schema": { 150 | "$ref": "#/definitions/message" 151 | } 152 | } 153 | ], 154 | "responses": { 155 | "200": { 156 | "description": "success", 157 | "schema": { 158 | "$ref": "#/definitions/message" 159 | } 160 | }, 161 | "401": { 162 | "description": "not authenticated" 163 | }, 164 | "404": { 165 | "description": "not found" 166 | }, 167 | "500": { 168 | "description": "general error" 169 | } 170 | }, 171 | "description": "Updates the resource identified by id using data.", 172 | "summary": "", 173 | "tags": [ 174 | "message" 175 | ], 176 | "consumes": [ 177 | "application/json" 178 | ], 179 | "produces": [ 180 | "application/json" 181 | ], 182 | "security": [] 183 | }, 184 | "patch": { 185 | "parameters": [ 186 | { 187 | "description": "ID of message to update", 188 | "in": "path", 189 | "required": true, 190 | "name": "id", 191 | "type": "integer" 192 | }, 193 | { 194 | "in": "body", 195 | "name": "body", 196 | "required": true, 197 | "schema": { 198 | "$ref": "#/definitions/message" 199 | } 200 | } 201 | ], 202 | "responses": { 203 | "200": { 204 | "description": "success", 205 | "schema": { 206 | "$ref": "#/definitions/message" 207 | } 208 | }, 209 | "401": { 210 | "description": "not authenticated" 211 | }, 212 | "404": { 213 | "description": "not found" 214 | }, 215 | "500": { 216 | "description": "general error" 217 | } 218 | }, 219 | "description": "Updates the resource identified by id using data.", 220 | "summary": "", 221 | "tags": [ 222 | "message" 223 | ], 224 | "consumes": [ 225 | "application/json" 226 | ], 227 | "produces": [ 228 | "application/json" 229 | ], 230 | "security": [] 231 | }, 232 | "delete": { 233 | "parameters": [ 234 | { 235 | "description": "ID of message to remove", 236 | "in": "path", 237 | "required": true, 238 | "name": "id", 239 | "type": "integer" 240 | } 241 | ], 242 | "responses": { 243 | "200": { 244 | "description": "success", 245 | "schema": { 246 | "$ref": "#/definitions/message" 247 | } 248 | }, 249 | "401": { 250 | "description": "not authenticated" 251 | }, 252 | "404": { 253 | "description": "not found" 254 | }, 255 | "500": { 256 | "description": "general error" 257 | } 258 | }, 259 | "description": "Removes the resource with id.", 260 | "summary": "", 261 | "tags": [ 262 | "message" 263 | ], 264 | "consumes": [ 265 | "application/json" 266 | ], 267 | "produces": [ 268 | "application/json" 269 | ], 270 | "security": [] 271 | } 272 | } 273 | }, 274 | "definitions": { 275 | "message": { 276 | "type": "object", 277 | "properties": { 278 | "content": { 279 | "type": "string" 280 | } 281 | } 282 | }, 283 | "message_list": { 284 | "title": "message list", 285 | "type": "array", 286 | "items": { 287 | "$ref": "#/definitions/message" 288 | } 289 | } 290 | }, 291 | "swagger": "2.0", 292 | "schemes": [ 293 | "http" 294 | ], 295 | "tags": [ 296 | { 297 | "name": "message", 298 | "description": "A message service" 299 | } 300 | ], 301 | "basePath": "/", 302 | "consumes": [ 303 | "application/json" 304 | ], 305 | "produces": [ 306 | "application/json" 307 | ] 308 | } 309 | -------------------------------------------------------------------------------- /test/v3/expected-memory-spec-multi-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "title": "openapi generator v3 tests", 4 | "version": "1.0.0" 5 | }, 6 | "paths": { 7 | "/message": { 8 | "post": { 9 | "description": "Creates a new resource with data.", 10 | "parameters": [], 11 | "requestBody": { 12 | "content": { 13 | "application/json": { 14 | "schema": { 15 | "oneOf": [ 16 | { 17 | "$ref": "#/components/schemas/message" 18 | }, 19 | { 20 | "$ref": "#/components/schemas/messageList" 21 | } 22 | ] 23 | } 24 | } 25 | }, 26 | "required": true 27 | }, 28 | "responses": { 29 | "201": { 30 | "content": { 31 | "application/json": { 32 | "schema": { 33 | "oneOf": [ 34 | { 35 | "$ref": "#/components/schemas/message" 36 | }, 37 | { 38 | "$ref": "#/components/schemas/messageList" 39 | } 40 | ] 41 | } 42 | } 43 | }, 44 | "description": "created" 45 | }, 46 | "401": { 47 | "description": "not authenticated" 48 | }, 49 | "500": { 50 | "description": "general error" 51 | } 52 | }, 53 | "security": [], 54 | "summary": "", 55 | "tags": [ 56 | "message" 57 | ] 58 | }, 59 | "put": { 60 | "parameters": [], 61 | "responses": { 62 | "200": { 63 | "description": "success", 64 | "content": { 65 | "application/json": { 66 | "schema": { 67 | "$ref": "#/components/schemas/messageList" 68 | } 69 | } 70 | } 71 | }, 72 | "401": { 73 | "description": "not authenticated" 74 | }, 75 | "500": { 76 | "description": "general error" 77 | } 78 | }, 79 | "description": "Updates multiple resources.", 80 | "summary": "", 81 | "tags": [ 82 | "message" 83 | ], 84 | "security": [], 85 | "requestBody": { 86 | "required": true, 87 | "content": { 88 | "application/json": { 89 | "schema": { 90 | "$ref": "#/components/schemas/messageList" 91 | } 92 | } 93 | } 94 | } 95 | }, 96 | "patch": { 97 | "parameters": [ 98 | { 99 | "description": "Query parameters to filter", 100 | "in": "query", 101 | "name": "filter", 102 | "style": "form", 103 | "explode": true, 104 | "schema": { 105 | "$ref": "#/components/schemas/message" 106 | } 107 | } 108 | ], 109 | "responses": { 110 | "200": { 111 | "description": "success", 112 | "content": { 113 | "application/json": { 114 | "schema": { 115 | "$ref": "#/components/schemas/messageList" 116 | } 117 | } 118 | } 119 | }, 120 | "401": { 121 | "description": "not authenticated" 122 | }, 123 | "500": { 124 | "description": "general error" 125 | } 126 | }, 127 | "description": "Updates multiple resources queried by given filters.", 128 | "summary": "", 129 | "tags": [ 130 | "message" 131 | ], 132 | "security": [], 133 | "requestBody": { 134 | "required": true, 135 | "content": { 136 | "application/json": { 137 | "schema": { 138 | "$ref": "#/components/schemas/message" 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "delete": { 145 | "parameters": [ 146 | { 147 | "description": "Query parameters to filter", 148 | "in": "query", 149 | "name": "filter", 150 | "style": "form", 151 | "explode": true, 152 | "schema": { 153 | "$ref": "#/components/schemas/message" 154 | } 155 | } 156 | ], 157 | "responses": { 158 | "200": { 159 | "description": "success", 160 | "content": { 161 | "application/json": { 162 | "schema": { 163 | "$ref": "#/components/schemas/messageList" 164 | } 165 | } 166 | } 167 | }, 168 | "401": { 169 | "description": "not authenticated" 170 | }, 171 | "500": { 172 | "description": "general error" 173 | } 174 | }, 175 | "description": "Removes multiple resources queried by given filters.", 176 | "summary": "", 177 | "tags": [ 178 | "message" 179 | ], 180 | "security": [] 181 | } 182 | } 183 | }, 184 | "components": { 185 | "schemas": { 186 | "message": { 187 | "type": "object", 188 | "properties": { 189 | "content": { 190 | "type": "string" 191 | } 192 | } 193 | }, 194 | "messageList": { 195 | "title": "message list", 196 | "type": "array", 197 | "items": { 198 | "$ref": "#/components/schemas/message" 199 | } 200 | } 201 | } 202 | }, 203 | "openapi": "3.0.3", 204 | "tags": [ 205 | { 206 | "name": "message", 207 | "description": "A message service" 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /test/v3/expected-memory-spec-pagination-find.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "schemas": { 4 | "message": { 5 | "properties": { 6 | "content": { 7 | "type": "string" 8 | } 9 | }, 10 | "type": "object" 11 | }, 12 | "messageList": { 13 | "items": { 14 | "$ref": "#/components/schemas/message" 15 | }, 16 | "title": "message list", 17 | "type": "array" 18 | }, 19 | "messagePagination": { 20 | "title": "message pagination result", 21 | "type": "object", 22 | "required": true, 23 | "properties": { 24 | "total": { 25 | "type": "integer" 26 | }, 27 | "limit": { 28 | "type": "integer" 29 | }, 30 | "skip": { 31 | "type": "integer" 32 | }, 33 | "data": { 34 | "$ref": "#/components/schemas/messageList" 35 | } 36 | } 37 | } 38 | } 39 | }, 40 | "info": { 41 | "title": "openapi generator v3 tests", 42 | "version": "1.0.0" 43 | }, 44 | "openapi": "3.0.3", 45 | "paths": { 46 | "/message": { 47 | "get": { 48 | "description": "Retrieves a list of all resources from the service.", 49 | "parameters": [ 50 | { 51 | "description": "Number of results to return", 52 | "in": "query", 53 | "name": "$limit", 54 | "schema": { 55 | "type": "integer" 56 | } 57 | }, 58 | { 59 | "description": "Number of results to skip", 60 | "in": "query", 61 | "name": "$skip", 62 | "schema": { 63 | "type": "integer" 64 | } 65 | }, 66 | { 67 | "description": "Property to sort results", 68 | "in": "query", 69 | "name": "$sort", 70 | "style": "deepObject", 71 | "schema": { 72 | "type": "object" 73 | } 74 | }, 75 | { 76 | "description": "Query parameters to filter", 77 | "in": "query", 78 | "name": "filter", 79 | "style": "form", 80 | "explode": true, 81 | "schema": { 82 | "$ref": "#/components/schemas/message" 83 | } 84 | } 85 | ], 86 | "responses": { 87 | "200": { 88 | "content": { 89 | "application/json": { 90 | "schema": { 91 | "$ref": "#/components/schemas/messagePagination" 92 | } 93 | } 94 | }, 95 | "description": "success" 96 | }, 97 | "401": { 98 | "description": "not authenticated" 99 | }, 100 | "500": { 101 | "description": "general error" 102 | } 103 | }, 104 | "security": [], 105 | "summary": "", 106 | "tags": [ 107 | "message" 108 | ] 109 | } 110 | } 111 | }, 112 | "tags": [ 113 | { 114 | "description": "A message service", 115 | "name": "message" 116 | } 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /test/v3/expected-memory-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "schemas": { 4 | "message": { 5 | "properties": { 6 | "content": { 7 | "type": "string" 8 | } 9 | }, 10 | "type": "object" 11 | }, 12 | "messageList": { 13 | "items": { 14 | "$ref": "#/components/schemas/message" 15 | }, 16 | "title": "message list", 17 | "type": "array" 18 | } 19 | } 20 | }, 21 | "info": { 22 | "title": "openapi generator v3 tests", 23 | "version": "1.0.0" 24 | }, 25 | "openapi": "3.0.3", 26 | "paths": { 27 | "/message": { 28 | "get": { 29 | "description": "Retrieves a list of all resources from the service.", 30 | "parameters": [ 31 | { 32 | "description": "Number of results to return", 33 | "in": "query", 34 | "name": "$limit", 35 | "schema": { 36 | "type": "integer" 37 | } 38 | }, 39 | { 40 | "description": "Number of results to skip", 41 | "in": "query", 42 | "name": "$skip", 43 | "schema": { 44 | "type": "integer" 45 | } 46 | }, 47 | { 48 | "description": "Property to sort results", 49 | "in": "query", 50 | "name": "$sort", 51 | "style": "deepObject", 52 | "schema": { 53 | "type": "object" 54 | } 55 | }, 56 | { 57 | "description": "Query parameters to filter", 58 | "in": "query", 59 | "name": "filter", 60 | "style": "form", 61 | "explode": true, 62 | "schema": { 63 | "$ref": "#/components/schemas/message" 64 | } 65 | } 66 | ], 67 | "responses": { 68 | "200": { 69 | "content": { 70 | "application/json": { 71 | "schema": { 72 | "$ref": "#/components/schemas/messageList" 73 | } 74 | } 75 | }, 76 | "description": "success" 77 | }, 78 | "401": { 79 | "description": "not authenticated" 80 | }, 81 | "500": { 82 | "description": "general error" 83 | } 84 | }, 85 | "security": [], 86 | "summary": "", 87 | "tags": [ 88 | "message" 89 | ] 90 | }, 91 | "post": { 92 | "description": "Creates a new resource with data.", 93 | "parameters": [], 94 | "requestBody": { 95 | "content": { 96 | "application/json": { 97 | "schema": { 98 | "$ref": "#/components/schemas/message" 99 | } 100 | } 101 | }, 102 | "required": true 103 | }, 104 | "responses": { 105 | "201": { 106 | "content": { 107 | "application/json": { 108 | "schema": { 109 | "$ref": "#/components/schemas/message" 110 | } 111 | } 112 | }, 113 | "description": "created" 114 | }, 115 | "401": { 116 | "description": "not authenticated" 117 | }, 118 | "500": { 119 | "description": "general error" 120 | } 121 | }, 122 | "security": [], 123 | "summary": "", 124 | "tags": [ 125 | "message" 126 | ] 127 | } 128 | }, 129 | "/message/{id}": { 130 | "delete": { 131 | "description": "Removes the resource with id.", 132 | "parameters": [ 133 | { 134 | "description": "ID of message to remove", 135 | "in": "path", 136 | "name": "id", 137 | "required": true, 138 | "schema": { 139 | "type": "integer" 140 | } 141 | } 142 | ], 143 | "responses": { 144 | "200": { 145 | "content": { 146 | "application/json": { 147 | "schema": { 148 | "$ref": "#/components/schemas/message" 149 | } 150 | } 151 | }, 152 | "description": "success" 153 | }, 154 | "401": { 155 | "description": "not authenticated" 156 | }, 157 | "404": { 158 | "description": "not found" 159 | }, 160 | "500": { 161 | "description": "general error" 162 | } 163 | }, 164 | "security": [], 165 | "summary": "", 166 | "tags": [ 167 | "message" 168 | ] 169 | }, 170 | "get": { 171 | "description": "Retrieves a single resource with the given id from the service.", 172 | "parameters": [ 173 | { 174 | "description": "ID of message to return", 175 | "in": "path", 176 | "name": "id", 177 | "required": true, 178 | "schema": { 179 | "type": "integer" 180 | } 181 | } 182 | ], 183 | "responses": { 184 | "200": { 185 | "content": { 186 | "application/json": { 187 | "schema": { 188 | "$ref": "#/components/schemas/message" 189 | } 190 | } 191 | }, 192 | "description": "success" 193 | }, 194 | "401": { 195 | "description": "not authenticated" 196 | }, 197 | "404": { 198 | "description": "not found" 199 | }, 200 | "500": { 201 | "description": "general error" 202 | } 203 | }, 204 | "security": [], 205 | "summary": "", 206 | "tags": [ 207 | "message" 208 | ] 209 | }, 210 | "patch": { 211 | "description": "Updates the resource identified by id using data.", 212 | "parameters": [ 213 | { 214 | "description": "ID of message to update", 215 | "in": "path", 216 | "name": "id", 217 | "required": true, 218 | "schema": { 219 | "type": "integer" 220 | } 221 | } 222 | ], 223 | "requestBody": { 224 | "content": { 225 | "application/json": { 226 | "schema": { 227 | "$ref": "#/components/schemas/message" 228 | } 229 | } 230 | }, 231 | "required": true 232 | }, 233 | "responses": { 234 | "200": { 235 | "content": { 236 | "application/json": { 237 | "schema": { 238 | "$ref": "#/components/schemas/message" 239 | } 240 | } 241 | }, 242 | "description": "success" 243 | }, 244 | "401": { 245 | "description": "not authenticated" 246 | }, 247 | "404": { 248 | "description": "not found" 249 | }, 250 | "500": { 251 | "description": "general error" 252 | } 253 | }, 254 | "security": [], 255 | "summary": "", 256 | "tags": [ 257 | "message" 258 | ] 259 | }, 260 | "put": { 261 | "description": "Updates the resource identified by id using data.", 262 | "parameters": [ 263 | { 264 | "description": "ID of message to update", 265 | "in": "path", 266 | "name": "id", 267 | "required": true, 268 | "schema": { 269 | "type": "integer" 270 | } 271 | } 272 | ], 273 | "requestBody": { 274 | "content": { 275 | "application/json": { 276 | "schema": { 277 | "$ref": "#/components/schemas/message" 278 | } 279 | } 280 | }, 281 | "required": true 282 | }, 283 | "responses": { 284 | "200": { 285 | "content": { 286 | "application/json": { 287 | "schema": { 288 | "$ref": "#/components/schemas/message" 289 | } 290 | } 291 | }, 292 | "description": "success" 293 | }, 294 | "401": { 295 | "description": "not authenticated" 296 | }, 297 | "404": { 298 | "description": "not found" 299 | }, 300 | "500": { 301 | "description": "general error" 302 | } 303 | }, 304 | "security": [], 305 | "summary": "", 306 | "tags": [ 307 | "message" 308 | ] 309 | } 310 | } 311 | }, 312 | "tags": [ 313 | { 314 | "description": "A message service", 315 | "name": "message" 316 | } 317 | ] 318 | } 319 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "strictFunctionTypes": true, 9 | "noEmit": true, 10 | "target": "es6", 11 | 12 | // needed for index.test-d.ts 13 | "esModuleInterop": true, 14 | // If the library is an external module (uses `export`), this allows your test file to import "mylib" instead of "./index". 15 | // If the library is global (cannot be imported via `import` or `require`), leave this out. 16 | "baseUrl": ".", 17 | "paths": { "feathers-swagger": ["."] } 18 | } 19 | } 20 | --------------------------------------------------------------------------------