mirror of
https://github.com/typicode/json-server.git
synced 2025-07-29 05:03:04 +08:00
First commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
20
LICENSE
Normal file
20
LICENSE
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 typicode
|
||||||
|
|
||||||
|
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.
|
179
README.md
Normal file
179
README.md
Normal file
@ -0,0 +1,179 @@
|
|||||||
|

|
||||||
|
|
||||||
|
__JSON Server__
|
||||||
|
|
||||||
|
> Give it a JSON or JS seed file and it will serve it through REST routes.
|
||||||
|
|
||||||
|
_Created with :heart: for front-end developers who need a flexible back-end for quick prototyping and mocking._
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Command line interface
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cat db.json
|
||||||
|
{
|
||||||
|
posts: [
|
||||||
|
{ id: 1, body: 'foo' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
$ json-server --file db.json
|
||||||
|
$ curl -i http://localhost:3000/posts/1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node module
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var server = require('json-server');
|
||||||
|
|
||||||
|
var db = {
|
||||||
|
posts: [
|
||||||
|
{id: 1, body: 'foo'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
server.run(db);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
* Supports __GET__ but also __POST__, __PUT__, __DELETE__ and even __PATCH__ requests
|
||||||
|
* Can be used from anywhere through __cross domain__ requests (JSONP or CORS)
|
||||||
|
* Can load remote JSON files
|
||||||
|
* Can be deployed on Nodejitsu, Heroku, ...
|
||||||
|
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install -g json-server
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Command line interface
|
||||||
|
|
||||||
|
```bash
|
||||||
|
json-server --help
|
||||||
|
|
||||||
|
Usage: json-server [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-h, --help output usage information
|
||||||
|
-V, --version output the version number
|
||||||
|
-f --file <file> load db from a js or json file
|
||||||
|
-u --url <url> load db from a URL
|
||||||
|
-p --port [port] server port
|
||||||
|
--read-only read only mode
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON Server can load JSON from multiple sources:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ json-server --file db.json
|
||||||
|
$ json-server --file seed.js
|
||||||
|
$ json-server --url http://example.com/db.json
|
||||||
|
```
|
||||||
|
|
||||||
|
And be run in read-only mode (useful if deployed on a public server):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ json-server --file db.json --read-only
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
Here's 2 examples showing how to format JSON or JS seed file:
|
||||||
|
|
||||||
|
* __db.json__
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
posts: [
|
||||||
|
{ id: 1, body: 'foo'},
|
||||||
|
{ id: 2, body: 'bar'}
|
||||||
|
],
|
||||||
|
comments: [
|
||||||
|
{ id: 1, body: 'baz', postId: 1}
|
||||||
|
{ id: 2, body: 'qux', postId: 2}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* __seed.js__
|
||||||
|
```javascript
|
||||||
|
exports.run = function() {
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
data.posts = [];
|
||||||
|
data.posts.push({id: 1, body: 'foo'});
|
||||||
|
//...
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON Server expects JS files to export a ```run``` method that returns an object.
|
||||||
|
|
||||||
|
Seed files are useful if you need to programmaticaly create a lot of data.
|
||||||
|
|
||||||
|
|
||||||
|
### Node module
|
||||||
|
|
||||||
|
#### run(db, [options])
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var server = require('json-server'),
|
||||||
|
db = require('./seed').run();
|
||||||
|
|
||||||
|
var options = { port: 4000, readOnly: true };
|
||||||
|
|
||||||
|
server.run(db, options);
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, ```port``` is set to 3000 and ```readOnly``` to false.
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /:resource
|
||||||
|
GET /:resource?attr=&attr=&
|
||||||
|
GET /:parent/:parentId/:resource
|
||||||
|
GET /:resource/:id
|
||||||
|
POST /:resource
|
||||||
|
PUT /:resource/:id
|
||||||
|
PATCH /:resource/:id
|
||||||
|
DEL /:resource/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
For routes usage information, have a look at [JSONPlaceholder](https://github.com/typicode/jsonplaceholder) code examples.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /db
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns database state.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns default index file or content of ./public/index.html (useful if you need to set a custom home page).
|
||||||
|
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you like the project, please tell your friends about it, star it or give feedback :) It's very much appreciated!
|
||||||
|
|
||||||
|
For project updates or to get in touch, [@typicode](http://twitter.com/typicde). You can also send me a mail.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
$ npm test
|
||||||
|
```
|
48
bin/cli.js
Executable file
48
bin/cli.js
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
var program = require('commander'),
|
||||||
|
request = require('superagent'),
|
||||||
|
url = require('url'),
|
||||||
|
fs = require('fs'),
|
||||||
|
server = require('../server'),
|
||||||
|
logger = require('../utils/logger'),
|
||||||
|
options = {};
|
||||||
|
|
||||||
|
function loadFile(file, cb) {
|
||||||
|
var path = process.cwd() + '/' + file,
|
||||||
|
db;
|
||||||
|
|
||||||
|
if (/\.json$/.test(file)) db = require(path);
|
||||||
|
if (/\.js$/.test(file)) db = require(path).run();
|
||||||
|
|
||||||
|
cb(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadURL(url, cb) {
|
||||||
|
logger.info('Fetching ' + url + '...')
|
||||||
|
request
|
||||||
|
.get(url)
|
||||||
|
.end(function(error, res) {
|
||||||
|
if (error) {
|
||||||
|
logger.error(error);
|
||||||
|
} else {
|
||||||
|
cb(JSON.parse(res));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDatabaseLoaded(db) {
|
||||||
|
server.run(db, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.option('-f --file <file>', 'load db from a js or json file')
|
||||||
|
.option('-u --url <url>', 'load db from a URL')
|
||||||
|
.option('-p --port [port]', 'server port')
|
||||||
|
.option('--read-only', 'read only mode')
|
||||||
|
.parse(process.argv);
|
||||||
|
|
||||||
|
if (program.port) options.port = program.port;
|
||||||
|
if (program.readOnly) options.readOnly = true;
|
||||||
|
if (program.file) loadFile(program.file, onDatabaseLoaded);
|
||||||
|
if (program.url) loadURL(program.url, onDatabaseLoaded);
|
52
package.json
Normal file
52
package.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "json-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Serves JSON file through REST routes",
|
||||||
|
"main": "server.js",
|
||||||
|
"bin": "./bin/cli.js",
|
||||||
|
"directories": {
|
||||||
|
"test": "test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "~2.0.0",
|
||||||
|
"cors": "~2.1.0",
|
||||||
|
"express": "~3.4.4",
|
||||||
|
"logan": "~0.0.2",
|
||||||
|
"request": "~2.27.0",
|
||||||
|
"underscore": "~1.5.2",
|
||||||
|
"underscore.inflections": "~0.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"superagent": "~0.15.7",
|
||||||
|
"supertest": "~0.8.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "mocha -R spec test",
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/typicode/json-server.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"JSON",
|
||||||
|
"server",
|
||||||
|
"fake",
|
||||||
|
"REST",
|
||||||
|
"API",
|
||||||
|
"prototyping",
|
||||||
|
"mock",
|
||||||
|
"mocking",
|
||||||
|
"test",
|
||||||
|
"testing",
|
||||||
|
"rest",
|
||||||
|
"data",
|
||||||
|
"dummy"
|
||||||
|
],
|
||||||
|
"author": "Typicode <typicode@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/typicode/json-server/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/typicode/json-server"
|
||||||
|
}
|
BIN
public/images/logo.png
Normal file
BIN
public/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
79
public/index.html
Normal file
79
public/index.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>JSON Server</title>
|
||||||
|
<link href="//netdna.bootstrapcdn.com/bootswatch/3.0.2/journal/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="stylesheets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="https://github.com/typicode/json-server" class="logo">
|
||||||
|
<img src="images/logo.png">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="alert alert-info">
|
||||||
|
Congrats! You're successfully running JSON Server.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Resources</h2>
|
||||||
|
<p>
|
||||||
|
<ul id="resources">loading, please wait...</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To view database current state:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="db">db</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Requests</h2>
|
||||||
|
<p>
|
||||||
|
Resources can be accessed in various ways.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
JSON Server supports:
|
||||||
|
<ul>
|
||||||
|
<li>GET, POST, PUT, PATCH, DESTROY and OPTIONS verbs.</li>
|
||||||
|
<li>JSONP or CORS cross domain requests.</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Documentation</h2>
|
||||||
|
<p>
|
||||||
|
View
|
||||||
|
<a href="http://github.com/typicode/jsonserver">README</a>
|
||||||
|
on GitHub.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Issues</h2>
|
||||||
|
<p>Please go
|
||||||
|
<a href="https://github.com/typicode/jsonplaceholder/issues">here</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<i>To customize this page, just create a ./public/index.html file.</i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
|
||||||
|
<script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
$.get('db').then(function(data) {
|
||||||
|
$('#resources').empty();
|
||||||
|
$.each(data, function(key, value) {
|
||||||
|
$('#resources')
|
||||||
|
.append('<li><a href="/'+ key + '">' + key + '</a></li>');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
43
public/stylesheets/style.css
Normal file
43
public/stylesheets/style.css
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
body {
|
||||||
|
padding: 50px;
|
||||||
|
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
/*color: #00B7FF;*/
|
||||||
|
color: #3D72D1;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 1px #EEE solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logo {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.logo {
|
||||||
|
background: #00B7FF;
|
||||||
|
color: white;
|
||||||
|
font-size: 48px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 90px;
|
||||||
|
border-radius: 100px;
|
||||||
|
transform:rotate(90deg);
|
||||||
|
-ms-transform:rotate(90deg);
|
||||||
|
-webkit-transform:rotate(90deg);
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}*/
|
71
routes/read-only.js
Normal file
71
routes/read-only.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
var _ = require('underscore'),
|
||||||
|
utils = require('../utils/utils'),
|
||||||
|
db = {};
|
||||||
|
|
||||||
|
_.mixin(require('../utils/db-mixins'));
|
||||||
|
|
||||||
|
exports.setDatabase = function(object) {
|
||||||
|
db = object;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.database = function(req, res) {
|
||||||
|
res.jsonp(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /:resource?attr=&attr=
|
||||||
|
exports.list = function(req, res) {
|
||||||
|
var collection = db[req.params.resource],
|
||||||
|
properties = {},
|
||||||
|
result;
|
||||||
|
|
||||||
|
Object.keys(req.query).forEach(function (key) {
|
||||||
|
var value = req.query[key];
|
||||||
|
properties[key] = utils.toNative(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_(properties).isEmpty()) {
|
||||||
|
result = collection;
|
||||||
|
} else {
|
||||||
|
result = _(collection).where(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.jsonp(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /:parent/:parentId/:resource
|
||||||
|
exports.nestedList = function(req, res) {
|
||||||
|
var properties = {},
|
||||||
|
resource;
|
||||||
|
|
||||||
|
// Set parentID
|
||||||
|
properties[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId;
|
||||||
|
|
||||||
|
// Filter using parentID
|
||||||
|
resource = _.where(db[req.params.resource], properties);
|
||||||
|
|
||||||
|
res.jsonp(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /:resource/:id
|
||||||
|
exports.show = function(req, res) {
|
||||||
|
var resource = _.get(db, req.params.resource, +req.params.id);
|
||||||
|
|
||||||
|
res.jsonp(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.create = function(req, res) {
|
||||||
|
req.body.id = Math.round(new Date().getTime() / 1000);
|
||||||
|
res.jsonp(req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.update = function(req, res) {
|
||||||
|
var resource = _.get(db, req.params.resource, +req.params.id),
|
||||||
|
clonedResource = _.clone(resource),
|
||||||
|
result = _.extend(clonedResource, req.body);
|
||||||
|
|
||||||
|
res.jsonp(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.destroy = function(req, res) {
|
||||||
|
res.send(204)
|
||||||
|
}
|
79
routes/read-write.js
Normal file
79
routes/read-write.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
var _ = require('underscore'),
|
||||||
|
utils = require('../utils/utils'),
|
||||||
|
db = {};
|
||||||
|
|
||||||
|
_.mixin(require('../utils/db-mixins'));
|
||||||
|
|
||||||
|
exports.setDatabase = function(object) {
|
||||||
|
db = object;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.database = function(req, res) {
|
||||||
|
res.jsonp(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /:resource?attr=&attr=
|
||||||
|
exports.list = function(req, res) {
|
||||||
|
var collection = db[req.params.resource],
|
||||||
|
properties = {},
|
||||||
|
result;
|
||||||
|
|
||||||
|
Object.keys(req.query).forEach(function (key) {
|
||||||
|
var value = req.query[key];
|
||||||
|
properties[key] = utils.toNative(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_(properties).isEmpty()) {
|
||||||
|
result = collection;
|
||||||
|
} else {
|
||||||
|
result = _(collection).where(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.jsonp(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /:parent/:parentId/:resource
|
||||||
|
exports.nestedList = function(req, res) {
|
||||||
|
var properties = {},
|
||||||
|
resource;
|
||||||
|
|
||||||
|
// Set parentID
|
||||||
|
properties[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId;
|
||||||
|
|
||||||
|
// Filter using parentID
|
||||||
|
resource = _.where(db[req.params.resource], properties);
|
||||||
|
|
||||||
|
res.jsonp(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /:resource/:id
|
||||||
|
exports.show = function(req, res) {
|
||||||
|
var resource = _.get(db, req.params.resource, +req.params.id);
|
||||||
|
|
||||||
|
res.jsonp(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /:resource
|
||||||
|
exports.create = function(req, res) {
|
||||||
|
var resource = _.create(db, req.params.resource, req.body);
|
||||||
|
|
||||||
|
res.jsonp(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /:resource/:id
|
||||||
|
// PATCH /:resource/:id
|
||||||
|
exports.update = function(req, res) {
|
||||||
|
_.update(db, req.params.resource, +req.params.id, req.body);
|
||||||
|
var resource = _.get(db, req.params.resource, +req.params.id);
|
||||||
|
|
||||||
|
res.jsonp(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /:resource/:id
|
||||||
|
exports.destroy = function(req, res) {
|
||||||
|
_.remove(db, req.params.resource, +req.params.id);
|
||||||
|
_.clean(db);
|
||||||
|
|
||||||
|
res.send(204);
|
||||||
|
}
|
||||||
|
|
95
server.js
Normal file
95
server.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
var express = require('express'),
|
||||||
|
cors = require('cors'),
|
||||||
|
http = require('http'),
|
||||||
|
path = require('path'),
|
||||||
|
fs = require('fs'),
|
||||||
|
_ = require('underscore'),
|
||||||
|
logger = require('./utils/logger');
|
||||||
|
|
||||||
|
|
||||||
|
var defaultOptions = {
|
||||||
|
port: process.env.PORT || 3000,
|
||||||
|
readOnly: false
|
||||||
|
}
|
||||||
|
|
||||||
|
function createApp(db, options) {
|
||||||
|
// Create app
|
||||||
|
var app = express(),
|
||||||
|
options = options || {},
|
||||||
|
routes;
|
||||||
|
|
||||||
|
// Configure all environments
|
||||||
|
app.use(express.favicon());
|
||||||
|
app.use(express.logger('dev'));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded());
|
||||||
|
app.use(express.methodOverride());
|
||||||
|
|
||||||
|
|
||||||
|
// Configure development
|
||||||
|
if ('development' == app.get('env')) {
|
||||||
|
app.use(express.errorHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure using options provided
|
||||||
|
app.set('port', options.port);
|
||||||
|
routes = options.readOnly ? './routes/read-only' : './routes/read-write';
|
||||||
|
routes = require(routes);
|
||||||
|
|
||||||
|
// Use default or user public directory
|
||||||
|
// Note: should be done before CORS and app.router setting
|
||||||
|
console.log(fs.existsSync(process.cwd() + '/public'), process.cwd() + '/public')
|
||||||
|
if (fs.existsSync(process.cwd() + '/public')) {
|
||||||
|
app.use(express.static(process.cwd() + '/public'));
|
||||||
|
} else {
|
||||||
|
app.use(express.static(path.join(__dirname, './public')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable CORS for everything
|
||||||
|
app.use(cors());
|
||||||
|
app.options('*', cors());
|
||||||
|
|
||||||
|
// Set app.router
|
||||||
|
app.use(app.router);
|
||||||
|
|
||||||
|
// Set API entry points
|
||||||
|
app.get('/db', routes.database)
|
||||||
|
app.get('/:resource', routes.list);
|
||||||
|
app.get('/:parent/:parentId/:resource', routes.nestedList);
|
||||||
|
app.get('/:resource/:id', routes.show);
|
||||||
|
app.post('/:resource', routes.create);
|
||||||
|
app.put('/:resource/:id', routes.update);
|
||||||
|
app.patch('/:resource/:id', routes.update);
|
||||||
|
app.del('/:resource/:id', routes.destroy);
|
||||||
|
|
||||||
|
// Set database
|
||||||
|
routes.setDatabase(db);
|
||||||
|
|
||||||
|
// And done! Ready to serve JSON!
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(db, options) {
|
||||||
|
options = _.defaults(options, defaultOptions);
|
||||||
|
|
||||||
|
var app = createApp(db, options);
|
||||||
|
|
||||||
|
if (_.isEmpty(db)) {
|
||||||
|
logger.error('No resources found!');
|
||||||
|
} else {
|
||||||
|
logger.success('Available resources');
|
||||||
|
for (var prop in db) {
|
||||||
|
logger.url(options.port, prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http
|
||||||
|
.createServer(app)
|
||||||
|
.listen((options.port), function(){
|
||||||
|
logger.success('Express server listening on port ' + options.port);
|
||||||
|
logger.success('Congrats! Open http://localhost:' + options.port);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createApp = createApp;
|
||||||
|
exports.run = run;
|
18
test/fixture.js
Normal file
18
test/fixture.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Small database to be used during tests
|
||||||
|
module.exports = function() {
|
||||||
|
var db = {};
|
||||||
|
|
||||||
|
db.posts = [
|
||||||
|
{id: 1, body: 'foo'},
|
||||||
|
{id: 2, body: 'bar'}
|
||||||
|
]
|
||||||
|
|
||||||
|
db.comments = [
|
||||||
|
{id: 1, published: true, postId: 1},
|
||||||
|
{id: 2, published: false, postId: 1},
|
||||||
|
{id: 3, published: false, postId: 2},
|
||||||
|
{id: 4, published: false, postId: 2},
|
||||||
|
]
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
138
test/read-only.js
Normal file
138
test/read-only.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
var request = require('supertest'),
|
||||||
|
assert = require('assert'),
|
||||||
|
server = require('../server'),
|
||||||
|
fixture = require('./fixture'),
|
||||||
|
db,
|
||||||
|
app;
|
||||||
|
|
||||||
|
describe('Read only routes', function() {
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
db = fixture();
|
||||||
|
app = server.createApp(db, { readOnly: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /:resource', function() {
|
||||||
|
it('should respond with json and resources and corresponding resources', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/posts')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(db.posts)
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /:resource?attr=&attr=', function() {
|
||||||
|
it('should respond with json and filter resources', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/comments?postId=1&published=true')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect([db.comments[0]])
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /:parent/:parentId/:resource', function() {
|
||||||
|
it('should respond with json and corresponding nested resources', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/posts/1/comments')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect([db.comments[0], db.comments[1]])
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('GET /:resource/:id', function() {
|
||||||
|
it('should respond with json and corresponding resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/posts/1')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(db.posts[0])
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /db', function() {
|
||||||
|
it('should respond with json and full database', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/db')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(db)
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /:resource', function() {
|
||||||
|
it('should respond with fake json and not create a resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.post('/posts')
|
||||||
|
.send({body: '...'})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res){
|
||||||
|
if (err) return done(err);
|
||||||
|
assert(res.body.hasOwnProperty('id'));
|
||||||
|
assert.equal(res.body.body, '...');
|
||||||
|
assert.equal(db.posts.length, 2);
|
||||||
|
done()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /:resource/:id', function() {
|
||||||
|
it('should respond with fake json and not update resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.put('/posts/1')
|
||||||
|
.send({id: 999, body: '...'})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect({id: 999, body: '...'})
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res){
|
||||||
|
if (err) return done(err);
|
||||||
|
// Checking that first post wasn't updated
|
||||||
|
assert.deepEqual(db.posts[0], {id: 1, body: 'foo'});
|
||||||
|
done()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PATCH /:resource/:id', function() {
|
||||||
|
it('should respond with fake json and not update resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.patch('/posts/1')
|
||||||
|
.send({body: '...'})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect({id: 1, body: '...'})
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res){
|
||||||
|
if (err) return done(err);
|
||||||
|
// Checking that first post wasn't updated
|
||||||
|
assert.deepEqual(db.posts[0], {id: 1, body: 'foo'});
|
||||||
|
done()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /:resource/:id', function() {
|
||||||
|
it('should respond with empty data and not destroy resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.del('/posts/1')
|
||||||
|
.expect(204)
|
||||||
|
.end(function(err, res){
|
||||||
|
if (err) return done(err);
|
||||||
|
assert.equal(db.posts.length, 2);
|
||||||
|
assert.equal(db.comments.length, 4);
|
||||||
|
done()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OPTIONS /:resource/:id', function() {
|
||||||
|
it('should respond with empty data and not destroy resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.options('/posts/1')
|
||||||
|
.expect(204, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
130
test/read-write.js
Normal file
130
test/read-write.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
var request = require('supertest'),
|
||||||
|
assert = require('assert'),
|
||||||
|
server = require('../server'),
|
||||||
|
routes = require('../routes/read-write'),
|
||||||
|
fixture = require('./fixture'),
|
||||||
|
db,
|
||||||
|
app;
|
||||||
|
|
||||||
|
describe('Read write routes', function() {
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
db = fixture();
|
||||||
|
app = server.createApp(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /:resource', function() {
|
||||||
|
it('should respond with json and corresponding resources', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/posts')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(db.posts)
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /:resource?attr=&attr=', function() {
|
||||||
|
it('should respond with json and filter resources', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/comments?postId=1&published=true')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect([db.comments[0]])
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /:parent/:parentId/:resource', function() {
|
||||||
|
it('should respond with json and corresponding nested resources', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/posts/1/comments')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect([
|
||||||
|
db.comments[0],
|
||||||
|
db.comments[1]
|
||||||
|
])
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /:resource/:id', function() {
|
||||||
|
it('should respond with json and corresponding resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/posts/1')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(db.posts[0])
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /db', function() {
|
||||||
|
it('should respond with json and full database', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/db')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(db)
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /:resource', function() {
|
||||||
|
it('should respond with json and create a resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.post('/posts')
|
||||||
|
.send({body: 'foo'})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect({id: 3, body: 'foo'})
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res){
|
||||||
|
if (err) return done(err);
|
||||||
|
assert.equal(db.posts.length, 3);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /:resource/:id', function() {
|
||||||
|
it('should respond with json and update resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.put('/posts/1')
|
||||||
|
.send({id: 1, body: 'foo'})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect({id: 1, body: 'foo'})
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res){
|
||||||
|
if (err) return done(err);
|
||||||
|
assert.deepEqual(db.posts[0], {id: 1, body: 'foo'});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PATCH /:resource/:id', function() {
|
||||||
|
it('should respond with json and update resource', function(done) {
|
||||||
|
request(app)
|
||||||
|
.patch('/posts/1')
|
||||||
|
.send({body: 'bar'})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect({id: 1, body: 'bar'})
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res){
|
||||||
|
if (err) return done(err);
|
||||||
|
assert.deepEqual(db.posts[0], {id: 1, body: 'bar'});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /:resource/:id', function() {
|
||||||
|
it('should respond with empty data, destroy resource and dependent resources', function(done) {
|
||||||
|
request(app)
|
||||||
|
.del('/posts/1')
|
||||||
|
.expect(204)
|
||||||
|
.end(function(err, res){
|
||||||
|
if (err) return done(err);
|
||||||
|
assert.equal(db.posts.length, 1);
|
||||||
|
assert.equal(db.comments.length, 2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
31
test/static.js
Normal file
31
test/static.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
var request = require('supertest'),
|
||||||
|
assert = require('assert'),
|
||||||
|
server = require('../server'),
|
||||||
|
routes = require('../routes/read-write'),
|
||||||
|
app;
|
||||||
|
|
||||||
|
describe('Static routes', function() {
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
app = server.createApp({}, routes);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /', function() {
|
||||||
|
it('should respond with html', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/')
|
||||||
|
.expect('Content-Type', /html/)
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /stylesheets/style.css', function() {
|
||||||
|
it('should respond with css', function(done) {
|
||||||
|
request(app)
|
||||||
|
.get('/stylesheets/style.css')
|
||||||
|
.expect('Content-Type', /css/)
|
||||||
|
.expect(200, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
86
utils/db-mixins.js
Normal file
86
utils/db-mixins.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
(function(root) {
|
||||||
|
|
||||||
|
var _ = root._ || require('underscore');
|
||||||
|
|
||||||
|
if (!root._) {
|
||||||
|
_.mixin(require('underscore.inflections'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(db, table, id) {
|
||||||
|
return _.find(db[table], function (row) {
|
||||||
|
return row.id === id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exist(db, table, id) {
|
||||||
|
return !_.isUndefined(_.get(db, table, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createId(db, table) {
|
||||||
|
if (_.isEmpty(db[table])) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return _.max(db[table], function(row) {
|
||||||
|
return row.id;
|
||||||
|
}).id + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(db, table, obj) {
|
||||||
|
var clone = _.clone(obj);
|
||||||
|
|
||||||
|
if (_.isUndefined(clone.id)) clone.id = _.createId(db, table);
|
||||||
|
|
||||||
|
db[table].push(clone);
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(db, table, id, attrs) {
|
||||||
|
var row = get(db, table, id),
|
||||||
|
updatedRow = _.extend(row, attrs),
|
||||||
|
index = _.indexOf(db[table], row);
|
||||||
|
|
||||||
|
db[table][index] = updatedRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean(db) {
|
||||||
|
var toBeRemoved = [];
|
||||||
|
|
||||||
|
_(db).each(function(table, tableName) {
|
||||||
|
_(table).each(function(row) {
|
||||||
|
_(row).each(function(value, key) {
|
||||||
|
if (/Id$/.test(key)) {
|
||||||
|
var reference = _.pluralize(key.slice(0, - 2));
|
||||||
|
if (!_.exist(db, reference, row[key])) {
|
||||||
|
toBeRemoved.push({
|
||||||
|
tableName: tableName,
|
||||||
|
id: row.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
_(toBeRemoved).each(function(row) {
|
||||||
|
_.remove(db, row.tableName, row.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(db, table, id) {
|
||||||
|
var newTable = _.reject(db[table], function(row) {
|
||||||
|
return row.id === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
db[table] = newTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
_.get = get;
|
||||||
|
_.exist = exist;
|
||||||
|
_.createId = createId;
|
||||||
|
_.create = create;
|
||||||
|
_.update = update;
|
||||||
|
_.clean = clean;
|
||||||
|
_.remove = remove;
|
||||||
|
})(this);
|
10
utils/logger.js
Normal file
10
utils/logger.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
var logan = require('logan');
|
||||||
|
|
||||||
|
logan.set({
|
||||||
|
error: ['%', 'red'],
|
||||||
|
success: ['%', 'green'],
|
||||||
|
info: ['%', 'grey'],
|
||||||
|
url: [' http://localhost:%/'.grey + '%'.cyan, '.']
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = logan
|
11
utils/utils.js
Normal file
11
utils/utils.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
function toNative(value) {
|
||||||
|
if (value === 'true' || value === 'false') {
|
||||||
|
return value === 'true';
|
||||||
|
} else if (!isNaN(+value)) {
|
||||||
|
return +value;
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.toNative = toNative;
|
Reference in New Issue
Block a user