├── .gitignore ├── .eslintrc ├── .babelrc ├── .npmignore ├── .editorconfig ├── src ├── routes.js ├── controller.js ├── index.js └── docs.js ├── test ├── index.js └── test.js ├── rollup.config.js ├── LICENSE.md ├── readme.md ├── package.json ├── docs ├── api.md └── examples.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | public 4 | test/docs -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "comma-dangle": ["error", "never"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "external-helpers", 12 | "transform-object-rest-spread" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Sources 2 | test 3 | docs 4 | 5 | # Configurations 6 | .babelrc 7 | .eslintrc* 8 | npm-* 9 | webpack* 10 | 11 | # Default 12 | .*.swp 13 | ._* 14 | .DS_Store 15 | .git 16 | .hg 17 | .npmrc 18 | .lock-wscript 19 | .svn 20 | .wafpickle-* 21 | config.gypi 22 | CVS 23 | npm-debug.log -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import controllerProvider from './controller'; 2 | 3 | const Router = require('express').Router(); 4 | 5 | Router.use((req, res, next) => { 6 | res.header('Access-Control-Allow-Origin', '*'); 7 | res.header('Access-Control-Allow-Headers', 'X-Requested-With'); 8 | next(); 9 | }); 10 | 11 | export default (routes) => { 12 | routes.forEach((route) => { 13 | const formatedMethod = route.method.toLowerCase(); 14 | 15 | const controller = route.controller || route.json; 16 | const delay = route.delay || 0; 17 | 18 | try { 19 | Router[formatedMethod](route.url, controllerProvider(controller, delay)); 20 | } catch (e) { 21 | throw new Error(`${formatedMethod} is a wrong method`); 22 | } 23 | }); 24 | 25 | return Router; 26 | }; 27 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../public/index'); 2 | 3 | const { ternary } = server; 4 | 5 | const controller = ({ data }) => ternary({ 6 | condition: true, 7 | iftrue: { 8 | success: true, 9 | payload: [ 10 | { 11 | id: 1, 12 | name: 'andrei' 13 | }, 14 | { 15 | id: 1, 16 | name: 'andrei' 17 | }, 18 | { 19 | id: 1, 20 | name: 'andrei' 21 | }, 22 | { 23 | id: 1, 24 | name: 'andrei' 25 | } 26 | ], 27 | errorMessage: null 28 | }, 29 | iffalse: { 30 | success: false, 31 | payload: null, 32 | errorMessage: 'Error' 33 | } 34 | }); 35 | 36 | const testRoute = { 37 | method: 'post', 38 | url: 'test', 39 | controller, 40 | docs: { 41 | title: 'Hello' 42 | } 43 | }; 44 | 45 | const routes = [ 46 | testRoute 47 | ]; 48 | server.start({ 49 | routes 50 | }); 51 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import uglify from 'rollup-plugin-uglify'; 4 | import json from 'rollup-plugin-json'; 5 | import babel from 'rollup-plugin-babel'; 6 | import eslint from 'rollup-plugin-eslint'; 7 | import eslintConfig from 'eslint-config-airbnb'; 8 | 9 | 10 | // `npm run build` -> `production` is true 11 | // `npm run dev` -> `production` is false 12 | const production = !process.env.ROLLUP_WATCH; 13 | 14 | export default { 15 | input: 'src/index.js', 16 | output: { 17 | file: 'public/index.js', 18 | sourcemap: true, 19 | format: 'cjs' 20 | }, 21 | plugins: [ 22 | eslint(eslintConfig), 23 | babel({ 24 | exclude: 'node_modules/**' 25 | }), 26 | resolve(), // tells Rollup how to find date-fns in node_modules 27 | commonjs({ 28 | exclude: 'node_modules/**' 29 | }), // converts date-fns to ES modules 30 | production && uglify(), // minify, but only in production 31 | json() 32 | ] 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Andrei Fidelman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Mokker 2 | [ 3 | ![npm version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=js&type=6&v=0.3.2&x2=0) 4 | ](https://www.npmjs.com/package/mokker) 5 | 6 | Mokker is a simple express RESTful API mock server, which also provides few methods to make your data emulating easier. 7 | 8 | ## Installation 9 | ``` 10 | npm install --save-dev mokker 11 | yarn add --dev mokker 12 | ``` 13 | 14 | ## Dependencies 15 | - body-parser 16 | - express 17 | - morgan 18 | - query-string 19 | - react-dev-utils 20 | 21 | ## Usage 22 | 23 | ``` 24 | // server.js 25 | const mokker = require('mokker'); 26 | 27 | const routes = [{ 28 | method: 'get', 29 | url: '/api', 30 | json: { is: 'done' } 31 | }]; 32 | 33 | mokker.start({ routes }); 34 | // done 😍 35 | ``` 36 | 37 | `$ node server.js` 38 | 39 | 40 | 41 | ## Docs 42 | 43 | - [API](https://github.com/fidelman/mokker/blob/master/docs/api.md) 44 | - [Examples](https://github.com/fidelman/mokker/blob/master/docs/examples.md) 45 | 46 | ## License 47 | 48 | This project is licensed under [MIT License](https://github.com/fidelman/mokker/blob/master/LICENSE.md). See the license file for more details. 49 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiHttp = require('chai-http'); 3 | 4 | const should = chai.should(); // eslint-disable-line 5 | const baseUrl = 'http://localhost:3000'; 6 | 7 | chai.use(chaiHttp); 8 | 9 | describe('Test the Rest server', () => { // eslint-disable-line 10 | it('simple GET server', (done) => { // eslint-disable-line 11 | chai.request(baseUrl) 12 | .get('/test/get') 13 | .end((err, res) => { 14 | res.should.have.status(200); 15 | chai.assert.deepEqual(res.body, { 16 | 'simple-json': true, 17 | obj: { 18 | hi: '1', 19 | hello: { 20 | 1: 2 21 | } 22 | } 23 | }); 24 | done(); 25 | }); 26 | }); 27 | 28 | it('simple POST server', (done) => { // eslint-disable-line 29 | const body = { 'simple-json': true }; 30 | 31 | chai.request(baseUrl) 32 | .post('/test/post') 33 | .send(body) 34 | .end((err, res) => { 35 | res.should.have.status(200); 36 | chai.assert.deepEqual(res.body, body); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | 42 | describe('documentation', () => { 43 | it('create the formatted object for documentation', () => { 44 | const expected = { 45 | '@m_result': { value: 3 }, 46 | '@m_docs': { 47 | merged: { value: 3 }, 48 | objs: [{ value: 1 }, { value: 2 }, { value: 3 }] 49 | } 50 | }; 51 | 52 | const actual = ternary({ 53 | condition: 'x' === '1', 54 | iftrue: { value: 1 }, 55 | iffalse: ternary({ 56 | condition: 'x' === '2', 57 | iftrue: { value: 2 }, 58 | iffalse: { value: 3 } 59 | }) 60 | }); 61 | 62 | chai.assert.deepEqual(actual, expected); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mokker", 3 | "version": "0.3.3", 4 | "description": "Simple Rest API mock server", 5 | "module": "src/index.js", 6 | "main": "public/index.js", 7 | "author": { 8 | "name": "Andrei Fidelman", 9 | "email": "fidelmanlife@gmail.com" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.18.2", 14 | "colors": "^1.1.2", 15 | "express": "^4.16.2", 16 | "fs": "0.0.1-security", 17 | "json2md": "^1.5.10", 18 | "morgan": "^1.9.0", 19 | "query-string": "^5.1.0", 20 | "react-dev-utils": "^4.2.1" 21 | }, 22 | "scripts": { 23 | "build": "rollup -c", 24 | "watch": "rollup -c -w", 25 | "dev": "npm-run-all --parallel start watch", 26 | "start": "cd test/ && nodemon index.js", 27 | "test": "cd test/ && mocha test.js", 28 | "prepublishOnly": "npm run build" 29 | }, 30 | "keywords": [ 31 | "express", 32 | "mock", 33 | "node", 34 | "REST", 35 | "API", 36 | "functional", 37 | "api" 38 | ], 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/fidelman/mokker.git" 42 | }, 43 | "preferGlobal": true, 44 | "bugs": { 45 | "url": "https://github.com/fidelman/mokker/issues" 46 | }, 47 | "homepage": "https://github.com/fidelman/mokker", 48 | "devDependencies": { 49 | "babel-core": "^6.26.0", 50 | "babel-eslint": "^8.2.1", 51 | "babel-plugin-external-helpers": "^6.22.0", 52 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 53 | "babel-preset-env": "^1.6.1", 54 | "chai": "^4.1.2", 55 | "chai-http": "^3.0.0", 56 | "eslint": "^4.18.0", 57 | "eslint-config-airbnb": "^16.1.0", 58 | "eslint-plugin-import": "^2.8.0", 59 | "eslint-plugin-jsx-a11y": "^6.0.3", 60 | "eslint-plugin-react": "^7.6.1", 61 | "mocha": "^4.0.1", 62 | "path": "^0.12.7", 63 | "rollup-plugin-babel": "^3.0.3", 64 | "rollup-plugin-commonjs": "^8.3.0", 65 | "rollup-plugin-eslint": "^4.0.0", 66 | "rollup-plugin-json": "^2.3.0", 67 | "rollup-plugin-node-resolve": "^3.0.3", 68 | "rollup-plugin-uglify": "^3.0.0", 69 | "should": "^13.1.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ### `.start({ routes [, defaultPort, docsUrl] })` 4 | 5 | Run the server 6 | 7 | - `routes: [{ method, url, json, controller }]` – **required**, router settings 8 | - `method: string` – the request [method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) 9 | - `url: string` – the endpoint url 10 | - `delay: number = 0` – the response delay 11 | - `json: object` – the response JSON object of the request 12 | - `controller: (data, req, res) => object` – the custom controller 13 | - `data: { body, params, query, hostQuery }` 14 | - `body: object` – the body of the request 15 | - `params: object` – [route parameters](http://expressjs.com/en/guide/routing.html#route-parameters) `/api/:id` 16 | - `query: object` – object containing a property for each [query parameter](http://expressjs.com/en/api.html#req.query) `/api?x=1` 17 | - `hostQuery: object` – object containing of query properties from a host or to an endpoint 18 | - `req: object` – [the request Exress object](http://www.murvinlai.com/req-and-res-in-nodejs.html) 19 | - `res: object` – [the response Exress object](http://www.murvinlai.com/req-and-res-in-nodejs.html) 20 | - `docs: { title, description, fileName, query, body, hostQuery }` – for initialisation the docs-generation algorithm 21 | - `title: string ` – **required** 22 | - `description: string` 23 | - `fileName: string = title ` 24 | - `query: string[]` – the list of queries which planned to use 25 | - `body: object` – the body structure which planned to use 26 | - `hostQuery: string[]` – the list of host queries which planned to use 27 | - `defaultPort: number = 3000` 28 | - `docsUrl: string = path.resolve(process.cwd(), 'docs') ` – the absolute path to keep the docs 29 | - `headers: object` – optional, override request headers 30 | 31 | ### `.ternary({ condition, iftrue, iffalse }) => object` 32 | 33 | The method to write a controller with conditions, if the documentation is **required**. [Example](https://github.com/fidelman/mokker/blob/master/docs/examples.md#ternary-conroller) 34 | 35 | - `condtition: bool` 36 | - `iftrue: object` – is returned when condition is true 37 | - `iffalse: object` – is returned when condition is false 38 | 39 | -------------------------------------------------------------------------------- /src/controller.js: -------------------------------------------------------------------------------- 1 | const queryString = require('query-string'); 2 | 3 | const transformToTernary = (obj) => { 4 | let ternaryObject = obj; 5 | 6 | if (!('@m_result' in obj)) { 7 | ternaryObject = { 8 | '@m_result': obj, 9 | '@m_docs': { 10 | merged: {}, 11 | objs: [] 12 | } 13 | }; 14 | } 15 | 16 | return ternaryObject; 17 | }; 18 | 19 | const getObj = (iftrueTernary, iffalseTernary) => { 20 | let result; 21 | if (!iftrueTernary['@m_docs'].objs.length && !iffalseTernary['@m_docs'].objs.length) { 22 | result = [].concat(iftrueTernary['@m_result'], iffalseTernary['@m_result']); 23 | } else if (!iftrueTernary['@m_docs'].objs.length) { 24 | result = [].concat(iftrueTernary['@m_result'], iffalseTernary['@m_docs'].objs); 25 | } else if (!iffalseTernary['@m_docs'].objs.length) { 26 | result = [].concat(iffalseTernary['@m_result'], iftrueTernary['@m_docs'].objs); 27 | } else { 28 | result = [].concat(iftrueTernary['@m_docs'].objs, iffalseTernary['@m_docs'].objs); 29 | } 30 | 31 | return result; 32 | }; 33 | 34 | const getHostQuery = (req) => { 35 | let hostQuery; 36 | const host = req.headers.referer || req.url; 37 | const index = host.indexOf('?'); 38 | 39 | if (hostQuery === -1) { 40 | hostQuery = {}; 41 | } else { 42 | const formatedHost = host.slice(index); 43 | hostQuery = queryString.parse(formatedHost); 44 | } 45 | 46 | return hostQuery; 47 | }; 48 | 49 | export const ternary = ({ condition, iftrue, iffalse }) => { 50 | const iftrueTernary = transformToTernary(iftrue); 51 | const iffalseTernary = transformToTernary(iffalse); 52 | 53 | const result = condition ? iftrueTernary['@m_result'] : iffalseTernary['@m_result']; 54 | const merged = Object.assign({}, iftrueTernary['@m_result'], iffalseTernary['@m_result'], iftrueTernary['@m_docs'].merged, iffalseTernary['@m_docs'].merged); 55 | const objs = getObj(iftrueTernary, iffalseTernary); 56 | 57 | return { 58 | '@m_result': result, 59 | '@m_docs': { 60 | merged, 61 | objs 62 | } 63 | }; 64 | }; 65 | 66 | export default (customContoller, delay) => (req, res) => { 67 | const typeofCustomController = typeof customContoller; 68 | let response = {}; 69 | 70 | if (typeofCustomController === 'function') { 71 | const { body, params, query } = req; 72 | 73 | const hostQuery = getHostQuery(req); 74 | 75 | const data = { 76 | body, 77 | params, 78 | query, 79 | hostQuery 80 | }; 81 | 82 | const result = customContoller(data, req, res); 83 | response = '@m_result' in result ? result['@m_result'] : result; 84 | } else if (typeofCustomController === 'object' && !Array.isArray(customContoller)) { 85 | response = customContoller; 86 | } else { 87 | throw new Error(`Unacceptable type of controller: ${typeofCustomController}. It must be 'object' or 'function'.`); 88 | } 89 | 90 | setTimeout(() => res.json(response), delay); 91 | }; 92 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import createRouter from './routes'; 2 | import { ternary } from './controller'; 3 | import generateDocumentation from './docs'; 4 | 5 | const bodyParser = require('body-parser'); 6 | const express = require('express'); 7 | const morgan = require('morgan'); 8 | const { choosePort } = require('react-dev-utils/WebpackDevServerUtils'); 9 | const json2md = require('json2md'); 10 | const fs = require('fs'); 11 | const path = require('path'); 12 | const colors = require('colors'); // eslint-disable-line no-unused-vars 13 | 14 | const app = express(); 15 | 16 | app.use(bodyParser.json()); 17 | app.use(morgan('dev')); 18 | 19 | const requestHeaders = { 20 | 'Access-Control-Allow-Origin': '*', 21 | 'Access-Control-Allow-Methods': 'GET, PUT, POST, DELETE, OPTIONS, PATCH', 22 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Content-Length, X-Requested-With, X-Redmine-API-Key, X-On-Behalf-Of' 23 | }; 24 | 25 | /** 26 | * @param {Object<[key: string]: string>} headers 27 | * @returns void 28 | */ 29 | const setupHeaders = (headers) => { 30 | app.use((req, res, next) => { 31 | if (req.method === 'OPTIONS') { 32 | const headersArray = Object.keys(headers); 33 | 34 | if (headersArray.length) { 35 | headersArray.forEach((headerID) => { 36 | res.header(headerID, headers[headerID]); 37 | }); 38 | } 39 | 40 | res.sendStatus(200); 41 | } else { 42 | next(); 43 | } 44 | }); 45 | }; 46 | 47 | const writeFiles = (url, fileContent) => { 48 | fs.writeFile(url, json2md(fileContent), (err) => { 49 | if (err) { 50 | console.log(err.red); // eslint-disable-line no-console 51 | } else { 52 | console.log(`📄 ${url}`); // eslint-disable-line no-console 53 | } 54 | }); 55 | }; 56 | 57 | const clearDocsFolder = (docsUrl) => { 58 | try { 59 | const files = fs.readdirSync(docsUrl); 60 | Object.keys(files).forEach((key) => { 61 | const file = files[key]; 62 | fs.unlinkSync(path.join(docsUrl, file)); 63 | }); 64 | } catch (err) { 65 | if (err.code === 'ENOENT') { 66 | console.log(`Cannot find the path: ${docsUrl}`.red); // eslint-disable-line no-console 67 | } else { 68 | console.log(err); // eslint-disable-line no-console 69 | } 70 | } 71 | }; 72 | 73 | const createDocs = (routes, docsUrl) => { 74 | let docsFolderCleared = false; 75 | routes.forEach((route) => { 76 | if (route.docs) { 77 | if (!docsFolderCleared) { 78 | clearDocsFolder(docsUrl); 79 | docsFolderCleared = true; 80 | } 81 | 82 | const documentation = generateDocumentation(route); 83 | const url = `${docsUrl}/${documentation.fileName}`; 84 | 85 | writeFiles(url, documentation.fileContent); 86 | } 87 | }); 88 | }; 89 | 90 | const start = ({ 91 | routes = [], 92 | defaultPort = 3000, 93 | docsUrl = path.resolve(process.cwd(), 'docs'), 94 | headers = requestHeaders, 95 | }) => { 96 | setupHeaders(headers); 97 | 98 | app.use('/', createRouter(routes)); 99 | choosePort('0.0.0.0', defaultPort).then((port) => { 100 | if (port == null) return; 101 | app.listen(port, () => console.log(`🚀 App started on port: ${port}`.green)); // eslint-disable-line no-console 102 | }); 103 | 104 | createDocs(routes, docsUrl); 105 | }; 106 | 107 | module.exports = { 108 | requestHeaders, 109 | start, 110 | ternary, 111 | }; 112 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Simple Get request 4 | 5 | ``` 6 | // server.js 7 | const mokker = require('mokker'); 8 | 9 | const routes = [{ 10 | method: 'get', 11 | url: '/api', 12 | json: { is: 'done' } 13 | }]; 14 | 15 | mokker.start({ routes }); 16 | 17 | // app.js 18 | fetch('http://localhost:3000/api', { 19 | method: 'get' 20 | }); 21 | 22 | // response 23 | { 24 | "is": "done" 25 | } 26 | ``` 27 | 28 | ## Simple POST request 29 | 30 | ### Source code 31 | 32 | ``` 33 | // server.js 34 | const mokker = require('mokker'); 35 | 36 | const controller = (data) => { 37 | const { body, query, params, hostQuery } = data; 38 | let response = { 39 | error: 'No Token Found' 40 | }; 41 | 42 | if (hostQuery.token) { 43 | response = Object.assign({}, body); 44 | response.id = params.id; 45 | response.date = query.date; 46 | } 47 | 48 | return response; 49 | }; 50 | 51 | const routes = [{ 52 | method: 'post', 53 | url: '/api/:id', 54 | controller 55 | }]; 56 | 57 | mokker.start({ routes }); 58 | ``` 59 | 60 | ### Response 61 | 62 | ``` 63 | // the request from localhost:8000/152125?data=20181121 64 | // body { 65 | // name: 'John' 66 | // age: 20 67 | // } 68 | { 69 | error: 'No Token Found' 70 | } 71 | 72 | // the request from localhost:8000/152125?token=1&data=20181121 73 | { 74 | name: 'John', 75 | age: 20, 76 | id: '152125', 77 | data: '20181121' 78 | } 79 | ``` 80 | 81 | ## Custom request headers 82 | 83 | ### Set custom headers 84 | 85 | ```js 86 | // server.js 87 | const mokker = require('mokker'); 88 | 89 | const routes = [{ 90 | method: 'get', 91 | url: '/api', 92 | json: { is: 'done' } 93 | }]; 94 | 95 | mokker.start({ 96 | routes, 97 | headers: { 98 | 'Access-Control-Allow-Headers': 'MyCustomHeader' 99 | } 100 | }); 101 | ``` 102 | 103 | ### Extend default request headers 104 | 105 | Default request headers are accessible via `mokker.requestHeaders`. Its value is: 106 | 107 | ```js 108 | { 109 | 'Access-Control-Allow-Origin': '*', 110 | 'Access-Control-Allow-Methods': 'GET, PUT, POST, DELETE, OPTIONS, PATCH', 111 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Content-Length, X-Requested-With, X-Redmine-API-Key, X-On-Behalf-Of' 112 | } 113 | ``` 114 | 115 | You can extend headers as following: 116 | 117 | ```js 118 | // server.js 119 | const mokker = require('mokker'); 120 | 121 | const routes = [{ 122 | method: 'get', 123 | url: '/api', 124 | json: { is: 'done' } 125 | }]; 126 | 127 | mokker.start({ 128 | routes, 129 | headers: { 130 | ...mokker.requestHeaders, 131 | 'Access-Control-Allow-Headers': 'MyCustomHeader' 132 | } 133 | }); 134 | ``` 135 | 136 | ## The documented request 137 | 138 | ### Source code 139 | 140 | ``` 141 | // server.js 142 | const mokker = require('mokker'); 143 | 144 | const controller = (data) => { 145 | const { body, query, params, hostQuery } = data; 146 | 147 | const response = Object.assign({}, body); 148 | response.id = params.id; 149 | response.date = query.date; 150 | response.token = hostQuery.token; 151 | 152 | return response; 153 | }; 154 | 155 | const routes = [{ 156 | method: 'post', 157 | url: '/api/:id', 158 | controller, 159 | docs: { 160 | title: 'Post Request', 161 | description: 'The example of a post request', 162 | fileName: 'post-request-docs', 163 | query: ['date'], 164 | hostQuery: ['token'], 165 | body: { 166 | name: '', 167 | age: 1 168 | } 169 | } 170 | }]; 171 | 172 | mokker.start({ routes }); 173 | ``` 174 | 175 | 176 | 177 | ### Documentation 178 | 179 | ``` 180 | // ./docs/post-request-docs.md 181 | 182 | # Post Request 183 | 184 | > The example of a post request 185 | 186 | ## Method 187 | 188 | 189 | POST 190 | 191 | ## URL 192 | 193 | ​```js 194 | /api/:id 195 | ​``` 196 | 197 | ## Host Query Parameters 198 | 199 | > For mock development 200 | 201 | ​```js 202 | [ 203 | "token" 204 | ] 205 | ​``` 206 | 207 | ## Query Parameters 208 | 209 | ​```js 210 | [ 211 | "date" 212 | ] 213 | ​``` 214 | 215 | ## Body 216 | 217 | ​```js 218 | { 219 | "name": "string", 220 | "age": "number" 221 | } 222 | ​``` 223 | 224 | ## Response 225 | 226 | ​```js 227 | { 228 | "name": "string", 229 | "age": "number", 230 | "id": "string", 231 | "date": "string", 232 | "token": "string" 233 | } 234 | ​``` 235 | ``` 236 | 237 | 238 | 239 | ## Ternary conroller 240 | 241 | ### Source code 242 | 243 | ``` 244 | // server.js 245 | const mokker = require('mokker'); 246 | 247 | const controller = (data) => { 248 | const { ternary } = mokker; 249 | const { hostQuery: { token } } = data; 250 | return ternary({ 251 | condition: !token, 252 | iftrue: { 253 | success: false, 254 | payload: null, 255 | errorMessage: 'No token found' 256 | }, 257 | iffalse: ternary({ 258 | condition: token === 'experimental' 259 | iftrue: { 260 | success: true, 261 | payload: { 262 | token, 263 | prop: true 264 | }, 265 | errorMessage: null 266 | }, 267 | iffalse: { 268 | success: true, 269 | payload: { 270 | token 271 | }, 272 | errorMessage: null 273 | } 274 | }) 275 | }) 276 | }; 277 | 278 | const routes = [{ 279 | method: 'get', 280 | url: '/api', 281 | controller, 282 | docs: { 283 | title: 'Ternary test', 284 | hostQuery: ['token'] 285 | } 286 | }]; 287 | ``` 288 | 289 | ### Response 290 | 291 | ``` 292 | // the request from localhost:8000 293 | { 294 | success: false, 295 | payload: null, 296 | errorMessage: 'No token found' 297 | } 298 | 299 | // the request from localhost:8000?token=experimental 300 | { 301 | success: true, 302 | payload: { 303 | token: 'experimental', 304 | prop: true 305 | }, 306 | errorMessage: null 307 | } 308 | 309 | // the request from localhost:8000?token=1 310 | { 311 | success: true, 312 | payload: { 313 | token: 1 314 | }, 315 | errorMessage: null 316 | } 317 | ``` 318 | 319 | ### Documentation 320 | 321 | ``` 322 | // ./docs/ternary-test.md 323 | # Ternary test 324 | 325 | ## Method 326 | 327 | GET 328 | 329 | ## URL 330 | ​```js 331 | /api 332 | ​``` 333 | 334 | ## Host Query Parameters 335 | 336 | > For mock development 337 | 338 | ​```js 339 | [ 340 | "token" 341 | ] 342 | ​``` 343 | 344 | ## Response 345 | 346 | ​```js 347 | { 348 | "success": "boolean", 349 | "?errorMessage": "string", 350 | "?payload": { 351 | "token": "string | number", 352 | "?prop": bool 353 | } 354 | } 355 | ​``` 356 | -------------------------------------------------------------------------------- /src/docs.js: -------------------------------------------------------------------------------- 1 | function parseObject(obj) { 2 | const parsedObject = {}; 3 | Object.keys(obj).forEach((key) => { 4 | const value = obj[key]; 5 | let type = typeof value; 6 | 7 | if (Array.isArray(value)) { 8 | type = parseArray(value); // eslint-disable-line 9 | } else if (value === null) { 10 | type = 'null'; 11 | } else if (type === 'object') { 12 | type = parseObject(value); 13 | } 14 | 15 | parsedObject[key] = type; 16 | }); 17 | 18 | return parsedObject; 19 | } 20 | 21 | function parseArray(arr) { 22 | let parsedArray; 23 | const firstItem = arr[0]; 24 | 25 | if (!firstItem) { 26 | parsedArray = '[]'; 27 | } else if (Array.isArray(firstItem)) { 28 | parsedArray = `${parseArray(firstItem)}[]`; 29 | } else if (typeof firstItem === 'object') { 30 | parsedArray = [parseObject(firstItem)]; 31 | } else { 32 | parsedArray = `${typeof firstItem}[]`; 33 | } 34 | 35 | return parsedArray; 36 | } 37 | 38 | const isPropMandatory = (types, matches, possibleMatches) => { 39 | let isMandatory = false; 40 | 41 | if (types.includes('null') || types.includes('undefined')) { 42 | isMandatory = false; 43 | } else if (matches === possibleMatches) { 44 | isMandatory = true; 45 | } 46 | 47 | return isMandatory; 48 | }; 49 | 50 | const getMandatoryFlag = ({ 51 | types, 52 | matches, 53 | possibleMatches, 54 | flagIfYes, 55 | flagIfNo 56 | }) => (isPropMandatory(types, matches, possibleMatches) ? flagIfYes : flagIfNo); 57 | 58 | const findTypesObjects = types => types.filter(type => typeof type === 'object'); 59 | const findTypesNoObjects = types => types.filter(type => typeof type !== 'object'); 60 | 61 | const generateTypesWithMergedObjects = (types) => { 62 | const typesObjects = findTypesObjects(types); 63 | let typesWithMergedObjects = types; 64 | 65 | if (typesObjects.length > 1) { 66 | const merged = Object.assign({}, ...typesObjects); 67 | 68 | const JSONFromTernary = getJSONFromTernary({ // eslint-disable-line 69 | objs: typesObjects, 70 | merged 71 | }); 72 | 73 | typesWithMergedObjects = [].concat(findTypesNoObjects(types), JSONFromTernary); 74 | } 75 | 76 | return typesWithMergedObjects; 77 | }; 78 | 79 | const generateJSONValue = (types) => { 80 | let JSONValue = ''; 81 | 82 | const typesWithMergedObject = generateTypesWithMergedObjects(types); 83 | 84 | const typesWithoutNullAndUndef = typesWithMergedObject.filter(type => type !== 'undefined' && type !== 'null'); 85 | 86 | typesWithoutNullAndUndef.forEach((item, index) => { 87 | if (typeof item === 'object') { 88 | JSONValue += `${JSON.stringify(item, null, 2)}`; 89 | } else { 90 | JSONValue += `${item}`; 91 | } 92 | 93 | if (index < typesWithoutNullAndUndef.length - 1) JSONValue += ' | '; 94 | }); 95 | 96 | return JSONValue; 97 | }; 98 | 99 | const parseStringifiedObject = (stringifiedObject) => { 100 | const parsedObject = {}; 101 | Object.keys(stringifiedObject).forEach((key) => { 102 | const value = stringifiedObject[key]; 103 | 104 | const type = typeof value; 105 | 106 | if (type === 'string') { 107 | try { 108 | const parsed = JSON.parse(value); 109 | if (typeof parsed === 'object') { 110 | parsedObject[key] = parseStringifiedObject(parsed); 111 | } else { 112 | parsedObject[key] = value; 113 | } 114 | } catch (e) { 115 | parsedObject[key] = value; 116 | } 117 | } else { 118 | parsedObject[key] = value; 119 | } 120 | }); 121 | 122 | if (parsedObject[0]) return [parsedObject[0]]; 123 | 124 | return parsedObject; 125 | }; 126 | 127 | function getJSONFromTernary(ternaryObject) { 128 | const { merged, objs } = ternaryObject; 129 | const mergedKeys = Object.keys(merged); 130 | 131 | const stringifiedObject = {}; 132 | 133 | mergedKeys.forEach((key) => { 134 | let matches = 0; 135 | const types = []; 136 | 137 | objs.forEach((obj) => { 138 | if (key in obj) { 139 | const value = obj[key]; 140 | matches += 1; 141 | let type = typeof value; 142 | 143 | if (Array.isArray(value)) { 144 | type = parseArray(value); 145 | } else if (value === null) { 146 | type = 'null'; 147 | } else if (typeof value === 'object') { 148 | type = parseObject(value); 149 | } 150 | 151 | if (!types.includes(type)) types.push(type); 152 | } 153 | }); 154 | 155 | const mandatoryFlag = getMandatoryFlag({ 156 | types, 157 | matches, 158 | possibleMatches: objs.length, 159 | flagIfYes: '', 160 | flagIfNo: '?' 161 | }); 162 | const JSONKey = `${mandatoryFlag}${key}`; 163 | const JSONValue = generateJSONValue(types); 164 | 165 | stringifiedObject[JSONKey] = JSONValue; 166 | }); 167 | 168 | const JSONFromTernary = parseStringifiedObject(stringifiedObject); 169 | 170 | return JSONFromTernary; 171 | } 172 | 173 | const getParamsFromUrl = (url) => { 174 | const paramsFromUrl = {}; 175 | url 176 | .split('?')[0] 177 | .split('/') 178 | .filter(item => item.includes(':')) 179 | .forEach((param) => { 180 | paramsFromUrl[param.replace(':', '')] = ''; 181 | }); // all route params are strings 182 | 183 | return paramsFromUrl; 184 | }; 185 | 186 | const getDataFromArray = (array) => { 187 | const data = {}; 188 | array.forEach((item) => { 189 | data[item] = ''; 190 | }); 191 | return data; 192 | }; 193 | 194 | const getArrayOfJSON = (json) => { 195 | const string = JSON.stringify(json, null, 2).replace(/\\"/g, "'").replace(/\\n/g, ` 196 | `); 197 | 198 | const array = string.split('\n'); 199 | 200 | return array; 201 | }; 202 | 203 | const generateDocsFromArray = (array) => { 204 | const docs = { 205 | language: 'js', 206 | content: [] 207 | }; 208 | 209 | getArrayOfJSON(array).forEach(item => docs.content.push(item)); 210 | 211 | return docs; 212 | }; 213 | 214 | const generateDocsFromObject = (response, body, hostQuery = [], queryArray = [], url = '') => { 215 | const docs = { 216 | language: 'js', 217 | content: [] 218 | }; 219 | 220 | let json; 221 | 222 | if (Array.isArray(response)) { 223 | json = `[${parseObject(response)}]`; 224 | } else if (typeof response === 'object') { 225 | json = parseObject(response); 226 | } else { 227 | const responseJSON = response({ 228 | body, 229 | params: getParamsFromUrl(url), 230 | query: getDataFromArray(queryArray), 231 | hostQuery: getDataFromArray(hostQuery) 232 | }); 233 | 234 | if ('@m_docs' in responseJSON) { 235 | json = getJSONFromTernary(responseJSON['@m_docs']); 236 | } else { 237 | json = parseObject(responseJSON); 238 | } 239 | } 240 | 241 | getArrayOfJSON(json).forEach(item => docs.content.push(item)); 242 | 243 | return docs; 244 | }; 245 | 246 | const getFileName = fileName => `${fileName.toLocaleLowerCase().replace(new RegExp(' ', 'g'), '-')}.md`; 247 | 248 | export default (route) => { 249 | const { docs } = route; 250 | const fileContent = []; 251 | 252 | fileContent.push({ 253 | h1: docs.title 254 | }); 255 | 256 | if (docs.description) { 257 | fileContent.push({ 258 | blockquote: docs.description 259 | }); 260 | } 261 | 262 | fileContent.push({ 263 | h2: 'Method' 264 | }); 265 | 266 | fileContent.push({ 267 | p: route.method.toLocaleUpperCase() 268 | }); 269 | 270 | fileContent.push({ 271 | h2: 'URL' 272 | }); 273 | 274 | fileContent.push({ 275 | code: { 276 | language: 'js', 277 | content: [route.url.split('?')[0]] // ignore query params 278 | } 279 | }); 280 | 281 | if (docs.hostQuery) { 282 | fileContent.push({ 283 | h2: 'Host Query Parameters' 284 | }); 285 | fileContent.push({ 286 | blockquote: 'For mock development' 287 | }); 288 | fileContent.push({ 289 | code: generateDocsFromArray(docs.hostQuery) 290 | }); 291 | } 292 | 293 | if (docs.query) { 294 | fileContent.push({ 295 | h2: 'Query Parameters' 296 | }); 297 | fileContent.push({ 298 | code: generateDocsFromArray(docs.query) 299 | }); 300 | } 301 | 302 | if (docs.body) { 303 | fileContent.push({ 304 | h2: 'Body' 305 | }); 306 | fileContent.push({ 307 | code: generateDocsFromObject(docs.body) 308 | }); 309 | } 310 | 311 | if (route.json) { 312 | fileContent.push({ 313 | h2: 'Response' 314 | }); 315 | fileContent.push({ 316 | code: generateDocsFromObject(route.json) 317 | }); 318 | } else if (route.controller) { 319 | fileContent.push({ 320 | h2: 'Response' 321 | }); 322 | fileContent.push({ 323 | code: generateDocsFromObject(route.controller, docs.body, docs.hostQuery, docs.query, route.url) // eslint-disable-line 324 | }); 325 | } 326 | 327 | const fileName = getFileName(docs.fileName || docs.title); 328 | 329 | return { 330 | fileName, 331 | fileContent 332 | }; 333 | }; 334 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.4: 6 | version "1.3.4" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" 8 | dependencies: 9 | mime-types "~2.1.16" 10 | negotiator "0.6.1" 11 | 12 | array-flatten@1.1.1: 13 | version "1.1.1" 14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 15 | 16 | basic-auth@~2.0.0: 17 | version "2.0.0" 18 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.0.tgz#015db3f353e02e56377755f962742e8981e7bbba" 19 | dependencies: 20 | safe-buffer "5.1.1" 21 | 22 | body-parser@1.18.2, body-parser@^1.18.2: 23 | version "1.18.2" 24 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" 25 | dependencies: 26 | bytes "3.0.0" 27 | content-type "~1.0.4" 28 | debug "2.6.9" 29 | depd "~1.1.1" 30 | http-errors "~1.6.2" 31 | iconv-lite "0.4.19" 32 | on-finished "~2.3.0" 33 | qs "6.5.1" 34 | raw-body "2.3.2" 35 | type-is "~1.6.15" 36 | 37 | bytes@3.0.0: 38 | version "3.0.0" 39 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 40 | 41 | content-disposition@0.5.2: 42 | version "0.5.2" 43 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 44 | 45 | content-type@~1.0.4: 46 | version "1.0.4" 47 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 48 | 49 | cookie-signature@1.0.6: 50 | version "1.0.6" 51 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 52 | 53 | cookie@0.3.1: 54 | version "0.3.1" 55 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 56 | 57 | debug@2.6.9: 58 | version "2.6.9" 59 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 60 | dependencies: 61 | ms "2.0.0" 62 | 63 | decode-uri-component@^0.2.0: 64 | version "0.2.0" 65 | resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" 66 | 67 | depd@1.1.1, depd@~1.1.1: 68 | version "1.1.1" 69 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 70 | 71 | destroy@~1.0.4: 72 | version "1.0.4" 73 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 74 | 75 | ee-first@1.1.1: 76 | version "1.1.1" 77 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 78 | 79 | encodeurl@~1.0.1: 80 | version "1.0.1" 81 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 82 | 83 | escape-html@~1.0.3: 84 | version "1.0.3" 85 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 86 | 87 | etag@~1.8.1: 88 | version "1.8.1" 89 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 90 | 91 | express@^4.16.2: 92 | version "4.16.2" 93 | resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" 94 | dependencies: 95 | accepts "~1.3.4" 96 | array-flatten "1.1.1" 97 | body-parser "1.18.2" 98 | content-disposition "0.5.2" 99 | content-type "~1.0.4" 100 | cookie "0.3.1" 101 | cookie-signature "1.0.6" 102 | debug "2.6.9" 103 | depd "~1.1.1" 104 | encodeurl "~1.0.1" 105 | escape-html "~1.0.3" 106 | etag "~1.8.1" 107 | finalhandler "1.1.0" 108 | fresh "0.5.2" 109 | merge-descriptors "1.0.1" 110 | methods "~1.1.2" 111 | on-finished "~2.3.0" 112 | parseurl "~1.3.2" 113 | path-to-regexp "0.1.7" 114 | proxy-addr "~2.0.2" 115 | qs "6.5.1" 116 | range-parser "~1.2.0" 117 | safe-buffer "5.1.1" 118 | send "0.16.1" 119 | serve-static "1.13.1" 120 | setprototypeof "1.1.0" 121 | statuses "~1.3.1" 122 | type-is "~1.6.15" 123 | utils-merge "1.0.1" 124 | vary "~1.1.2" 125 | 126 | finalhandler@1.1.0: 127 | version "1.1.0" 128 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" 129 | dependencies: 130 | debug "2.6.9" 131 | encodeurl "~1.0.1" 132 | escape-html "~1.0.3" 133 | on-finished "~2.3.0" 134 | parseurl "~1.3.2" 135 | statuses "~1.3.1" 136 | unpipe "~1.0.0" 137 | 138 | forwarded@~0.1.2: 139 | version "0.1.2" 140 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 141 | 142 | fresh@0.5.2: 143 | version "0.5.2" 144 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 145 | 146 | http-errors@1.6.2, http-errors@~1.6.2: 147 | version "1.6.2" 148 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 149 | dependencies: 150 | depd "1.1.1" 151 | inherits "2.0.3" 152 | setprototypeof "1.0.3" 153 | statuses ">= 1.3.1 < 2" 154 | 155 | iconv-lite@0.4.19: 156 | version "0.4.19" 157 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 158 | 159 | inherits@2.0.3: 160 | version "2.0.3" 161 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 162 | 163 | ipaddr.js@1.5.2: 164 | version "1.5.2" 165 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" 166 | 167 | media-typer@0.3.0: 168 | version "0.3.0" 169 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 170 | 171 | merge-descriptors@1.0.1: 172 | version "1.0.1" 173 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 174 | 175 | methods@~1.1.2: 176 | version "1.1.2" 177 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 178 | 179 | mime-db@~1.30.0: 180 | version "1.30.0" 181 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" 182 | 183 | mime-types@~2.1.15, mime-types@~2.1.16: 184 | version "2.1.17" 185 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" 186 | dependencies: 187 | mime-db "~1.30.0" 188 | 189 | mime@1.4.1: 190 | version "1.4.1" 191 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" 192 | 193 | morgan@^1.9.0: 194 | version "1.9.0" 195 | resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.0.tgz#d01fa6c65859b76fcf31b3cb53a3821a311d8051" 196 | dependencies: 197 | basic-auth "~2.0.0" 198 | debug "2.6.9" 199 | depd "~1.1.1" 200 | on-finished "~2.3.0" 201 | on-headers "~1.0.1" 202 | 203 | ms@2.0.0: 204 | version "2.0.0" 205 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 206 | 207 | negotiator@0.6.1: 208 | version "0.6.1" 209 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 210 | 211 | object-assign@^4.1.0: 212 | version "4.1.1" 213 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 214 | 215 | on-finished@~2.3.0: 216 | version "2.3.0" 217 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 218 | dependencies: 219 | ee-first "1.1.1" 220 | 221 | on-headers@~1.0.1: 222 | version "1.0.1" 223 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" 224 | 225 | parseurl@~1.3.2: 226 | version "1.3.2" 227 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 228 | 229 | path-to-regexp@0.1.7: 230 | version "0.1.7" 231 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 232 | 233 | proxy-addr@~2.0.2: 234 | version "2.0.2" 235 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" 236 | dependencies: 237 | forwarded "~0.1.2" 238 | ipaddr.js "1.5.2" 239 | 240 | qs@6.5.1: 241 | version "6.5.1" 242 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 243 | 244 | query-string@^5.0.1: 245 | version "5.0.1" 246 | resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88" 247 | dependencies: 248 | decode-uri-component "^0.2.0" 249 | object-assign "^4.1.0" 250 | strict-uri-encode "^1.0.0" 251 | 252 | range-parser@~1.2.0: 253 | version "1.2.0" 254 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 255 | 256 | raw-body@2.3.2: 257 | version "2.3.2" 258 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 259 | dependencies: 260 | bytes "3.0.0" 261 | http-errors "1.6.2" 262 | iconv-lite "0.4.19" 263 | unpipe "1.0.0" 264 | 265 | safe-buffer@5.1.1: 266 | version "5.1.1" 267 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 268 | 269 | send@0.16.1: 270 | version "0.16.1" 271 | resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" 272 | dependencies: 273 | debug "2.6.9" 274 | depd "~1.1.1" 275 | destroy "~1.0.4" 276 | encodeurl "~1.0.1" 277 | escape-html "~1.0.3" 278 | etag "~1.8.1" 279 | fresh "0.5.2" 280 | http-errors "~1.6.2" 281 | mime "1.4.1" 282 | ms "2.0.0" 283 | on-finished "~2.3.0" 284 | range-parser "~1.2.0" 285 | statuses "~1.3.1" 286 | 287 | serve-static@1.13.1: 288 | version "1.13.1" 289 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" 290 | dependencies: 291 | encodeurl "~1.0.1" 292 | escape-html "~1.0.3" 293 | parseurl "~1.3.2" 294 | send "0.16.1" 295 | 296 | setprototypeof@1.0.3: 297 | version "1.0.3" 298 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 299 | 300 | setprototypeof@1.1.0: 301 | version "1.1.0" 302 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 303 | 304 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 305 | version "1.3.1" 306 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 307 | 308 | strict-uri-encode@^1.0.0: 309 | version "1.1.0" 310 | resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" 311 | 312 | type-is@~1.6.15: 313 | version "1.6.15" 314 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 315 | dependencies: 316 | media-typer "0.3.0" 317 | mime-types "~2.1.15" 318 | 319 | unpipe@1.0.0, unpipe@~1.0.0: 320 | version "1.0.0" 321 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 322 | 323 | url-query@^2.0.0: 324 | version "2.0.0" 325 | resolved "https://registry.yarnpkg.com/url-query/-/url-query-2.0.0.tgz#b181232a6fc7defe88d2ca37ee3add292ea45e8f" 326 | 327 | utils-merge@1.0.1: 328 | version "1.0.1" 329 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 330 | 331 | vary@~1.1.2: 332 | version "1.1.2" 333 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 334 | --------------------------------------------------------------------------------