Repository: leboncoin/morphlingjs Branch: master Commit: 383a274a57d6 Files: 45 Total size: 76.7 KB Directory structure: gitextract_8_ylaml_/ ├── .babelrc ├── .dockerignore ├── .gitignore ├── .npmignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── cli/ │ ├── index.js │ ├── morphling-apply.js │ ├── morphling-bash.js │ ├── morphling-config.js │ ├── morphling-describe.js │ ├── morphling-list.js │ ├── morphling-remove-all.js │ ├── morphling-remove.js │ ├── morphling-restart.js │ ├── morphling-start.js │ ├── morphling-stop.js │ ├── morphling-toggle.js │ └── utils/ │ ├── prompt-props.js │ └── util.js ├── docker-compose.yml ├── makefile ├── package.json ├── src/ │ ├── api/ │ │ ├── apply.js │ │ ├── describe.js │ │ ├── index.js │ │ ├── list.js │ │ ├── middleware/ │ │ │ ├── error.js │ │ │ └── loki.js │ │ ├── remove-all.js │ │ ├── remove.js │ │ └── toggle.js │ ├── data/ │ │ └── .gitkeep │ ├── db/ │ │ ├── collection.js │ │ ├── fixtures.js │ │ └── index.js │ ├── server.js │ └── utils/ │ ├── createRouterFromSwagger.js │ ├── morphling.js │ ├── override-router.js │ └── utils.js ├── swagger-examples/ │ └── example.yaml └── swagger-override-examples/ ├── demo-with-body.json └── demo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "env" ], "plugins": [ "transform-runtime", "transform-object-rest-spread" ] } ================================================ FILE: .dockerignore ================================================ node_modules npm-debug.log .idea src/fileSources/remoteFiles/*.yml ================================================ FILE: .gitignore ================================================ .idea/ bin/ node_modules/ npm-debug.log cli/morphling-config.json src/data/*.yaml src/data/*.yml src/data/*.json *.tgz package ================================================ FILE: .npmignore ================================================ src cli .babelrc .idea ================================================ FILE: Dockerfile ================================================ FROM node:8.2.1 RUN mkdir -p /usr/app WORKDIR /usr/app COPY ./package.json ./ RUN npm install RUN npm install pm2 -g --silent COPY bin/src ./src RUN mkdir -p /usr/app/src/data ARG NODE_PORT=8883 ENV NODE_PORT=$NODE_PORT EXPOSE ${NODE_PORT} CMD ["pm2-docker", "src/server.js"] ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2017 Leboncoin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Morphling *Cause nobody aint got time to wait for back-ends to be developped.* ## Features - A sweet all-in-CLI with **no knowledge of Javascript necessary** _(if you can write JSON you good 👍 )_ - **Mocks any object, array or field** declared in a swagger with meaningful **autogenerated data** - **Persists route mocks** to allow you to develop your front-end as fast as a lightning - Uses **Faker generators** behind the curtains - **Empty responses, error codes, any http method, any body** _not sure about that last one but I'm trying ok_ - Two lines install and **minimal knowledge of backend necessary** - Supports **JSON and YAML** swaggers - Supports (a big part of) **OpenAPI 3.0** ## Installation - Ensure that you have both `docker` and `docker-compose` installed and available to your terminal. - Also, make sure that the docker daemon is running. - Now just go for it: `npm i -g morphlingjs` it takes a while so grab a cub of coffee on the way. - Run the configuration utility to select the port morphling will run on: `morphling config` - Add a swagger.yml file to Morphling by doing a `morphling apply your/file` Now morphling knows your swagger and can mock it properly. Just try to hit a route that is described in your swagger ! To see what swaggers Morphling knows do a `morphling list`. *Note: If having any issues during the install with npm such as 'Cannot read property 0 of undefined', try to downgrade your current npm install to 5.2.0* ## Overriding a route Morphling allows you to create a mock for a specific route ; because sometimes you need a specific body from your server. For instance, you want your local server to return a specific response on a given route. It can be a route that is described in your swagger where you don't want autogenerated data or just a route you don't want to bother putting in the swagger. You will want to use **an override file**, which you can find two examples of in the [examples](https://github.com/leboncoin/morphlingjs/tree/master/swagger-override-examples). Doing a - `morphling apply swagger-override-examples/demo.json -o` and then - `morphling toggle demo.json` will create a route on `localhost:8883/v2/store/order/1` and then enable it (if morphling runs on 8883). This route will return, as seen in the example file, a 200 (OK) HTTP code, with an empty body. ## CLI Commands Any command here can be run with the `--help` flag to give you a tad more info 👍 ### help - `help [cmd] display help for [cmd]` Displays the help ### apply `apply|a Apply a Swagger to morphling` Send a Swagger file to the Morphling instance for it to be mocked. If used with `-o` flag, save file as an **Override file** to Morphling. Overrides are **disabled by default**. To enable/disable them, check the `toggle` command. There is no validation working on the server at the time of writing this which means that it will break silently. If a route is not mocked, I suggest that you `morphling bash` into the instance and run `pm2 logs` while trying to `apply` the swagger. Might give you some info. If not, please send an issue! I'll do my best to fix it ASAP. ### bash - `bash|b Bash into the morphling container` Open a bash straight into the container. Useful for checking logs with `pm2 logs` for instance. ### config - `config|c Configure the morphling server` Persist CLI options, for instance the port that the CLI will hit on localhost ### describe - `describe|d Describe a morphling override` Describe an override file's content. ### list - `list|ls List files saved in morphling` List all swaggers saved to Morphling. Use with `-o` to list override files. ### remove - NOT IMPLEMENTED YET - `remove|rm Remove a Swagger file saved in Morphling` ### remove-all - NOT IMPLEMENTED YET - `remove-all|rma Clear Swagger files saved in Morphling` ### restart - `restart|rr Restart the morphling server` Restart the Morphling instance. _who would have thought_ ### start - `start|s Start the morphling server on 8883 if no port is provided` Start the morphling instance. ### stop - `stop|k Stop the morphling server`\ Stop the morphling instance. ### toggle - `toggle|t Toggle an override` Enable/disable an override. Use with `-d` to disable all, or `-e` to enable all. _Note: if you think about other useful commands, feel free to submit an issue! 😉_ ## Installation from source _Here be dragons, you probably don't need to do this, except if want to contribute, which would be lovely._ Ensure that you have both `docker` and `docker-compose` installed and available to your terminal. - Clone the package - Inside the package: `make start` and wait for the build to be over - If the message `Morphling started on 8883` pops, it means that everything went well! - `npm link` to make `morphling` available globally to your terminal - `morphling config` and go through the short process - `morphling --help` for other informations should help you out. *Note: If having any issues during the install with npm such as 'Cannot read property 0 of undefined', try to downgrade your current npm install to 5.2.0* ## Useful development commands I've made available a `morphling bash` command which immediately bashes you inside the currently-running Docker instance of Morphling `make ` - `start` Run the build proces and start the Morphling docker instance immediatly then pipe the logs - `dev` Start the server without docker on a bare Nodejs process - `build` Build all javascript files with *Babel*. - `docker-build` Run the docker build process and run the server. - `clear-docker` Hard-delete the Morphling instance from your local Docker - `devstart-hard` Clear all dependencies and docker Morphling instances and start ## Upcoming features _feel free to submit PRs_ - SwaggerUI! - Print proper Morphlingjs version with `--version / -v` - On the go switching of running ports - Add ID to overrides and Swagger files so you dont have to type them down / or maybe just autocomplete them - `morphling validate` A proper validator for the swagger files and an associated CLI command - `morphling remove` Remove a file from Morphling - `morphling remove-all` Remove all files from Morphling (also remove local saves of fixtures) always with --force - A better autocompletion using the npm package `commander-completion` - Overriding a field key in an object with a specific faker generator (ie: zipcode, username...) - Override checker (ensure that a specific override does not already exist when adding another one) ================================================ FILE: cli/index.js ================================================ #! /usr/bin/env node import program from 'commander'; program .version('0.0.1') .description('CLI interface for Morphling') .command('apply ', 'Apply a Swagger to morphling').alias('a') .command('bash', 'Bash into the morphling container').alias('b') .command('config', 'Configure the morphling server').alias('c') .command('describe ', 'Describe a morphling override').alias('d') .command('list', 'List files saved in morphling').alias('ls') .command('remove', 'Remove a Swagger file saved in Morphling').alias('rm') .command('remove-all', 'Clear Swagger files saved in Morphling').alias('rma') .command('restart', 'Restart the morphling server').alias('rr') .command('start', 'Start the morphling server on 8883 if no port is provided').alias('s') .command('stop', 'Stop the morphling server').alias('k') .command('toggle ', 'Toggle an override').alias('t') program.parse(process.argv); ================================================ FILE: cli/morphling-apply.js ================================================ #! /usr/bin/env node import program from 'commander'; import FormData from 'form-data'; import chalk from 'chalk'; import fetch from 'node-fetch'; import fs from 'fs'; import * as utilFunctions from './utils/util' import { restartAction } from './morphling-restart'; const applyAction = async (dir) => { try { const config = await utilFunctions.returnConfigIfExists(); const port = config.port || program.port || 8883; const isOverrideFile = program.override || false; const url = `http://localhost:${port}`; const splitDir = dir.split('/'); const filename = splitDir[splitDir.length - 1]; const filenameForOverride = isOverrideFile ? `morphling-override-${filename}` : filename; const listUrl = `${url}/morphling/list?filename=${filenameForOverride}`; const request = await fetch(listUrl); const { fileExists } = await request.json(); if (!fileExists || program.force) { console.log(chalk.green(`Applying '${chalk.bold(filename)}' to Morphling ...`)); const form = new FormData(); form.append('file', fs.createReadStream(`${process.cwd()}/${dir}`)); form.append('fileName', filenameForOverride); await fetch(`${url}/morphling/apply${isOverrideFile ? '?override=true' : ''}`, { method: 'POST', body: form, }); console.log(chalk.green(`${isOverrideFile ? 'Override file' : 'File'} '${chalk.bold(filename)}' successfully uploaded! Restarting...`)); await restartAction(); } else { console.log(chalk.red(`${isOverrideFile ? 'Override file' : 'File'} '${chalk.bold(filename)}' already exists on Morphling. Type morphling apply with -f to force.`)); } } catch (e) { if (e.code === 'ECONNREFUSED') { console.log(chalk.red(`Connection to Morphling was refused. Is Morphling currently running?`)); } } } program .action(applyAction) .option('-p, --port ', 'A port number', parseInt) .option('-v, --verbose', 'Print all the things') .option('-f, --force', 'Erase file that might already exist') .option('-o --override', 'Apply a custom override to morphling') .parse(process.argv); ================================================ FILE: cli/morphling-bash.js ================================================ #! /usr/bin/env node import program from 'commander'; import shell from 'shelljs'; import chalk from 'chalk'; import _ from 'lodash'; import childProcess from 'child_process'; program .parse(process.argv); console.log(chalk.white('Container ID is:')); shell.exec('docker ps --filter "name=morphlingjs_web" -q', (code, stdout, stderr) => { if (stderr) { console.log(stderr); } else { console.log(chalk.white(`Bashing into Morphling.. Type ${chalk.bold('exit')} to exit.`)); childProcess.spawnSync('docker', ['exec', '-it', _.trim(stdout), 'bash'], { stdio: 'inherit' }) } }) ================================================ FILE: cli/morphling-config.js ================================================ #! /usr/bin/env node import program from 'commander'; import prompt from 'prompt'; import chalk from 'chalk'; import promptProps from './utils/prompt-props'; import * as utilFunctions from './utils/util'; import { deleteConfigFile } from './utils/util'; import prettyjson from 'prettyjson'; import { returnConfigIfExists } from './utils/util' program .option('-p, --port ', 'A port number', parseInt) .option('-v, --verbose', 'Print all the things') .option('-f, --force', 'Override existing config') .option('-d --delete', 'Delete existing configuration') .option('-s --show', 'Show existing configuration') .parse(process.argv); (async () => { const configFileAlreadyExists = await utilFunctions.checkConfigFileExistence(); if (program.delete) { try { await deleteConfigFile(); console.log(`${chalk.green('Config deleted')}`) } catch (e) { if (e.code === 'ENOENT') { console.log(`${chalk.red(' 😱 Config file does not exist !')}`); console.log(`${chalk.blue(`${chalk.bold('morphling config')}`)} will walk you through the config`); } else { console.error(e); } } return; } else if (program.show) { try { const config = await returnConfigIfExists(); console.log(' 👇 Current configuration'); console.log(''); console.log(prettyjson.render(config)); console.log(''); } catch(e) { if (e.code === 'ENOENT') { console.log(`${chalk.red(' 😱 Config file does not exist !')}`); console.log(`${chalk.blue(`${chalk.bold('morphling config')}`)} will walk you through the config`); } else { console.error(e); } } return; } prompt.start(); prompt.message = promptProps.message; if (configFileAlreadyExists && !program.force) { console.log(chalk.red(` 😱 Config file already exists! Use ${chalk.bold('morphling config')} with -f to override the existing config.`)); return; } prompt.get(promptProps.propertiesList.config, async (err, res) => { if (err) { console.log(err); return; } if (res.makeDefault) { const objectTosave = { port: res.port, default: res.makeDefault } try { await utilFunctions.writeConfigFile(objectTosave); } catch (e) { console.error('ERROR WHILE SAVING CONFIG', e); return; } console.log(chalk.green(` 🖖 Alright, Morphling will always run on ${res.port}!`)); console.log(chalk.white(` 😉 You can change that by doing ${chalk.bold('morphling config')} again`)); console.log(chalk.white(` 😉 Now start morphling by using ${chalk.bold('morphling start')}`)); } else { console.log(chalk.green(` 🖖 Alright, Morphling will run on ${res.port}, but config will not be saved!`)); } }) })(); ================================================ FILE: cli/morphling-describe.js ================================================ #! /usr/bin/env node import _ from 'lodash'; import program from 'commander'; import chalk from 'chalk'; import fetch from 'node-fetch' import Table from 'cli-table3'; import * as utilFunctions from './utils/util' import * as util from 'util' import prettyjson from 'prettyjson' const describeAction = async (filename) => { try { let port; const config = await utilFunctions.returnConfigIfExists(); if (config) { port = config.port; } else { port = program.port || 8883; } const url = `http://localhost:${port}/morphling/describe?filename=${filename}`; const request = await fetch(url) if (request.status === 404) { console.log(chalk.red(` 😱 File was not found. Use ${chalk.bold('morphling list -o')} to list all overrides`)); return; } const res = await request.json(); let table = new Table({ head: [ chalk.blue('Method'), chalk.blue('Route'), chalk.blue('Code'), chalk.blue('Empty Response?'), chalk.blue('Enabled'), ] }); const bodyIsEmpty = _.keys(res.body).length === 0; console.log(''); console.log(`Description for override ${chalk.bold(filename)}`); table.push([ res.method, res.route, res.httpCode, bodyIsEmpty ? 'Yes' : 'No', res.overrideStatus ? chalk.green('ON') : chalk.red('OFF'), ]); console.log(table.toString()); if (!bodyIsEmpty && !program.withBody && !program.withBodyRaw) { console.log(chalk.blue(`Use ${chalk.bold('morphling describe -b ')} to display the body`)) } if (program.withBody || program.withBodyRaw) { if (bodyIsEmpty) { console.log(chalk.red(` 😱 Body is empty!`)); } else { console.log(''); if (!program.withBodyRaw) { console.log(' 👇 Prettified body:'); console.log(''); console.log(prettyjson.render(res.body)); console.log(''); } else { console.log(' 👇 Raw body:'); console.log(''); console.log(util.inspect(res.body, { depth: null, colors: true })) console.log(''); } } } } catch (e) { if (e.code === 'ECONNREFUSED') { console.log(chalk.red(` 😱 Connection to Morphling was refused. Is Morphling currently running?`)); } console.error(e); } } program .action(describeAction) .option('-p, --port ', 'A port number', parseInt) .option('-v, --verbose', 'Print all the things') .option('-b, --with-body', 'Pretty-print the body') .option('-r, --with-body-raw', 'Print the body as-is') .parse(process.argv); if (program.args.length === 0) { console.log(chalk.red(` 😱 No filename provided. Use ${chalk.bold('morphling list -o')} to list all overrides`)); } ================================================ FILE: cli/morphling-list.js ================================================ #! /usr/bin/env node import program from 'commander'; import chalk from 'chalk'; import fetch from 'node-fetch'; import _ from 'lodash'; import Table from 'cli-table3'; import * as utilFunctions from './utils/util'; program .option('-v, --verbose', 'Print all the things') .option('-o --overrides', 'List overrides') .option('-p, --port ', 'A port number', parseInt) .parse(process.argv); (async () => { try { let port; const config = await utilFunctions.returnConfigIfExists(); if (config) { port = config.port; } else { port = program.port || 8883; } const listOverrides = !!program.overrides; const url = `http://localhost:${port}/morphling/list?list-overrides=${listOverrides}`; const request = await fetch(url) const { files } = await request.json(); if (files.length === 0) { console.log(chalk.yellow(` 🤔 No ${!listOverrides ? 'swaggers' : 'overrides'} saved on morphling!`)); return; } console.log(chalk.green(` 👇 ${!listOverrides ? 'Swaggers' : 'Overrides'} currently saved on Morphling:`)); let table = new Table({ head: [chalk.blue('File name')] }); _.each(files, (file) => { table.push([file]); }) console.log(table.toString()); } catch (e) { if (e.code === 'ECONNREFUSED') { console.log(chalk.red(` 😱 Connection to Morphling was refused. Is Morphling currently running?`)); } console.log('Error in response', e); } })() ================================================ FILE: cli/morphling-remove-all.js ================================================ #! /usr/bin/env node import program from 'commander'; import shell from 'shelljs'; import chalk from 'chalk'; program .option('-v, --verbose', 'Print all the things') .option('-p, --port ', 'A port number', parseInt) .parse(process.argv); ================================================ FILE: cli/morphling-remove.js ================================================ #! /usr/bin/env node import program from 'commander'; import shell from 'shelljs'; import chalk from 'chalk'; program .option('-v, --verbose', 'Print all the things') .option('-p, --port ', 'A port number', parseInt) .parse(process.argv); ================================================ FILE: cli/morphling-restart.js ================================================ #! /usr/bin/env node import program from 'commander'; import shell from 'shelljs'; import chalk from 'chalk'; import * as utilFunctions from './utils/util' import { getInstalledPath } from 'get-installed-path' export const restartAction = async () => { const config = await utilFunctions.returnConfigIfExists(); const port = config.port || program.port || 8883; const verbose = program.verbose || false; if (!shell.which('docker')) { console.log(chalk.red(` 🖕 Morphling requires docker to be able to run.`)) shell.exit(1); } if (!shell.which('docker-compose')) { console.log(chalk.red(` 🖕 Morphling requires docker-compose to be able to run.`)) shell.exit(1); } const installedPath = await getInstalledPath('morphlingjs'); if (!verbose) { if(!process.env.DEV){ shell.exec(`export NODE_PORT=${port} && cd ${installedPath} && docker-compose restart > /dev/null 2>&1`); } else { shell.exec(`export NODE_PORT=${port} && docker-compose restart > /dev/null 2>&1`); } } else { if(!process.env.DEV){ shell.exec(`export NODE_PORT=${port} && cd ${installedPath} && docker-compose restart`); } else { shell.exec(`export NODE_PORT=${port} && docker-compose up restart`); } } console.log(chalk.green(` 🖖 Morphling successfully restarted on ${port}`)); }; program .option('-p, --port ', 'A port number', parseInt) .option('-v, --verbose', 'Print all the things') .parse(process.argv); ================================================ FILE: cli/morphling-start.js ================================================ #! /usr/bin/env node import program from 'commander'; import shell from 'shelljs'; import chalk from 'chalk'; import * as utilFunctions from './utils/util'; import { getInstalledPath } from 'get-installed-path' program .option('-p, --port ', 'A port number', parseInt) .option('-v, --verbose', 'Print all the things') .parse(process.argv); (async () => { let port; const verbose = program.verbose || false; const config = await utilFunctions.returnConfigIfExists(); if (config) { console.log(chalk.white('Starting morphling using config file...')) port = config.port; } else { port = program.port || 8883; } if (!shell.which('docker')) { console.log(chalk.red(` 😱 Morphling requires docker to be able to run. 😱 `)) shell.exit(1); } if (!shell.which('docker-compose')) { console.log(chalk.red(` 😱 Morphling requires docker-compose to be able to run. 😱 `)) shell.exit(1); } const installedPath = await getInstalledPath('morphlingjs'); console.log(`export NODE_PORT=${port} && cd ${installedPath} && docker-compose up -d > /dev/null 2>&1`); if (!verbose) { if(!process.env.DEV){ shell.exec(`export NODE_PORT=${port} && cd ${installedPath} && docker-compose up -d > /dev/null 2>&1`); } else { shell.exec(`export NODE_PORT=${port} && docker-compose up -d > /dev/null 2>&1`); } } else { if(!process.env.DEV){ shell.exec(`export NODE_PORT=${port} && cd ${installedPath} && docker-compose up -d`); } else { shell.exec(`export NODE_PORT=${port} && docker-compose up -d`); } } console.log(chalk.green(` 🖖 Morphling started on ${port}`)); })() ================================================ FILE: cli/morphling-stop.js ================================================ #! /usr/bin/env node import program from 'commander'; import shell from 'shelljs'; import chalk from 'chalk'; import * as utilFunctions from './utils/util' import { getInstalledPath } from 'get-installed-path' program .option('-v, --verbose', 'Print all the things') .option('-p, --port ', 'A port number', parseInt) .parse(process.argv); (async () => { let port; const config = await utilFunctions.returnConfigIfExists(); if (config) { console.log(chalk.white('Starting morphling using config file...')) port = config.port; } else { port = program.port || 8883; } const verbose = program.verbose || false; if (!shell.which('docker')) { console.log(chalk.red(` 😱 Morphling requires docker to be able to run. 😱 `)) shell.exit(1); } if (!shell.which('docker-compose')) { console.log(chalk.red(` 😱 Morphling requires docker-compose to be able to run. 😱 `)) shell.exit(1); } const installedPath = await getInstalledPath('morphlingjs'); if (!verbose) { shell.exec(`cd ${installedPath} && NODE_PORT=${port} docker-compose stop > /dev/null 2>&1`); } else { shell.exec(`cd ${installedPath} && NODE_PORT=${port} docker-compose stop`) } console.log(chalk.green(`Morphling instance was killed 🖖`)); })() ================================================ FILE: cli/morphling-toggle.js ================================================ #! /usr/bin/env node import program from 'commander'; import chalk from 'chalk'; import fetch from 'node-fetch'; import * as utilFunctions from './utils/util' const toggleAction = async (filename) => { try { let port; const config = await utilFunctions.returnConfigIfExists(); if (config) { port = config.port; } else { port = program.port || 8883; } let url = `http://localhost:${port}/morphling/toggle`; if (program.enableAll) { url += '?enable=all'; } else if (program.disableAll) { url += '?enable=none'; } else { url += `?filename=${filename}`; } const request = await fetch(url); const res = await request.json(); const status = res.overrideStatus ? chalk.green('ON') : chalk.red('OFF'); if (program.enableAll || program.disableAll) { console.log(` 👍 All overrides are now ${status}`); } else { console.log(` 👍 Override ${filename} is now ${status}`); } } catch (e) { if (e.code === 'ECONNREFUSED') { console.log(chalk.red(` 😱 Connection to Morphling was refused. Is Morphling currently running?`)); return; } } } program .action(toggleAction) .option('-p, --port ', 'A port number', parseInt) .option('-v, --verbose', 'Print all the things') .option('-e, --enable-all', 'Enable all overrides') .option('-d, --disable-all', 'Disable all overrides') .parse(process.argv); if (program.args.length === 0 && !program.enableAll && !program.disableAll) { console.log(chalk.red(` 😱 No filename provided. Use ${chalk.bold('morphling list -o')} to list all overrides`)); } if (program.enableAll || program.disableAll) { (async () => { try { await toggleAction() } catch (e) { } })() } ================================================ FILE: cli/utils/prompt-props.js ================================================ import chalk from 'chalk'; export default { message: chalk.green('Morphling'), propertiesList: { config: [ { name: 'port', type: 'integer', description: chalk.white('On which port do you want to run Morphling?'), default: 8883, }, { name: 'makeDefault', type: 'string', required: true, description: chalk.white('Do you want to save that as the default morphling port (y/n)?'), pattern: /[yn]/, default: 'y', before:(value)=> value === 'y' } ], start: [ { name: 'run', type: 'boolean', required: true, description: chalk.white(`You're done! Wanna run Morphling now?`) } ] } } ================================================ FILE: cli/utils/util.js ================================================ import fs from 'fs'; import { getInstalledPath } from 'get-installed-path'; const env = process.env.DEV ? '' : '/bin'; export const writeConfigFile = async (objectToWrite) => { const installedPath = await getInstalledPath('morphlingjs'); const path = `${installedPath}${env}/cli/morphling-config.json`; return new Promise(function (resolve, reject) { fs.writeFile(path, JSON.stringify(objectToWrite), function (err) { if (err) { console.log(err); return reject(err); } return resolve(); }); }); } export const deleteConfigFile = async () => { const installedPath = await getInstalledPath('morphlingjs'); const path = `${installedPath}${env}/cli/morphling-config.json`; return new Promise(function (resolve, reject) { fs.unlink(path, (err) => { if (err) { return reject(err); } return resolve(); }); }); } export const checkConfigFileExistence = async () => { const installedPath = await getInstalledPath('morphlingjs'); const path = `${installedPath}${env}/cli/morphling-config.json`; return new Promise((resolve) => { fs.stat(path, (e) => { if (e) return resolve(false) return resolve(true) }) }); } export const readFile = async (path) => { return new Promise((resolve, reject) => { fs.readFile(path, 'utf8', (err, data) => { if (err) { return reject(err); } return resolve(data); }) }); } export const returnConfigIfExists = async () => { const installedPath = await getInstalledPath('morphlingjs'); const path = `${installedPath}${env}/cli/morphling-config.json`; return (await checkConfigFileExistence()) ? JSON.parse(await readFile(path)) : false; } export const parseFile = async () => { try { const fileContent = await readFile(path); const parsedFile = overrideFileParser(fileContent); } catch (e) { console.log(e); } } export const overrideFileParser = async (file) => { let fileContent; try { fileContent = JSON.parse(file); } catch (e) { return Promise.reject('UNPARSED_JSON') } if (!fileContent.route) { return Promise.reject(new Error('NO_ROUTE_DEFINED')); } if (!fileContent.method) { return Promise.reject(new Error('NO_METHOD_DEFINED')); } if (!fileContent.exitHttpCode) { return Promise.reject(new Error('NO_HTTP_CODE_DEFINED')); } return fileContent; } ================================================ FILE: docker-compose.yml ================================================ version: "2" services: web: build: . environment: - NODE_ENV=production - NODE_PORT=${NODE_PORT} ports: - "${NODE_PORT}:${NODE_PORT}" ================================================ FILE: makefile ================================================ start: npm install || true make build NODE_PORT=8883 make docker-build NODE_PORT=8883 make docker-up dev: DEV=true ./node_modules/.bin/babel-watch src/server.js docker-start: make docker-build make docker-up devstart-hard: make clear-build || true make clear-deps || true make clear-docker || true make start build: make clear-build || true mkdir bin || true ./node_modules/.bin/babel src -d bin/src ./node_modules/.bin/babel cli -d bin/cli mkdir bin/src/data clear-build: rm -rf bin || true clear-deps: rm -rf ./node_modules || true clear-docker: docker-compose down || true docker-up: docker-compose up docker-build: NODE_PORT=${NODE_PORT} docker-compose build ================================================ FILE: package.json ================================================ { "name": "morphlingjs", "version": "0.1.99", "description": "", "main": "index.js", "scripts": { "postinstall": "NODE_PORT=1 docker-compose build", "prepublish": "make build" }, "bin": { "morphling": "bin/cli/index.js" }, "repository": { "type": "git", "url": "https://github.com/leboncoin/morphlingjs" }, "files": [ "bin/", "Dockerfile", "docker-compose.yml", "README.md" ], "keywords": [ "mock", "swagger", "docker", "CLI" ], "author": "Valentin Roudge ", "license": "ISC", "devDependencies": { "babel-cli": "~6.24.1", "babel-core": "~6.25.0", "babel-plugin-transform-object-rest-spread": "~6.23.0", "babel-plugin-transform-runtime": "~6.23.0", "babel-preset-env": "~1.6.0", "babel-watch": "~2.0.7" }, "dependencies": { "babel-runtime": "^6.26.0", "chalk": "^2.1.0", "cli-table3": "^0.5.0", "commander": "^2.11.0", "faker": "^4.1.0", "form-data": "^2.3.1", "get-installed-path": "^4.0.8", "koa": "~2.3.0", "koa-body": "^2.3.0", "koa-router": "~7.2.1", "lodash": "~4.17.5", "lokijs": "1.5.0", "node-fetch": "^1.7.3", "prettyjson": "^1.2.1", "prompt": "^1.0.0", "shelljs": "^0.7.8", "swagger-parser": "3.4.2", "yamljs": "~0.3.0" } } ================================================ FILE: src/api/apply.js ================================================ import koaRouter from 'koa-router' import koaBody from 'koa-body' import { writeFileStream, writeFile } from '../utils/utils' const router = koaRouter(); router.post('/apply', koaBody({ multipart: true, formidable: { uploadDir: `${process.cwd()}/src/data` } }), async (ctx) => { try { const { fields, files } = ctx.request.body; const { fileName } = fields; const { file } = files; const writtenFile = await writeFileStream(fileName, file); if (ctx.query.override && ctx.query.override === 'true') { const overridenFile = JSON.parse(writtenFile); //TODO give an option in the cli to immediately enable overridenFile.overrideStatus = false; await writeFile(`${fileName}`, JSON.stringify(overridenFile)); } ctx.statusCode = 200; } catch (e) { console.error(e); ctx.throw(500); } }); export default router; ================================================ FILE: src/api/describe.js ================================================ import koaRouter from 'koa-router' import { readFile } from '../utils/utils' const router = koaRouter(); router.get('/describe', async (ctx) => { try { if (!ctx.query['filename']) { return ctx.throw(new Error('NOT_FOUND')) } const { filename } = ctx.query; let fileDir = `${process.cwd()}/src/data/morphling-override-` ctx.body = await readFile(`${fileDir}${filename}`); } catch (e) { } }); export default router; ================================================ FILE: src/api/index.js ================================================ import koaRouter from 'koa-router' import morphlingApply from './apply' import morphlingList from './list' import morphlingRemove from './remove' import morphlingRemoveAll from './remove-all' import morphlingDescribe from './describe' import morphlingToggle from './toggle' const router = koaRouter({ prefix: '/morphling' }) router .use(morphlingApply.routes(), morphlingApply.allowedMethods()) .use(morphlingList.routes(), morphlingList.allowedMethods()) .use(morphlingRemove.routes(), morphlingRemove.allowedMethods()) .use(morphlingRemoveAll.routes(), morphlingRemoveAll.allowedMethods()) .use(morphlingDescribe.routes(), morphlingDescribe.allowedMethods()) .use(morphlingToggle.routes(), morphlingToggle.allowedMethods()); export default router; ================================================ FILE: src/api/list.js ================================================ import _ from 'lodash'; import koaRouter from 'koa-router' import { checkFileExistence, readDir } from '../utils/utils' const router = koaRouter(); router.get('/list', async (ctx) => { try { if (ctx.query.filename) { const fileExists = await checkFileExistence(`${process.cwd()}/src/data/${ctx.query.filename}`); ctx.body = fileExists ? { fileExists: true } : { fileExists: false }; } else if (ctx.query['list-overrides'] === 'true') { const files = await readDir(`${process.cwd()}/src/data/`, (files) => { return _.filter(files, file => file.includes('morphling-override')) .map(file => file.replace('morphling-override-', '')) } ); ctx.body = { files }; } else { const files = await readDir(`${process.cwd()}/src/data/`, (files) => { return _.filter(files, file => !file.includes('morphling-override') && !['db.json', '.gitkeep'].includes(file) ); }); ctx.body = { files }; } } catch (e) { console.log('Error in get list', e); } }); export default router; ================================================ FILE: src/api/middleware/error.js ================================================ export default async (ctx, next) => { try { await next(); } catch (err) { // will only respond with JSON ctx.status = err.code || 500; ctx.body = { message: err.message }; } } ================================================ FILE: src/api/middleware/loki.js ================================================ import { getDB } from '../../db/index' export default async(ctx, next)=> { try { ctx.db = getDB() await next(); } catch(e) { console.log('Error in getDB', e); } } ================================================ FILE: src/api/remove-all.js ================================================ import koaRouter from 'koa-router' const router = koaRouter(); router.delete('/remove-all', async (ctx) => { ctx.body = 'OK' }); export default router; ================================================ FILE: src/api/remove.js ================================================ import koaRouter from 'koa-router' const router = koaRouter(); router.delete('/remove', async (ctx) => { ctx.body = 'OK' }); export default router; ================================================ FILE: src/api/toggle.js ================================================ import _ from 'lodash'; import koaRouter from 'koa-router' import { readDir, readFile, writeFile } from '../utils/utils' const router = koaRouter(); router.get('/toggle', async (ctx) => { try { if (ctx.query['filename']) { const { filename } = ctx.query; let file = `${process.cwd()}/src/data/morphling-override-${filename}`; const fileToToggle = JSON.parse(await readFile(file)); fileToToggle.overrideStatus = !fileToToggle.overrideStatus; await writeFile(`morphling-override-${filename}`, fileToToggle); ctx.body = { overrideStatus: fileToToggle.overrideStatus }; } else if (ctx.query['enable']) { const trueIfEnableAll = ctx.query['enable'] === 'all'; const files = await readDir(`${process.cwd()}/src/data/`, (files) => { return _.filter(files, file => file.includes('morphling-override') ); }); await Promise.all( _.map(files, async (file) => { const fileToToggle = JSON.parse(await readFile(`${process.cwd()}/src/data/${file}`)); fileToToggle.overrideStatus = trueIfEnableAll; return await writeFile(file, fileToToggle); }) ); ctx.body = { overrideStatus: trueIfEnableAll }; } else { ctx.throw(404); } } catch (e) { ctx.throw(e); } }); export default router; ================================================ FILE: src/data/.gitkeep ================================================ ================================================ FILE: src/db/collection.js ================================================ export const findOne = (collection, value, eq) => { const query = {}; query[value] = { '$eq': eq } return collection.find(query)[0] } /** * Creates a collection in database * @param db database instance * @param modelName name of model * @param opts other options (See lokiJS docs) * @returns {Collection|*} */ export const createCollection = (db, modelName, opts) => { const entries = db.getCollection(modelName); if (entries) { throw new Error('Collection already exists'); } return db.addCollection(modelName, opts) } /** * Get collection from database * @param db database instance * @param modelName * @returns {Collection} */ export const findCollection = (db, modelName) => { const entries = db.getCollection(modelName); if (!entries) { throw new Error('MODEL_DOES_NOT_EXIST') } return entries; } ================================================ FILE: src/db/fixtures.js ================================================ import _ from 'lodash' import faker from 'faker' import { getOneModel, createModel } from './index' import { createCollection, findCollection, insertOne } from './collection' /** * Create all fixtures from a given list of parsed swagger definitions * @param db A LokiJS DB instance * @param definitions A list of parsed swagger definitions */ export const createFixturesFromDefinitions = (db, definitions) => { try { const createdModels = _.map(definitions, definition => { // store all models in datase return createModel(db, definition) }) // after all models are stored in database, we can start to generate fixtures for all // since we have all references stored return _.map(createdModels, model => { const fixtures = _.map(_.range(5), iter => generateOneFixture(db, model)); const { modelName } = model; let currentModelCollection; try { currentModelCollection = findCollection(db, modelName) } catch (e) { if (e.message === 'MODEL_DOES_NOT_EXIST') { currentModelCollection = createCollection(db, modelName) } } return currentModelCollection.insert(fixtures) }) } catch (e) { console.error(e); } } /** * Create one fixture from a given model * @param db Loki DB instance * @param model model content * @returns {{}} */ const generateOneFixture = (db, model) => { const { type, modelName } = model; switch (type) { case 'object': const { properties } = model; //handle dictionnaries (additonalProperties instead of properties) if (!properties) { const fixture = {} //TODO handle maps or object with additionalProperties here return fixture; } else { const fixture = {}; fixture[modelName] = _.reduce(properties, (acc, prop, propName) => generate(db, acc, prop, propName, modelName), {}) return fixture; } break; case 'array': const fixture = {} fixture[modelName] = _.map(_.range(5), (elem) => generate(db, {}, model.items, modelName)[modelName]); return fixture break; default: throw new Error('Non handled generable type', { model, type }) break; } } /** * Get reference model name from reference string * @param {string} item */ const cleanPropertyReference = (item) => { return item.replace(`#/definitions/`, '') }; /** * Generate one fixture * @param db * @param accumulator * @param property * @param propertyName * @param parentModelName * @returns {*} */ const generate = (db, accumulator, property, propertyName, parentModelName) => { accumulator[propertyName] = accumulator[propertyName] ? accumulator[propertyName] : {} const mapping = { 'integer': faker.random.number, 'number': faker.random.number, 'string': faker.random.word, 'boolean': faker.random.boolean, } //TODO make sure that we handle enum fields with non-random values const customMapping = { 'array': (property, propertyName) => { const { items } = property; //handle case where array is an array of references to another object //we get the model from database and then generate from it; if (items['$ref']) { //TODO chain 5 times const modelName = cleanPropertyReference(items['$ref']); const model = getOneModel(db, modelName); // if the current parsed model is the same than its container it is a circular reference if (modelName === parentModelName) { return 'MORPHLING_CIRCULAR_REFERENCE' } return _.map(_.range(5), elem => generateOneFixture(db, model)); } //else we just generate an array of fixtures return _.map(_.range(5), elem => generate(db, {}, items, propertyName)[propertyName]); }, 'lat': faker.address.latitude, 'lng': faker.address.longitude, 'zipcode': faker.address.zipCode, 'address': faker.address.streetAddress, } const fakerGenerators = { ...faker.random, ...faker.address, ...faker.company, ...faker.finance, ...faker.internet, ...faker.name, ...faker.random, }; //it is a reference to another object, keep reference for further generation down the line if (property['$ref']) { const modelName = cleanPropertyReference(property['$ref']); const model = getOneModel(db, modelName); //console.log(generateOneFixture(db, model)); accumulator[propertyName] = generateOneFixture(db, model); } // else a manual override exists for a given property else if (customMapping[propertyName]) { accumulator[propertyName] = customMapping[propertyName](); } //else a faker generator with the same name exists (using it will give more realistic data) else if (fakerGenerators[propertyName]) { accumulator[propertyName] = fakerGenerators[propertyName](); } //else use property type to generate the data else if (mapping[property.type]) { accumulator[propertyName] = mapping[property.type](); } //else object is an array, we need to recure to generate else if (property.type === 'array') { accumulator[propertyName] = customMapping['array'](property, propertyName); } //keep property as is if no field where generation could be done was found else { //TODO maybe example ? accumulator[propertyName] = property; } return accumulator; } ================================================ FILE: src/db/index.js ================================================ import Loki from 'lokijs' import _ from 'lodash' import * as collectionUtils from './collection' export const MODEL_TABLE_NAME = 'models' export const MODEL_NAME_FIELD = 'modelName' export const FIXTURE_FILE_NAME = 'db.json' let db; /** * Initalizes the LokiJS database instance * @returns {Promise.<*>} */ export const getDB = () => { if (!db) { db = new Loki(`${process.cwd()}/src/data/${FIXTURE_FILE_NAME}`, { autosave: true, autosaveInterval: 100 }); db.save(); } return db; } /** * Creates a model in database model collection if it does not exist * Also creates the model table if that table does not exist * @param db database instance * @param definition parsed swagger model definition * @returns {object} inserted model instance */ export const createModel = (db, definition) => { let modelCollection try { modelCollection = collectionUtils.findCollection(db, MODEL_TABLE_NAME) } catch (e) { if (e.message === 'MODEL_DOES_NOT_EXIST') { modelCollection = collectionUtils.createCollection(db, MODEL_TABLE_NAME, { unique: [MODEL_NAME_FIELD] }) } else { console.error('Something went wrong while creating the models table.') } } let foundModel = collectionUtils.findOne(modelCollection, MODEL_NAME_FIELD, definition.modelName) if (!foundModel) { modelCollection.insert(definition); foundModel = collectionUtils.findOne(modelCollection, MODEL_NAME_FIELD, definition.modelName) } return foundModel; } /** * Ensure there are no duplicates in model names * @param models array of model names * @returns {Array} */ export const validateModelNames = (models) => { if (_.uniq(models).length !== _.keys(models).length) { console.warn('BEWARE, MODELS ARE DUPLICATED BETWEEN SWAGGERS. REMOVING DUPLICATES.'); } return _.uniq(models); } /** * Get one model from DB * @param db * @param modelName */ export const getOneModel = (db, modelName) => { const modelTable = collectionUtils.findCollection(db, MODEL_TABLE_NAME) return collectionUtils.findOne(modelTable, MODEL_NAME_FIELD, modelName) } ================================================ FILE: src/server.js ================================================ import Koa from 'koa' import _ from 'lodash'; import apiRouter from './api' import errorMiddleware from './api/middleware/error' import lokiMiddleware from './api/middleware/loki' import Morphling from './utils/morphling'; import overrideRouter from './utils/override-router'; const serverPort = process.env.NODE_PORT || 8883; (async () => { const app = new Koa() app.use(errorMiddleware) app.use(lokiMiddleware) app.use(apiRouter.routes(), apiRouter.allowedMethods()) try { const overrideRouters = await overrideRouter(); _.map(overrideRouters, (router)=>{ app.use(router.routes(), router.allowedMethods()); }); const morphlingInstance = await Morphling(); _.map(morphlingInstance, (router)=>{ app.use(router.routes(), router.allowedMethods()); }); } catch(e) { console.error(e); } app.on('error', (err, ctx) => { console.error('server error', err, ctx) }) app.listen(serverPort) console.log(`Morphling ready on port ${serverPort}`) })() ================================================ FILE: src/utils/createRouterFromSwagger.js ================================================ import koaRouter from 'koa-router' import _ from 'lodash' import { findCollection } from '../db/collection' /** * Put all the other stuff down there together * * @param db * @param basePath * @param paths * @returns {*} */ export default (db, { basePath, paths }) => { //remove last slash of route if there is one const cleanedBasePath = basePath[basePath.length - 1] !== '/' ? basePath : basePath.slice(0, -1); //create router container let router = koaRouter({ prefix: cleanedBasePath || '' }) _.each(paths, (pathOptions, route) => { //get methods without parameters const routerMethods = _.compact(_.map(_.keys(pathOptions), key => key !== 'parameters' ? key : null)) // mount methods to router if it has any if (routerMethods.length) { router = addMethodsToRouter(db, router, { route, routerMethods, pathOptions }) } }) return router } /** * Add methods + bind callbacks to the router + bind parameters * @param db * @param router * @param route * @param routerMethods * @param pathOptions * @returns {*} */ const addMethodsToRouter = (db, router, { route, routerMethods, pathOptions }) => { const formattedRouteParameters = createAliasesForParameters(route) const formattedRoute = _.reduce(formattedRouteParameters, (acc, elem) => { return acc.replace(elem.name, `:${elem.alias}`).replace(/[{}]/g, '') }, route) let routerRef = router _.each(routerMethods, (method) => { //create route const { responses, parameters } = pathOptions[method] const getter = responseGetter(db, responses); router[method](formattedRoute, getter); //add parameters link to route if there are any to add if (formattedRouteParameters) { const swaggerOptions = _.filter(pathOptions[method].parameters, { in: 'path' }) router = addUrlParamToRouter(router, formattedRouteParameters, swaggerOptions) } //add handlers to route }) return routerRef } /** * Create route callback function, defining randomly how many fixtures we will return * and which one exactly from the local db * * @param db * @param responses * @returns {function(*)} */ const responseGetter = (db, responses) => { let body; const defaultSuccess = responses[200] || responses[201] || responses[204]; return ((ctx) => { if (defaultSuccess && defaultSuccess.schema) { const objectRef = getReferencesAndCount(defaultSuccess.schema); const collection = findCollection(db, objectRef.object); body = _.take( _.shuffle( _.map( collection.find(), `${objectRef.object}` ) ) , objectRef.fixtureCount); if (defaultSuccess.schema.type === 'array') { ctx.body = body; } else if (defaultSuccess.schema['$ref']) { ctx.body = body[0]; } return; } else if (defaultSuccess) { const possibleCodes = [200, 201, 204]; for (const code of possibleCodes){ if(responses[code]){ ctx.status = code; break; } } return; } ctx.throw('Unsupported format! Can you create an issue in Github about that?') }); //2 handle parameters in body (in: body) // -> is body required ? // -> validate against schema //3 define if should give success or error through override // -> if so, do return error from schema //4 get smallest response code and accept it as default one } /** * Get the fixture that should be return + a random number * * @param reference * @returns {{fixtureCount: number, object}} */ const getReferencesAndCount = (reference) => { if (reference['$ref']) { const object = reference['$ref'].replace('#/definitions/', ''); return { fixtureCount: 1, object } } else if (reference.items) { const object = reference.items['$ref'].replace('#/definitions/', ''); return { fixtureCount: (Math.round(Math.random() * 5)) || 1, object } } else { return { fixtureCount: 0, object: reference.description } } } /** * Format parameters * * @param route * @returns {*} */ const createAliasesForParameters = (route) => { const urlParams = route.match(/{(.*)}/g) return _.reduce(urlParams, (acc, param) => { const rawParameter = param.replace(/[{}]/g, '') if (!acc[rawParameter]) { acc[rawParameter] = { name: rawParameter, alias: param.replace(/\-/g, '') } } return acc }, {}) } /** * Binds parameters to the router so they can be correctly recognized on query * @param router * @param formattedRouteParameters * @param swaggerOptions * @returns {*} */ const addUrlParamToRouter = (router, formattedRouteParameters, swaggerOptions) => { const routerRef = router _.each(formattedRouteParameters, ({ name }) => { //get details of parameter (required, type...) const swaggerOptionsForParameter = _.find(swaggerOptions, { name }); routerRef.param(name, async (value, ctx, next) => { //check that parameter exists if (swaggerOptionsForParameter.required) { if (!value) { return ctx.throw(`Parameter ${name} is missing`) } } return next() }) }) return routerRef } ================================================ FILE: src/utils/morphling.js ================================================ import _ from 'lodash' import swaggerParser from 'swagger-parser' import YAML from 'yamljs' import { getDB, validateModelNames } from '../db' import { createFixturesFromDefinitions } from '../db/fixtures' import createRouterFromSwagger from './createRouterFromSwagger' import { readDir, readFile } from './utils' const SOURCE_FILES_LOCATION = `${process.cwd()}/src/data`; /** * Read swagger files in data directory and pass them to the fixture + routes creator * @returns {Promise.} */ export default async () => { try { const localSources = await readDir(SOURCE_FILES_LOCATION, (files) => { return _.filter(files, file => !file.includes('morphling-override') && !['db.json', '.gitkeep'].includes(file) ); }); const swaggers = await Promise.all( _.map(localSources, async (fileName) => { const fileContent = await readFile(`${SOURCE_FILES_LOCATION}/${fileName}`); if (fileName.includes('.json')) { return JSON.parse(fileContent); } else if (fileName.includes('.yml') || fileName.includes('.yaml')) { return YAML.parse(fileContent) } }) ) return await Morphling(swaggers); } catch (e) { console.log('Error while fetching local swaggers', e); } } /** * Takes an array of swaggers in, returns a router with bound fixtures out * @param swaggers * @returns {Promise.} * @constructor */ const Morphling = async (swaggers) => { const db = getDB() try { const parsedSwaggers = await Promise.all( _.map(swaggers, (elem) => { return parseSwagger(elem) })); const allModelDefinitions = _.merge(..._.map(parsedSwaggers, (swagger) => { return _.map(swagger.definitions, (def, defName) => ({ modelName: defName, ...def })) })); const cleanedModelDefinitions = validateModelNames(allModelDefinitions); const fixtures = createFixturesFromDefinitions(db, cleanedModelDefinitions); return _.map(swaggers, (source) => { const { basePath, paths, } = source; return createRouterFromSwagger(db, { basePath, paths }) }) } catch (e) { console.error(e) } } /** * Swagger parser, dont not parse circular references because JSON cannot handle that * @param source * @returns {Promise.<*>} */ const parseSwagger = async (source) => { return await swaggerParser.bundle(source, { $refs: { circular: 'ignore' } }); } ================================================ FILE: src/utils/override-router.js ================================================ import koaRouter from 'koa-router' import _ from 'lodash' import { readDir, readFile } from './utils' const SOURCE_FILES_LOCATION = `${process.cwd()}/src/data`; /** * Create a router for a given override file * * The route is created during the boot, but we check again * the override status during the request, to have working toggle * * @param name * @param overrideContent * @returns {*} */ const createRouter = (name, overrideContent) => { const router = koaRouter(); const { httpCode, body, route, method } = JSON.parse(overrideContent); const lowerCasedMethod = method.toLowerCase(); router[lowerCasedMethod](route, async (ctx) => { console.log('pass'); const { overrideStatus } = JSON.parse(await readFile(`${SOURCE_FILES_LOCATION}/${name}`)); if(overrideStatus){ ctx.body = _.keys(body).length > 0 ? body : null; ctx.status = httpCode; } }); return router; } /** * Read override files in data directory and return an array of koa routers ready for use * @returns {Promise.<*[]>} */ export default async () => { try { const overrideFiles = await readDir(SOURCE_FILES_LOCATION, (files) => { return _.filter(files, file => file.includes('morphling-override')); }); return await Promise.all( _.map(overrideFiles, async (fileName) => { const fileContent = await readFile(`${SOURCE_FILES_LOCATION}/${fileName}`); return createRouter(fileName, fileContent) }) ) } catch (e) { console.error(e); } } ================================================ FILE: src/utils/utils.js ================================================ import fs from 'fs' import _ from 'lodash'; /** * Read file content in path * @param path * @returns {Promise} */ export const readFile = async (path) => { return new Promise((resolve, reject) => { fs.readFile(path, 'utf8', (err, data) => { if (err) { return reject(err); } return resolve(data); }) }); } /** * Write file in path from other file * @returns {Promise} * @param fileName * @param file */ export const writeFileStream = async (fileName, file) => { try { const readStream = fs.createReadStream(file.path); const writeStream = fs.createWriteStream(`${process.cwd()}/src/data/${fileName}`); readStream.pipe(writeStream); fs.unlink(file.path); return await readFile(`${process.cwd()}/src/data/${fileName}`); } catch (e) { console.log('Error while writing file!', e); } } /** * Write file in path * @param filename file name * @param content JSON object * @returns {Promise} */ export const writeFile = async (filename, content) => { const contentToWrite = _.isObject(content) ? JSON.stringify(content) : content; return new Promise(function (resolve, reject) { fs.writeFile(`${process.cwd()}/src/data/${filename}`, contentToWrite, 'utf8', (err, res) => { if (err) { return reject(err); } return resolve(res); }); }); } /** * check if file exists * @returns {Promise} * @param path */ export const checkFileExistence = async (path) => { return new Promise((resolve) => { fs.stat(path, (e) => { if (e) return resolve(false) return resolve(true) }) }); } /** * Read directory file names * @param path path to directory * @param exclude files to filter out of exit * @returns {Promise} resolved filenames */ export const readDir = async (path, exclude) => { return new Promise((resolve, reject) => { fs.readdir(path, (err, files) => { if (err) { return reject(err); } if (exclude) { if (typeof exclude === 'function') { return resolve(exclude(files)) } return resolve( _.filter(files, file => !exclude.includes(file)) ); } return resolve(files); }) }); } ================================================ FILE: swagger-examples/example.yaml ================================================ --- swagger: "2.0" info: version: "1.0.0" title: "Swagger Petstore" termsOfService: "http://swagger.io/terms/" contact: email: "apiteam@swagger.io" license: name: "Apache 2.0" url: "http://www.apache.org/licenses/LICENSE-2.0.html" host: "petstore.swagger.io" basePath: "/v2/" tags: - name: "pet" description: "Everything about your Pets" externalDocs: description: "Find out more" url: "http://swagger.io" - name: "store" description: "Access to Petstore orders" - name: "user" description: "Operations about user" externalDocs: description: "Find out more about our store" url: "http://swagger.io" schemes: - "http" paths: /pet: post: tags: - "pet" summary: "Add a new pet to the store" description: "" operationId: "addPet" consumes: - "application/json" - "application/xml" produces: - "application/xml" - "application/json" parameters: - in: "body" name: "body" description: "Pet object that needs to be added to the store" required: true schema: $ref: "#/definitions/Pet" responses: 405: description: "Invalid input" security: - petstore_auth: - "write:pets" - "read:pets" put: tags: - "pet" summary: "Update an existing pet" description: "" operationId: "updatePet" consumes: - "application/json" - "application/xml" produces: - "application/xml" - "application/json" parameters: - in: "body" name: "body" description: "Pet object that needs to be added to the store" required: true schema: $ref: "#/definitions/Pet" responses: 400: description: "Invalid ID supplied" 404: description: "Pet not found" 405: description: "Validation exception" security: - petstore_auth: - "write:pets" - "read:pets" /pet/findByStatus: get: tags: - "pet" summary: "Finds Pets by status" description: "Multiple status values can be provided with comma separated strings" operationId: "findPetsByStatus" produces: - "application/xml" - "application/json" parameters: - name: "status" in: "query" description: "Status values that need to be considered for filter" required: true type: "array" items: type: "string" enum: - "available" - "pending" - "sold" default: "available" collectionFormat: "multi" responses: 200: description: "successful operation" schema: type: "array" items: $ref: "#/definitions/Pet" 400: description: "Invalid status value" security: - petstore_auth: - "write:pets" - "read:pets" /pet/findByTags: get: tags: - "pet" summary: "Finds Pets by tags" operationId: "findPetsByTags" produces: - "application/xml" - "application/json" parameters: - name: "tags" in: "query" description: "Tags to filter by" required: true type: "array" items: type: "string" collectionFormat: "multi" responses: 200: description: "successful operation" schema: type: "array" items: $ref: "#/definitions/Pet" 400: description: "Invalid tag value" security: - petstore_auth: - "write:pets" - "read:pets" deprecated: true /pet/{petId}: get: tags: - "pet" summary: "Find pet by ID" description: "Returns a single pet" operationId: "getPetById" produces: - "application/xml" - "application/json" parameters: - name: "petId" in: "path" description: "ID of pet to return" required: true type: "integer" format: "int64" responses: 200: description: "successful operation" schema: $ref: "#/definitions/Pet" 400: description: "Invalid ID supplied" 404: description: "Pet not found" security: - api_key: [] post: tags: - "pet" summary: "Updates a pet in the store with form data" description: "" operationId: "updatePetWithForm" consumes: - "application/x-www-form-urlencoded" produces: - "application/xml" - "application/json" parameters: - name: "petId" in: "path" description: "ID of pet that needs to be updated" required: true type: "integer" format: "int64" - name: "name" in: "formData" description: "Updated name of the pet" required: false type: "string" - name: "status" in: "formData" description: "Updated status of the pet" required: false type: "string" responses: 405: description: "Invalid input" security: - petstore_auth: - "write:pets" - "read:pets" delete: tags: - "pet" summary: "Deletes a pet" description: "" operationId: "deletePet" produces: - "application/xml" - "application/json" parameters: - name: "api_key" in: "header" required: false type: "string" - name: "petId" in: "path" description: "Pet id to delete" required: true type: "integer" format: "int64" responses: 400: description: "Invalid ID supplied" 404: description: "Pet not found" security: - petstore_auth: - "write:pets" - "read:pets" /pet/{petId}/uploadImage: post: tags: - "pet" summary: "uploads an image" description: "" operationId: "uploadFile" consumes: - "multipart/form-data" produces: - "application/json" parameters: - name: "petId" in: "path" description: "ID of pet to update" required: true type: "integer" format: "int64" - name: "additionalMetadata" in: "formData" description: "Additional data to pass to server" required: false type: "string" - name: "file" in: "formData" description: "file to upload" required: false type: "file" responses: 200: description: "successful operation" schema: $ref: "#/definitions/ApiResponse" security: - petstore_auth: - "write:pets" - "read:pets" /store/inventory: get: tags: - "store" summary: "Returns pet inventories by status" description: "Returns a map of status codes to quantities" operationId: "getInventory" produces: - "application/json" parameters: [] responses: 200: description: "successful operation" schema: type: "object" additionalProperties: type: "integer" format: "int32" security: - api_key: [] /store/order: post: tags: - "store" summary: "Place an order for a pet" description: "" operationId: "placeOrder" produces: - "application/xml" - "application/json" parameters: - in: "body" name: "body" description: "order placed for purchasing the pet" required: true schema: $ref: "#/definitions/Order" responses: 200: description: "successful operation" schema: $ref: "#/definitions/Order" 400: description: "Invalid Order" /store/order/{orderId}: get: tags: - "store" summary: "Find purchase order by ID" operationId: "getOrderById" produces: - "application/xml" - "application/json" parameters: - name: "orderId" in: "path" description: "ID of pet that needs to be fetched" required: true type: "integer" maximum: 10.0 minimum: 1.0 format: "int64" responses: 200: description: "successful operation" schema: $ref: "#/definitions/Order" 400: description: "Invalid ID supplied" 404: description: "Order not found" delete: tags: - "store" summary: "Delete purchase order by ID" operationId: "deleteOrder" produces: - "application/xml" - "application/json" parameters: - name: "orderId" in: "path" description: "ID of the order that needs to be deleted" required: true type: "integer" minimum: 1.0 format: "int64" responses: 400: description: "Invalid ID supplied" 404: description: "Order not found" /user: post: tags: - "user" summary: "Create user" description: "This can only be done by the logged in user." operationId: "createUser" produces: - "application/xml" - "application/json" parameters: - in: "body" name: "body" description: "Created user object" required: true schema: $ref: "#/definitions/User" responses: default: description: "successful operation" /user/createWithArray: post: tags: - "user" summary: "Creates list of users with given input array" description: "" operationId: "createUsersWithArrayInput" produces: - "application/xml" - "application/json" parameters: - in: "body" name: "body" description: "List of user object" required: true schema: type: "array" items: $ref: "#/definitions/User" responses: default: description: "successful operation" /user/createWithList: post: tags: - "user" summary: "Creates list of users with given input array" description: "" operationId: "createUsersWithListInput" produces: - "application/xml" - "application/json" parameters: - in: "body" name: "body" description: "List of user object" required: true schema: type: "array" items: $ref: "#/definitions/User" responses: default: description: "successful operation" /user/login: get: tags: - "user" summary: "Logs user into the system" description: "" operationId: "loginUser" produces: - "application/xml" - "application/json" parameters: - name: "username" in: "query" description: "The user name for login" required: true type: "string" - name: "password" in: "query" description: "The password for login in clear text" required: true type: "string" responses: 200: description: "successful operation" schema: type: "string" headers: X-Rate-Limit: type: "integer" format: "int32" description: "calls per hour allowed by the user" X-Expires-After: type: "string" format: "date-time" description: "date in UTC when token expires" 400: description: "Invalid username/password supplied" /user/logout: get: tags: - "user" summary: "Logs out current logged in user session" description: "" operationId: "logoutUser" produces: - "application/xml" - "application/json" parameters: [] responses: default: description: "successful operation" /user/{username}: get: tags: - "user" summary: "Get user by user name" description: "" operationId: "getUserByName" produces: - "application/xml" - "application/json" parameters: - name: "username" in: "path" description: "The name that needs to be fetched. Use user1 for testing. " required: true type: "string" responses: 200: description: "successful operation" schema: $ref: "#/definitions/User" 400: description: "Invalid username supplied" 404: description: "User not found" put: tags: - "user" summary: "Updated user" description: "This can only be done by the logged in user." operationId: "updateUser" produces: - "application/xml" - "application/json" parameters: - name: "username" in: "path" description: "name that need to be updated" required: true type: "string" - in: "body" name: "body" description: "Updated user object" required: true schema: $ref: "#/definitions/User" responses: 400: description: "Invalid user supplied" 404: description: "User not found" delete: tags: - "user" summary: "Delete user" description: "This can only be done by the logged in user." operationId: "deleteUser" produces: - "application/xml" - "application/json" parameters: - name: "username" in: "path" description: "The name that needs to be deleted" required: true type: "string" responses: 400: description: "Invalid username supplied" 404: description: "User not found" securityDefinitions: petstore_auth: type: "oauth2" authorizationUrl: "http://petstore.swagger.io/oauth/dialog" flow: "implicit" scopes: write:pets: "modify pets in your account" read:pets: "read your pets" api_key: type: "apiKey" name: "api_key" in: "header" definitions: Order: type: "object" properties: id: type: "integer" format: "int64" petId: type: "integer" format: "int64" quantity: type: "integer" format: "int32" shipDate: type: "string" format: "date-time" status: type: "string" description: "Order Status" enum: - "placed" - "approved" - "delivered" complete: type: "boolean" default: false xml: name: "Order" Category: type: "object" properties: id: type: "integer" format: "int64" name: type: "string" xml: name: "Category" User: type: "object" properties: id: type: "integer" format: "int64" username: type: "string" firstName: type: "string" lastName: type: "string" email: type: "string" password: type: "string" phone: type: "string" userStatus: type: "integer" format: "int32" description: "User Status" xml: name: "User" Tag: type: "object" properties: id: type: "integer" format: "int64" name: type: "string" xml: name: "Tag" Pet: type: "object" required: - "name" - "photoUrls" properties: id: type: "integer" format: "int64" category: $ref: "#/definitions/Category" name: type: "string" example: "doggie" photoUrls: type: "array" xml: name: "photoUrl" wrapped: true items: type: "string" tags: type: "array" xml: name: "tag" wrapped: true items: $ref: "#/definitions/Tag" status: type: "string" description: "pet status in the store" enum: - "available" - "pending" - "sold" xml: name: "Pet" ApiResponse: type: "object" properties: code: type: "integer" format: "int32" type: type: "string" message: type: "string" externalDocs: description: "Find out more about Swagger" url: "http://swagger.io" ================================================ FILE: swagger-override-examples/demo-with-body.json ================================================ { "route": "/v2/user/lololo", "body": { "hello": "world", "foo": [12,123,1234], "top":{ "kek":"lololo" } }, "empty": false, "httpCode": 200, "method": "GET" } ================================================ FILE: swagger-override-examples/demo.json ================================================ { "route": "/v2/store/order/1", "body": { }, "empty": true, "httpCode": 200, "method": "GET" }