├── .gitignore
├── yaml-server.png
├── .coveragebadgesrc
├── src
├── config.js
├── __tests__
│ ├── db.yml
│ ├── original-db.yml
│ ├── db.json
│ ├── original-db.json
│ └── server.js
├── fileHelper.js
├── index.js
└── server.js
├── CONTRIBUTING.md
├── db.yml
├── db.json
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── azure-pipelines.yml
├── .vscode
└── settings.json
├── .eslintrc.json
├── badges
└── coverage.svg
├── CHANGELOG.md
├── LICENSE
├── package.json
├── CODE_OF_CONDUCT.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
--------------------------------------------------------------------------------
/yaml-server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/softchris/yaml-server/HEAD/yaml-server.png
--------------------------------------------------------------------------------
/.coveragebadgesrc:
--------------------------------------------------------------------------------
1 | {
2 | "source": "./coverage/coverage-summary.json",
3 | "attribute": "total.statements.pct"
4 | }
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = (dbName) => ({ dbPath: path.join(process.cwd(), dbName) });
4 |
5 |
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | This project welcomes contributions and suggestions.
2 |
3 | Any feature suggestion, bugs, tests. Please raise an issue at `https://github.com/softchris/gatsby-cli/issues`.
--------------------------------------------------------------------------------
/db.yml:
--------------------------------------------------------------------------------
1 | products:
2 | - id: 1
3 | name: tomato
4 | - id: 2
5 | name: lettuce
6 | orders:
7 | - id: 1
8 | name: order1
9 | - id: 2
10 | name: order2
11 |
--------------------------------------------------------------------------------
/src/__tests__/db.yml:
--------------------------------------------------------------------------------
1 | products:
2 | - id: 1
3 | name: tomato
4 | - id: 2
5 | name: lettuce
6 | orders:
7 | - id: 1
8 | name: order1
9 | - id: 2
10 | name: order2
--------------------------------------------------------------------------------
/src/__tests__/original-db.yml:
--------------------------------------------------------------------------------
1 | products:
2 | - id: 1
3 | name: tomato
4 | - id: 2
5 | name: lettuce
6 | orders:
7 | - id: 1
8 | name: order1
9 | - id: 2
10 | name: order2
--------------------------------------------------------------------------------
/src/__tests__/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "products": [{
3 | "id": 1,
4 | "name": "tomato"
5 | }, {
6 | "id": 2,
7 | "name": "lettuce"
8 | }],
9 | "orders": [{
10 | "id": 1,
11 | "name": "order1"
12 | },
13 | {
14 | "id": 1,
15 | "name": "order1"
16 | }]
17 | }
--------------------------------------------------------------------------------
/src/__tests__/original-db.json:
--------------------------------------------------------------------------------
1 | {
2 | "products": [{
3 | "id": 1,
4 | "name": "tomato"
5 | }, {
6 | "id": 2,
7 | "name": "lettuce"
8 | }],
9 | "orders": [{
10 | "id": 1,
11 | "name": "order1"
12 | },
13 | {
14 | "id": 1,
15 | "name": "order1"
16 | }]
17 | }
--------------------------------------------------------------------------------
/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "products": [
3 | {
4 | "id": 1,
5 | "name": "tomato"
6 | },
7 | {
8 | "id": 2,
9 | "name": "lettuce"
10 | }
11 | ],
12 | "orders": [
13 | {
14 | "id": 1,
15 | "name": "order1"
16 | },
17 | {
18 | "id": 2,
19 | "name": "order2"
20 | },
21 | {
22 | "id": 3,
23 | "name": "order4"
24 | }
25 | ]
26 | }
--------------------------------------------------------------------------------
/src/fileHelper.js:
--------------------------------------------------------------------------------
1 | const YAML = require('yaml');
2 |
3 | module.exports = {
4 | toJson(content, dialect) {
5 | if (dialect === 'yaml') {
6 | const doc = YAML.parseDocument(content);
7 | json = doc.toJSON();
8 | return json;
9 | }
10 | return JSON.parse(content); // just return JSON
11 | },
12 | transform(content, dialect) {
13 | if (dialect === 'yaml') {
14 | return YAML.stringify(content);
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # Node.js
2 | # Build a general Node.js project with npm.
3 | # Add steps that analyze code, save build artifacts, deploy, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
5 |
6 | trigger:
7 | - master
8 |
9 | pool:
10 | vmImage: 'ubuntu-latest'
11 |
12 | steps:
13 | - task: NodeTool@0
14 | inputs:
15 | versionSpec: '10.x'
16 | displayName: 'Install Node.js'
17 |
18 | - script: |
19 | npm install
20 | displayName: 'npm install and build'
21 |
22 | - script: |
23 | npm run test
24 | displayName: 'npm test'
25 |
26 | - task: PublishCodeCoverageResults@1
27 | inputs:
28 | codeCoverageTool: Cobertura # or JaCoCo
29 | summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml'
30 | reportDirectory: '$(System.DefaultWorkingDirectory)/**/coverage'
31 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.colorCustomizations": {
3 | "activityBar.activeBackground": "#cc3636",
4 | "activityBar.activeBorder": "#134c13",
5 | "activityBar.background": "#cc3636",
6 | "activityBar.foreground": "#e7e7e7",
7 | "activityBar.inactiveForeground": "#e7e7e799",
8 | "activityBarBadge.background": "#134c13",
9 | "activityBarBadge.foreground": "#e7e7e7",
10 | "statusBar.background": "#a52a2a",
11 | "statusBar.border": "#a52a2a",
12 | "statusBar.foreground": "#e7e7e7",
13 | "statusBarItem.hoverBackground": "#cc3636",
14 | "titleBar.activeBackground": "#a52a2a",
15 | "titleBar.activeForeground": "#e7e7e7",
16 | "titleBar.border": "#a52a2a",
17 | "titleBar.inactiveBackground": "#a52a2a99",
18 | "titleBar.inactiveForeground": "#e7e7e799"
19 | },
20 | "peacock.color": "brown"
21 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true,
6 | "jest/globals": true
7 | },
8 | "extends": [ "eslint:recommended", "plugin:node/recommended" ],
9 | "globals": {
10 | "Atomics": "readonly",
11 | "SharedArrayBuffer": "readonly"
12 | },
13 | "parserOptions": {
14 | "ecmaVersion": 11
15 | },
16 | "plugins": ["jest"],
17 | "rules": {
18 | "semi": [
19 | "error",
20 | "always"
21 | ],
22 | "quotes": [
23 | "error",
24 | "double"
25 | ],
26 | "jest/no-disabled-tests": "warn",
27 | "jest/no-focused-tests": "error",
28 | "jest/no-identical-title": "error",
29 | "jest/prefer-to-have-length": "warn",
30 | "jest/valid-expect": "error",
31 | "node/no-unpublished-require": "off",
32 | "node/no-extraneous-require": "off"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/badges/coverage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.2.0 - 2020-05-20
4 |
5 | - Adding support for query parameters `page` and `pageSize`
6 |
7 | ## 1.2.1 - 2020-05-20
8 |
9 | - Made run instructions clearer
10 |
11 | ## 1.3.1 - 2020-05-20
12 |
13 | - Adding support for new resource creation `//new`
14 |
15 | ## 1.3.2 - 2020-05-20
16 |
17 | - Test added to ensure no new resource is created with `//new` if resource already exist
18 |
19 | ## 1.3.3 - 2020-05-21
20 |
21 | - Adding CORS, all requests are CORS enabled
22 |
23 | ## 1.4.4 - 2020-05-21
24 |
25 | - Adding ESLint
26 |
27 | ## 1.5.0 - 2020-05-21
28 |
29 | - Adding CI with Azure Devops
30 |
31 | ## 1.6.0 - 2020-05-21
32 |
33 | - Added ability to sort.
34 |
35 | ## 1.7.0 - 2020-05-21
36 |
37 | - Static file hosting, use `--static` to specify what directory, otherwise root is used.
38 |
39 | ## 1.8.0 - 2020-05-21
40 |
41 | - Auto start browser at `http://localhost:/info`.
42 |
43 | ## 1.9.0 - 2020-05-29
44 |
45 | - Hot reload, you can now edit the db file and the server will restart by itself.
46 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const argv = require("yargs").argv;
3 | const fs = require('fs');
4 |
5 | function getDefaultByDialect(dialect) {
6 | switch (dialect) {
7 | case 'yaml':
8 | return 'db.yml'
9 | case 'json':
10 | return 'db.json'
11 | default:
12 | return '';
13 | }
14 | }
15 |
16 | const { createServer, getHttpServer } = require("./server");
17 | const dialect = argv.dialect || 'yaml';
18 | const { dbPath } = require("./config")(getDefaultByDialect(dialect));
19 |
20 | const port = argv.port || 3000;
21 | const database = argv.database || dbPath;
22 | const autoStart = argv.autoStart === 'off' ? false : true;
23 | const hotReload = argv.hotReload === "off" ? false : true;
24 |
25 | createServer(port, database, argv.static, autoStart, dialect);
26 |
27 | if (hotReload) {
28 | fs.watchFile(database, (curr, prev) => {
29 | console.log("Database changed");
30 | getHttpServer().close(() => {
31 | console.log("Restarting the server");
32 | createServer(port, database, argv.static, autoStart);
33 | });
34 | });
35 | }
36 |
37 |
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Chris Noring
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yaml-server",
3 | "version": "1.10.0",
4 | "engines": {
5 | "node": ">=8.3.0"
6 | },
7 | "bin": {
8 | "yaml-server": "./src/index.js"
9 | },
10 | "description": "A command line tool that creates a REST server from a YAML file that you specify",
11 | "main": "index.js",
12 | "scripts": {
13 | "start": "node ./src/index.js",
14 | "test": "jest --coverage",
15 | "test:watch": "jest --watchAll",
16 | "premake-badge": "$(npm bin)/jest --coverage",
17 | "make-badge": "$(npm bin)/coverage-badges",
18 | "toc": "npx markdown-toc README.md",
19 | "lint": "npx eslint ./src/**"
20 | },
21 | "jest": {
22 | "coverageReporters": [
23 | "text",
24 | "lcov",
25 | "json-summary",
26 | "cobertura"
27 | ]
28 | },
29 | "keywords": [
30 | "YAML",
31 | "YML",
32 | "server",
33 | "fake",
34 | "REST",
35 | "API",
36 | "prototyping",
37 | "mock",
38 | "mocking",
39 | "test",
40 | "testing",
41 | "rest",
42 | "data"
43 | ],
44 | "repository": {
45 | "type": "git",
46 | "url": "https://github.com/softchris/yaml-server.git"
47 | },
48 | "homepage": "https://softchris.github.io/yaml-server.html",
49 | "bugs": {
50 | "url": "https://github.com/softchris/yaml-server/issues"
51 | },
52 | "author": "Chris Noring (https://softchris.github.io)",
53 | "contributors": [
54 | {
55 | "name": "chris noring",
56 | "url": "https://softchris.github.io"
57 | }
58 | ],
59 | "license": "MIT",
60 | "dependencies": {
61 | "chalk": "^4.0.0",
62 | "cors": "^2.8.5",
63 | "express": "^4.17.1",
64 | "opn": "^6.0.0",
65 | "yaml": "^1.9.2",
66 | "yargs": "^15.3.1"
67 | },
68 | "devDependencies": {
69 | "coverage-badges": "^1.0.4",
70 | "eslint": "^7.0.0",
71 | "eslint-plugin-jest": "^23.13.1",
72 | "eslint-plugin-node": "^11.1.0",
73 | "jest": "^26.0.1",
74 | "markdown-toc": "^1.2.0",
75 | "supertest": "^4.0.2"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at christoffer.noring@microsoft.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://dev.azure.com/devrel/chris-testing/_build/latest?definitionId=23&branchName=master)
2 | 
3 | [](https://www.npmjs.com/package/yaml-server)
4 | [](https://www.npmjs.com/package/yaml-server)
5 | [](http://opensource.org/licenses/MIT)
6 |
7 | ## Table of Contents
8 |
9 | - [About](#about)
10 | - [Features](#features)
11 | - [Install](#install)
12 | - [Run](#run)
13 | - [Routes](#routes)
14 |
15 | ## About
16 |
17 | Recognition, this project wouldn't be here with out the great `json-server`. I thought to myself that JSON was a little verbose. So I created `yaml-server` so you can have a Mock REST API based on a YAML file instead.
18 |
19 | `yaml-server` is a command line tool that create a REST server based on a YAML file.
20 |
21 | 
22 |
23 | ## Features
24 |
25 | - **RESTful API** Do HTTP requests towards a Mock API using GET, PUT, POST and DELETE created off of a `db.yml` file.
26 | - **Filter** your GET calls with query parameters `page` and `pageSize`, example:
27 |
28 | ```bash
29 | /products?page=1&pageSize=10
30 | ```
31 |
32 | - **JSON support**, yes you can have your database in JSON as well. All you need is to specify the `--dialect` argument like so:
33 |
34 | ```bash
35 | npx yaml-server --dialect=json
36 | ```
37 |
38 | The above will look after a `db.json` file at the root. You override where it looks for this if you specify `--database` like for example:
39 |
40 | ```bash
41 | npx yaml-server --dialect=json --database ./db/db.json
42 | ```
43 |
44 | Above you need to ensure the `db.json` is located in sub directory `db` as seen from the root.
45 |
46 | - **Create new resource**, make a POST call with the following format `//new`, example:
47 |
48 | ```bash
49 | /kittens/new
50 | ```
51 |
52 | Ensure you have a payload as well, looking like for example `{ title: 'paw paw' }`
53 |
54 | - **Sorting, by order and key**, you can sort a resource response using query parameters `sortOrder` and `sortKey`. Assume we have the resource `/kittens` where one kitten object looks like so `{ id: 1, name: 'paws' }` and the entire resource looks like so:
55 |
56 | ```javascript
57 | [{
58 | id: 1,
59 | name: 'paws'
60 | }, {
61 | id: 2,
62 | name: 'alpha paw'
63 | }]
64 | ```
65 |
66 | Use sorting by appending `sortOrder` and `sortKey` like below:
67 |
68 | ```bash
69 | /kittens?sortOrder=ASC&sortKey=name
70 | ```
71 |
72 | This would give the response:
73 |
74 | ```javascript
75 | [{
76 | id: 2,
77 | name: 'alpha paw'
78 | }, {
79 | id: 1,
80 | name: 'paws'
81 | }]
82 | ```
83 |
84 | - **browser autostart**, the Browser auto starts at `http://locallhost:/info`. Should you not wish to have that behavior, you can shut it off like so:
85 |
86 | ```bash
87 | npx yaml-server --autoStart=off
88 | ```
89 |
90 | - **Static file server**
91 |
92 | By default a static file server is starting up to host your files at root directory. You can change that by specifying `--static`. Here's how you would do that:
93 |
94 | ```bash
95 | npx yaml-server --static=public
96 | ```
97 |
98 | The above would start a static file server from the sub folder `public`.
99 |
100 | - **Hot reload**
101 |
102 | The server will restart if you make changes to your database file. No need for closing and starting the server after a database change. Should you not wish that behavior, you can shut it off with:
103 |
104 | ```bash
105 | npx yaml-server --hotReload=off
106 | ```
107 |
108 | ## Install
109 |
110 | Either install it globally with:
111 |
112 | ```bash
113 | npm install -g yaml-server
114 | ```
115 |
116 | OR use `NPX`
117 |
118 | ```bash
119 | npx yaml-server --port 3000 --database ./db.yml
120 | ```
121 |
122 | ## Run
123 |
124 | 1. Create a `db.yml`.
125 | 1. Give `db.yml` an example content, for example:
126 |
127 | ```yaml
128 | products:
129 | - id: 1
130 | name: tomato
131 | - id: 2
132 | name: lettuce
133 | orders:
134 | - id: 1
135 | name: order1
136 | - id: 2
137 | name: order2
138 | ```
139 |
140 | 1. There are two ways to start:
141 | 1. **Quick start**, run `npx yaml-server`, this will start a server on `http://localhost:3000` and base it off a `db.yml` at the project root that you created.
142 | 1. **With parameters**, You can also configure like so `npx yaml-server --port 8000 --database ./db/mydb.yml` (If you place db file under `./db/mydb.yml`)
143 |
144 | ### See your routes
145 |
146 | Open up a browser and navigate to `http://localhost:/info`. Default port is `3000`, if you specified port use that as port instead.
147 |
148 | The page at route `http://localhost:/info` will tell you what routes and operations are available. Here's a typical response for the default page:
149 |
150 | ```output
151 | Welcome to YAML Server
152 |
153 | Routes available are:
154 |
155 | GET /products
156 | GET /products/:id
157 | PUT /products
158 | DELETE /products/:id
159 |
160 | GET /orders
161 | GET /orders/:id
162 | PUT /orders
163 | DELETE /orders/:id
164 | ```
165 |
166 | ## Routes
167 |
168 | Routes are created from a YAML file. The default value is `db.yml`. You can name it whatever you want though.
169 |
170 | Routes are first level elements. Consider the following example file:
171 |
172 | ```yml
173 | # db.yml
174 | products:
175 | - id: 1
176 | name: tomato
177 | - id: 2
178 | name: lettuce
179 | orders:
180 | - id: 1
181 | name: order1
182 | - id: 2
183 | name: order2
184 | ```
185 |
186 | This will produce routes `/products`, `/orders`. Below is a table of supported operations with `products` as example resource. The same operations are also supports for `orders/`.
187 |
188 | | VERB |Route | Input | Output |
189 | |----------|---------------|------------|--------------------|
190 | | GET | /products | *None* | **Array** |
191 | | GET | /products/:id | **e.g 3** | **Object** |
192 | | POST | /products | **object** | **Created object** |
193 | | PUT | /products | **object** | **Updated object** |
194 | | DELETE | /products/:id | **e.g 3** | **Deleted object** |
195 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | const YAML = require("yaml");
2 | const fs = require("fs");
3 | var bodyParser = require("body-parser");
4 | const cors = require("cors");
5 | const chalk = require("chalk");
6 | const express = require("express");
7 | const { toJson, transform } = require('./fileHelper');
8 | const opn = require('opn');
9 |
10 | let httpServer;
11 |
12 | function createServer(portNumber, dbPath, staticDirectory, autoStart = true, dialect = 'yaml') {
13 | let app = express();
14 | app.use(cors());
15 |
16 | const port = portNumber || 3000;
17 |
18 | app.use(bodyParser.json());
19 |
20 | let routes = [];
21 | let json;
22 | if (!fs.existsSync(dbPath)) {
23 | console.log(chalk.redBright(`DB does not exist at path "${dbPath}"`));
24 | } else {
25 | const content = fs.readFileSync(dbPath, { encoding: "utf8" });
26 | json = toJson(content, dialect)
27 | routes = Object.keys(json);
28 | }
29 |
30 | let staticPath = staticDirectory || ".";
31 | app.use(express.static(staticPath));
32 | console.log(chalk.blueBright(`Static files served from ${staticPath}/`));
33 |
34 | const routesString = routes.reduce((acc, curr) => {
35 | return `${acc} GET /${curr} \n GET /${curr}/:id \n PUT /${curr} \n DELETE /${curr}/:id \n\n`;
36 | }, "");
37 |
38 | const routeDefault = (req, res) => res.send(`Welcome to YAML Server \n
39 | Routes available are: \n
40 | ${routesString}
41 | `);
42 |
43 | function setupRoutesForResource(route) {
44 | app.get(`/${route}`, (req, res) => routeGet(req, res, route));
45 | app.get(`/${route}/:id`, (req, res) => routeGetWithParam(req, res, route));
46 | app.post("/:newRoute/new", (req, res) => routeNewResource(req, res, route));
47 | app.post(`/${route}`, (req, res) => routePost(req, res, route));
48 | app.put(`/${route}`, (req, res) => routePut(req, res, route));
49 | app.delete(`/${route}/:id`, (req, res) => routeDelete(req, res, route));
50 | }
51 |
52 | function routeGet(req, res, route) {
53 | const page = req.query.page;
54 | const pageSize = req.query.pageSize;
55 | const sortOrder = req.query.sortOrder;
56 | const sortKey = req.query.sortKey;
57 | const sortOrders = [ 'ASC', 'DESC' ];
58 |
59 | const sortAscending = (a, b) => {
60 | console.log('sort ascending')
61 | if (a[sortKey] > b[sortKey]) {
62 | return 1;
63 | } else if (a[sortKey] < b[sortKey]) {
64 | return -1;
65 | }
66 | return 0;
67 | }
68 |
69 | const sortDescending = (a, b) => {
70 | if (a[sortKey] > b[sortKey]) {
71 | return -1;
72 | } else if (a[sortKey] < b[sortKey]) {
73 | return 1;
74 | }
75 | return 0;
76 | }
77 |
78 | if (sortKey && json[route][0][sortKey] === undefined) {
79 | res.statusCode = 400;
80 | res.send(`${sortKey} is not a valid sort key`);
81 | }
82 |
83 | if (sortOrder && sortOrders.includes(sortOrder)) {
84 | const sortMethod = sortOrder === 'ASC'? sortAscending : sortDescending;
85 | const copyArr = [ ...json[route]];
86 | copyArr.sort(sortMethod)
87 | res.json(copyArr)
88 | } else if (!sortOrder && sortKey) {
89 | const copyArr = [...json[route]];
90 | copyArr.sort(sortAscending)
91 | res.json(copyArr)
92 | }
93 |
94 | if (/\d+/.test(page) && /\d+/.test(pageSize)) {
95 | const pageNo = +page;
96 | const pageSizeNo = +pageSize;
97 | const start = pageSizeNo * (+pageNo - 1);
98 | const end = start + pageSizeNo;
99 | res.json(json[route].slice(start, end));
100 | }
101 | res.json(json[route]);
102 | }
103 |
104 | function routeGetWithParam(req, res, route) {
105 | const foundItem = json[route].find((item) => item.id == req.params.id);
106 | if (!foundItem) {
107 | res.statusCode = 404;
108 | res.json({});
109 | } else {
110 | res.json(foundItem);
111 | }
112 | }
113 |
114 | function routeNewResource(req, res) {
115 | const { newRoute } = req.params;
116 |
117 | if (!json[newRoute]) {
118 | json[newRoute] = { ...req.body, id: 1 };
119 | setupRoutesForResource(newRoute);
120 | fs.writeFileSync(dbPath, transform(json, dialect));
121 | res.statusCode = 201;
122 | res.json({ ...req.body, id: 1 });
123 | } else {
124 | res.statusCode = 400;
125 | res.send(`/${newRoute} already exist`);
126 | }
127 | }
128 |
129 | function routePost(req, res, route) {
130 | const posted = { ...req.body, id: 0 };
131 |
132 | if (json[route].length > 0) {
133 | const [firstItem] = json[route];
134 | const props = [...Object.keys(firstItem)].sort();
135 | const postedProps = Object.keys(posted).sort();
136 |
137 | if (JSON.stringify(props) !== JSON.stringify(postedProps)) {
138 | res.statusCode = 400;
139 | res.send("");
140 | }
141 | }
142 |
143 | const insertObject = { ...posted, id: json[route].length + 1 };
144 |
145 | json[route].push(insertObject);
146 |
147 | fs.writeFileSync(dbPath, transform(json, dialect));
148 | res.statusCode = 201;
149 | res.json(insertObject);
150 | }
151 |
152 | function routePut(req, res, route) {
153 | const posted = req.body;
154 | let foundItem;
155 | json[route] = json[route].map((item) => {
156 | if (item.id === +posted.id) {
157 | foundItem = item;
158 | return { ...item, ...posted };
159 | }
160 | return item;
161 | });
162 | if (foundItem) {
163 | fs.writeFileSync(dbPath, transform(json, dialect));
164 | res.json({ ...foundItem, ...posted });
165 | } else {
166 | res.statusCode = 404;
167 | res.send("Item not found with ID" + posted.id);
168 | }
169 | }
170 |
171 | function routeDelete(req, res, route) {
172 | let deletedItem;
173 | json[route] = json[route].filter((item) => {
174 | if (item.id === +req.params.id) {
175 | deletedItem = item;
176 | }
177 | return item.id !== +req.params.id;
178 | });
179 | fs.writeFileSync(dbPath, transform(json, dialect));
180 | return res.json(deletedItem);
181 | }
182 |
183 | app.get("/info", routeDefault);
184 |
185 |
186 | routes.forEach(setupRoutesForResource);
187 |
188 | httpServer = app.listen(port, async() => {
189 | console.log(`Example app listening on port ${port}!`);
190 | console.log(chalk.greenBright(routesString));
191 | if (autoStart) {
192 | const ref = await opn(`http://localhost:${port}/info`);
193 | }
194 | });
195 | return app;
196 | }
197 |
198 | function getHttpServer() {
199 | return httpServer;
200 | }
201 |
202 | module.exports = {
203 | createServer,
204 | getHttpServer
205 | };
206 |
--------------------------------------------------------------------------------
/src/__tests__/server.js:
--------------------------------------------------------------------------------
1 | const { createServer, getHttpServer } = require("../server");
2 | const path = require("path");
3 | const fs = require("fs");
4 | const supertest = require("supertest");
5 | const jsonOption = {
6 | dialect: 'json',
7 | dbFilename: 'db.json'
8 | };
9 |
10 | const ymlOption = {
11 | dialect: 'yaml',
12 | dbFilename: 'db.yml'
13 | };
14 |
15 | function createOption(option) {
16 | if (option === 'yaml') {
17 | return ymlOption
18 | } else if (option === 'json') {
19 | return jsonOption;
20 | } else {
21 | throw new Error(`Unknown option ${option}`)
22 | }
23 | }
24 |
25 | const selectedOption = createOption('json');
26 |
27 | const server = createServer(
28 | 3000,
29 | path.join(__dirname, selectedOption.dbFilename),
30 | ".",
31 | false,
32 | selectedOption.dialect
33 | );
34 |
35 | const request = supertest(server);
36 |
37 | function restore(dialect) {
38 | if (dialect === 'yaml') {
39 | const original = fs.readFileSync(path.join(__dirname, "original-db.yml"));
40 | fs.writeFileSync(path.join(__dirname, "db.yml"), original);
41 | } else if (dialect === 'json') {
42 | const original = fs.readFileSync(path.join(__dirname, "original-db.json"));
43 | fs.writeFileSync(path.join(__dirname, "db.json"), original);
44 | }
45 | }
46 |
47 | describe("server", () => {
48 | afterAll(async(done) => {
49 | // restore yml
50 | // TODO, ensure we have an original based on dialect
51 | // const original = fs.readFileSync(path.join(__dirname, "original-db.yml"));
52 | // fs.writeFileSync(path.join(__dirname, "db.yml"), original);
53 | restore(selectedOption.dialect);
54 | getHttpServer().close(() => {
55 | console.log("server closed!");
56 | done();
57 | });
58 | });
59 |
60 | test("should return products", async(done) => {
61 | const products = [{
62 | id: 1,
63 | name: "tomato"
64 | }, {
65 | id: 2,
66 | name: "lettuce"
67 | }];
68 |
69 | const res = await request.get("/products");
70 | expect(res.status).toBe(200);
71 | expect(res.body).toEqual(products);
72 | done();
73 | });
74 |
75 | test("should sort the data based on sortOrder and sortKey", async () => {
76 | const expected = [{ id: 2, name: 'lettuce' }, { id: 1, name: 'tomato' }];
77 |
78 | const res = await request.get('/products?sortOrder=ASC&sortKey=name')
79 | expect(expected).toEqual(res.body);
80 | })
81 |
82 | test("should sort the data based on sortOrder and sortKey - DESCENDING", async () => {
83 | const expected = [{ id: 1, name: 'tomato' }, { id: 2, name: 'lettuce' }];
84 |
85 | const res = await request.get('/products?sortOrder=DESC&sortKey=name')
86 | expect(expected).toEqual(res.body);
87 | })
88 |
89 | test("should sort with ascending when sortOrder is missing but sortKey is present", async () => {
90 | const expected = [{ id: 2, name: 'lettuce' }, { id: 1, name: 'tomato' }];
91 |
92 | const res = await request.get('/products?sortKey=name')
93 | expect(expected).toEqual(res.body);
94 | })
95 |
96 | test("should sort not respect sortOrder when sortOrder value is NOT ASC or DESC", async () => {
97 | const expected = [{ id: 1, name: 'tomato' }, { id: 2, name: 'lettuce' }];
98 |
99 | const res = await request.get('/products?sortOrder=abc&sortKey=name')
100 | expect(expected).toEqual(res.body);
101 | })
102 |
103 | test("should respond with 400 when sortKey is not a valid column", async () => {
104 | const expected = [{ id: 1, name: 'tomato' }, { id: 2, name: 'lettuce' }];
105 |
106 | const res = await request.get('/products?sortOrder=abc&sortKey=notValidKey')
107 |
108 | expect(res.status).toBe(400)
109 | expect(res.text).toBe('notValidKey is not a valid sort key')
110 | })
111 |
112 | test("should filter by query parameters", async() => {
113 | const firstItem = { id: 1, name: "tomato" };
114 | const secondItem = { id: 2, name: "lettuce" };
115 |
116 | let res = await request.get("/products?page=1&pageSize=1");
117 | let [ item ] = res.body;
118 | expect(res.body).toHaveLength(1);
119 | expect(item).toEqual(firstItem);
120 |
121 | res = await request.get("/products/?page=2&pageSize=1");
122 | item = res.body[0];
123 | expect(res.body).toHaveLength(1);
124 | expect(item).toEqual(secondItem);
125 | });
126 |
127 | test("should NOT respect filter when query param missing", async() => {
128 | const res = await request.get("/products?page=1");
129 | expect(res.body).toHaveLength(2);
130 | });
131 |
132 | test("should NOT respect filter when query param has wrong value type", async () => {
133 | const res = await request.get("/products?page=1&pageSize=abc");
134 | expect(res.body).toHaveLength(2);
135 | });
136 |
137 | test("should return intro text on default route", async() => {
138 | const res = await request.get("/info");
139 | expect(res.text).toMatch(/Welcome to YAML Server/);
140 | });
141 |
142 | test("should return a product", async(done) => {
143 | const product = { id: 1, name: "tomato" };
144 | const res = await request.get("/products/1");
145 | expect(res.status).toBe(200);
146 | expect(res.body).toEqual(product);
147 | done();
148 | });
149 |
150 | test("should return 404 resource not found", async (done) => {
151 | const res = await request.get("/products/3");
152 | expect(res.status).toBe(404);
153 | done();
154 | });
155 |
156 | test("should add product to /products", async(done) => {
157 | const createdRecord = { id: 3, name: "cucumber" };
158 |
159 | const res = await request
160 | .post("/products")
161 | .send({ name : "cucumber" });
162 | expect(res.status).toBe(201);
163 | expect(res.body).toEqual(createdRecord);
164 | done();
165 | });
166 |
167 | test("should update product", async(done) => {
168 | const changeTo = { id : 3, name: "gurkin" };
169 | let res = await request
170 | .put("/products")
171 | .send({ id: 3, name: "gurkin" });
172 | expect(res.status).toBe(200);
173 | expect(res.body).toEqual(changeTo);
174 |
175 | res = await request.get("/products/3");
176 | expect(changeTo).toEqual(res.body);
177 | done();
178 | });
179 |
180 | test("should return 404 and error message when trying to update non existing item", async() => {
181 | const nonExistingItem = { id: 99, name: "unknown" };
182 | let res = await request
183 | .put("/products")
184 | .send(nonExistingItem);
185 | expect(res.status).toBe(404);
186 | expect(res.text).toBe("Item not found with ID" + nonExistingItem.id);
187 | });
188 |
189 | test("should delete product", async(done) => {
190 | const deletedItem = { id: 3, name: "gurkin" };
191 | let res = await request.delete("/products/3");
192 | expect(res.status).toBe(200);
193 | expect(res.body).toEqual(deletedItem);
194 |
195 | res = await request.get("/products/3");
196 | expect(res.status).toBe(404);
197 | done();
198 | });
199 |
200 | test("should respect existing schema on POST", async() => {
201 | let res = await request
202 | .post("/products")
203 | .send({ title: "should not work" });
204 |
205 | expect(res.status).toBe(400);
206 | });
207 |
208 | test("should create a new resource based on a //new/ call", async() => {
209 | const kitten = { id: 1, title: "paw paw" };
210 | let res = await request
211 | .post("/kittens/new")
212 | .send({ title: "paw paw" });
213 |
214 | expect(res.status).toBe(201);
215 | expect(res.body).toEqual(kitten);
216 |
217 | res = await request.get("/kittens");
218 | expect(res.status).toBe(200);
219 | expect(res.body).toEqual(kitten);
220 | });
221 |
222 | test("should NOT create a new resource //new/ call when resource already exist", async() => {
223 | let res = await request
224 | .post("/kittens/new")
225 | .send({ title: "paw paw" });
226 |
227 | expect(res.status).toBe(400);
228 | expect(res.text).toBe("/kittens already exist");
229 | });
230 | });
--------------------------------------------------------------------------------