Full Code of leboncoin/morphlingjs for AI

master 383a274a57d6 cached
45 files
76.7 KB
19.5k tokens
5 symbols
1 requests
Download .txt
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 <file>         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 <filename>  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 <filename>    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 <file>', '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 <filename>', '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 <filename>', '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 <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 <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 <filename>')} 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 <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 <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 <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 <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 <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 <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 <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 <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 <v.roudge@gmail.com>",
  "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.<void>}
 */
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.<void>}
 * @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"
}
Download .txt
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
Download .txt
SYMBOL INDEX (5 symbols across 3 files)

FILE: src/db/index.js
  constant MODEL_TABLE_NAME (line 6) | const MODEL_TABLE_NAME = 'models'
  constant MODEL_NAME_FIELD (line 7) | const MODEL_NAME_FIELD = 'modelName'
  constant FIXTURE_FILE_NAME (line 8) | const FIXTURE_FILE_NAME = 'db.json'

FILE: src/utils/morphling.js
  constant SOURCE_FILES_LOCATION (line 10) | const SOURCE_FILES_LOCATION = `${process.cwd()}/src/data`;

FILE: src/utils/override-router.js
  constant SOURCE_FILES_LOCATION (line 5) | const SOURCE_FILES_LOCATION = `${process.cwd()}/src/data`;
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (85K chars).
[
  {
    "path": ".babelrc",
    "chars": 113,
    "preview": "{\n  \"presets\": [\n    \"env\"\n  ],\n  \"plugins\": [\n    \"transform-runtime\",\n    \"transform-object-rest-spread\"\n  ]\n}\n"
  },
  {
    "path": ".dockerignore",
    "chars": 67,
    "preview": "node_modules\nnpm-debug.log\n.idea\nsrc/fileSources/remoteFiles/*.yml\n"
  },
  {
    "path": ".gitignore",
    "chars": 131,
    "preview": ".idea/\nbin/\nnode_modules/\n\nnpm-debug.log\n\ncli/morphling-config.json\n\nsrc/data/*.yaml\nsrc/data/*.yml\nsrc/data/*.json\n\n*.t"
  },
  {
    "path": ".npmignore",
    "chars": 23,
    "preview": "src\ncli\n.babelrc\n.idea\n"
  },
  {
    "path": "Dockerfile",
    "chars": 279,
    "preview": "FROM node:8.2.1\nRUN mkdir -p /usr/app\nWORKDIR /usr/app\n\nCOPY ./package.json ./\nRUN npm install\nRUN npm install pm2 -g --"
  },
  {
    "path": "LICENSE.md",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2017 Leboncoin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "README.md",
    "chars": 6760,
    "preview": "# Morphling\n\n*Cause nobody aint got time to wait for back-ends to be developped.*\n\n\n## Features\n\n- A sweet all-in-CLI wi"
  },
  {
    "path": "cli/index.js",
    "chars": 970,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\n\nprogram\n    .version('0.0.1')\n    .description('CLI interface fo"
  },
  {
    "path": "cli/morphling-apply.js",
    "chars": 2297,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport FormData from 'form-data';\nimport chalk from 'chalk';\nimpo"
  },
  {
    "path": "cli/morphling-bash.js",
    "chars": 638,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport shell from 'shelljs';\nimport chalk from 'chalk';\nimport _ "
  },
  {
    "path": "cli/morphling-config.js",
    "chars": 3156,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport prompt from 'prompt';\nimport chalk from 'chalk';\nimport pr"
  },
  {
    "path": "cli/morphling-describe.js",
    "chars": 3173,
    "preview": "#! /usr/bin/env node\n\nimport _ from 'lodash';\nimport program from 'commander';\nimport chalk from 'chalk';\nimport fetch f"
  },
  {
    "path": "cli/morphling-list.js",
    "chars": 1601,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport chalk from 'chalk';\nimport fetch from 'node-fetch';\nimport"
  },
  {
    "path": "cli/morphling-remove-all.js",
    "chars": 259,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport shell from 'shelljs';\nimport chalk from 'chalk';\n\nprogram\n"
  },
  {
    "path": "cli/morphling-remove.js",
    "chars": 259,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport shell from 'shelljs';\nimport chalk from 'chalk';\n\nprogram\n"
  },
  {
    "path": "cli/morphling-restart.js",
    "chars": 1573,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport shell from 'shelljs';\nimport chalk from 'chalk';\nimport * "
  },
  {
    "path": "cli/morphling-start.js",
    "chars": 1780,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport shell from 'shelljs';\nimport chalk from 'chalk';\n\nimport *"
  },
  {
    "path": "cli/morphling-stop.js",
    "chars": 1352,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport shell from 'shelljs';\nimport chalk from 'chalk';\nimport * "
  },
  {
    "path": "cli/morphling-toggle.js",
    "chars": 1937,
    "preview": "#! /usr/bin/env node\n\nimport program from 'commander';\nimport chalk from 'chalk';\nimport fetch from 'node-fetch';\n\nimpor"
  },
  {
    "path": "cli/utils/prompt-props.js",
    "chars": 932,
    "preview": "import chalk from 'chalk';\n\nexport default {\n    message: chalk.green('Morphling'),\n    propertiesList: {\n        config"
  },
  {
    "path": "cli/utils/util.js",
    "chars": 2637,
    "preview": "import fs from 'fs';\nimport { getInstalledPath } from 'get-installed-path';\n\nconst env = process.env.DEV ? '' : '/bin';\n"
  },
  {
    "path": "docker-compose.yml",
    "chars": 166,
    "preview": "version: \"2\"\nservices:\n  web:\n    build: .\n    environment:\n      - NODE_ENV=production\n      - NODE_PORT=${NODE_PORT}\n "
  },
  {
    "path": "makefile",
    "chars": 693,
    "preview": "start:\n\tnpm install || true\n\tmake build\n\tNODE_PORT=8883 make docker-build\n\tNODE_PORT=8883 make docker-up\n\ndev:\n\tDEV=true"
  },
  {
    "path": "package.json",
    "chars": 1342,
    "preview": "{\n  \"name\": \"morphlingjs\",\n  \"version\": \"0.1.99\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"postins"
  },
  {
    "path": "src/api/apply.js",
    "chars": 949,
    "preview": "import koaRouter from 'koa-router'\nimport koaBody from 'koa-body'\nimport { writeFileStream, writeFile } from '../utils/u"
  },
  {
    "path": "src/api/describe.js",
    "chars": 485,
    "preview": "import koaRouter from 'koa-router'\nimport { readFile } from '../utils/utils'\n\nconst router = koaRouter();\n\nrouter.get('/"
  },
  {
    "path": "src/api/index.js",
    "chars": 780,
    "preview": "import koaRouter from 'koa-router'\n\nimport morphlingApply from './apply'\nimport morphlingList from './list'\nimport morph"
  },
  {
    "path": "src/api/list.js",
    "chars": 1222,
    "preview": "import _ from 'lodash';\n\nimport koaRouter from 'koa-router'\nimport { checkFileExistence, readDir } from '../utils/utils'"
  },
  {
    "path": "src/api/middleware/error.js",
    "chars": 241,
    "preview": "export default async (ctx, next) => {\n    try {\n        await next();\n    } catch (err) {\n        // will only respond w"
  },
  {
    "path": "src/api/middleware/loki.js",
    "chars": 202,
    "preview": "import { getDB } from '../../db/index'\n\nexport default async(ctx, next)=> {\n    try {\n        ctx.db = getDB()\n        a"
  },
  {
    "path": "src/api/remove-all.js",
    "chars": 162,
    "preview": "import koaRouter from 'koa-router'\n\nconst router = koaRouter();\n\nrouter.delete('/remove-all', async (ctx) => {\n    ctx.b"
  },
  {
    "path": "src/api/remove.js",
    "chars": 158,
    "preview": "import koaRouter from 'koa-router'\n\nconst router = koaRouter();\n\nrouter.delete('/remove', async (ctx) => {\n    ctx.body "
  },
  {
    "path": "src/api/toggle.js",
    "chars": 1526,
    "preview": "import _ from 'lodash';\nimport koaRouter from 'koa-router'\nimport { readDir, readFile, writeFile } from '../utils/utils'"
  },
  {
    "path": "src/data/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/db/collection.js",
    "chars": 881,
    "preview": "export const findOne = (collection, value, eq) => {\n    const query = {};\n    query[value] = { '$eq': eq }\n    return co"
  },
  {
    "path": "src/db/fixtures.js",
    "chars": 5928,
    "preview": "import _ from 'lodash'\nimport faker from 'faker'\n\nimport { getOneModel, createModel } from './index'\nimport { createColl"
  },
  {
    "path": "src/db/index.js",
    "chars": 2207,
    "preview": "import Loki from 'lokijs'\nimport _ from 'lodash'\n\nimport * as collectionUtils from './collection'\n\nexport const MODEL_TA"
  },
  {
    "path": "src/server.js",
    "chars": 1081,
    "preview": "import Koa from 'koa'\nimport _ from 'lodash';\n\nimport apiRouter from './api'\nimport errorMiddleware from './api/middlewa"
  },
  {
    "path": "src/utils/createRouterFromSwagger.js",
    "chars": 5710,
    "preview": "import koaRouter from 'koa-router'\nimport _ from 'lodash'\nimport { findCollection } from '../db/collection'\n\n/**\n * Put "
  },
  {
    "path": "src/utils/morphling.js",
    "chars": 2613,
    "preview": "import _ from 'lodash'\nimport swaggerParser from 'swagger-parser'\nimport YAML from 'yamljs'\n\nimport { getDB, validateMod"
  },
  {
    "path": "src/utils/override-router.js",
    "chars": 1620,
    "preview": "import koaRouter from 'koa-router'\nimport _ from 'lodash'\nimport { readDir, readFile } from './utils'\n\nconst SOURCE_FILE"
  },
  {
    "path": "src/utils/utils.js",
    "chars": 2461,
    "preview": "import fs from 'fs'\nimport _ from 'lodash';\n\n/**\n * Read file content in path\n * @param path\n * @returns {Promise}\n */\ne"
  },
  {
    "path": "swagger-examples/example.yaml",
    "chars": 16808,
    "preview": "---\nswagger: \"2.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Swagger Petstore\"\n  termsOfService: \"http://swagger.io/terms/\"\n  c"
  },
  {
    "path": "swagger-override-examples/demo-with-body.json",
    "chars": 193,
    "preview": "{\n  \"route\": \"/v2/user/lololo\",\n  \"body\": {\n    \"hello\": \"world\",\n    \"foo\": [12,123,1234],\n    \"top\":{\n      \"kek\":\"lol"
  },
  {
    "path": "swagger-override-examples/demo.json",
    "chars": 107,
    "preview": "{\n  \"route\": \"/v2/store/order/1\",\n  \"body\": {\n  },\n  \"empty\": true,\n  \"httpCode\": 200,\n  \"method\": \"GET\"\n}\n"
  }
]

About this extraction

This page contains the full source code of the leboncoin/morphlingjs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (76.7 KB), approximately 19.5k tokens, and a symbol index with 5 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!