mirror of
https://github.com/typicode/json-server.git
synced 2025-07-29 21:23:41 +08:00
Refactor CLI and add tests
This commit is contained in:
41
src/server/defaults.js
Normal file
41
src/server/defaults.js
Normal file
@ -0,0 +1,41 @@
|
||||
var fs = require('fs')
|
||||
var express = require('express')
|
||||
var logger = require('morgan')
|
||||
var cors = require('cors')
|
||||
var errorhandler = require('errorhandler')
|
||||
|
||||
var arr = []
|
||||
|
||||
// Logger
|
||||
arr.push(logger('dev', {
|
||||
skip: function (req, res) {
|
||||
return process.env.NODE_ENV === 'test' ||
|
||||
req.path === '/favicon.ico'
|
||||
}
|
||||
}))
|
||||
|
||||
// Enable CORS for all the requests, including static files
|
||||
arr.push(cors({ origin: true, credentials: true }))
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// only use in development
|
||||
arr.push(errorhandler())
|
||||
}
|
||||
|
||||
// Serve static files
|
||||
if (fs.existsSync(process.cwd() + '/public')) {
|
||||
arr.push(express.static(process.cwd() + '/public'))
|
||||
} else {
|
||||
arr.push(express.static(__dirname + '/public'))
|
||||
}
|
||||
|
||||
// No cache for IE
|
||||
// https://support.microsoft.com/en-us/kb/234067
|
||||
arr.push(function (req, res, next) {
|
||||
res.header('Cache-Control', 'no-cache')
|
||||
res.header('Pragma', 'no-cache')
|
||||
res.header('Expires', '-1')
|
||||
next()
|
||||
})
|
||||
|
||||
module.exports = arr
|
12
src/server/index.js
Normal file
12
src/server/index.js
Normal file
@ -0,0 +1,12 @@
|
||||
var express = require('express')
|
||||
|
||||
module.exports = {
|
||||
create: function () {
|
||||
var server = express()
|
||||
server.set('json spaces', 2)
|
||||
return server
|
||||
},
|
||||
defaults: require('./defaults'),
|
||||
router: require('./router'),
|
||||
rewriter: require('./rewriter')
|
||||
}
|
77
src/server/mixins.js
Normal file
77
src/server/mixins.js
Normal file
@ -0,0 +1,77 @@
|
||||
var uuid = require('node-uuid')
|
||||
var pluralize = require('pluralize')
|
||||
|
||||
module.exports = {
|
||||
getRemovable: getRemovable,
|
||||
createId: createId,
|
||||
deepQuery: deepQuery
|
||||
}
|
||||
|
||||
// Returns document ids that have unsatisfied relations
|
||||
// Example: a comment that references a post that doesn't exist
|
||||
function getRemovable (db) {
|
||||
var _ = this
|
||||
var removable = []
|
||||
_.each(db, function (coll, collName) {
|
||||
_.each(coll, function (doc) {
|
||||
_.each(doc, function (value, key) {
|
||||
if (/Id$/.test(key)) {
|
||||
var refName = pluralize.plural(key.slice(0, -2))
|
||||
// Test if table exists
|
||||
if (db[refName]) {
|
||||
// Test if references is defined in table
|
||||
var ref = _.getById(db[refName], value)
|
||||
if (_.isUndefined(ref)) {
|
||||
removable.push({name: collName, id: doc.id})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return removable
|
||||
}
|
||||
|
||||
// Return incremented id or uuid
|
||||
// Used to override underscore-db's createId with utils.createId
|
||||
function createId (coll) {
|
||||
var _ = this
|
||||
var idProperty = _.__id()
|
||||
if (_.isEmpty(coll)) {
|
||||
return 1
|
||||
} else {
|
||||
var id = _.max(coll, function (doc) {
|
||||
return doc[idProperty]
|
||||
})[idProperty]
|
||||
|
||||
if (_.isFinite(id)) {
|
||||
// Increment integer id
|
||||
return ++id
|
||||
} else {
|
||||
// Generate string id
|
||||
return uuid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deepQuery (value, q) {
|
||||
var _ = this
|
||||
if (value && q) {
|
||||
if (_.isArray(value)) {
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
if (_.deepQuery(value[i], q)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if (_.isObject(value) && !_.isArray(value)) {
|
||||
for (var k in value) {
|
||||
if (_.deepQuery(value[k], q)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if (value.toString().toLowerCase().indexOf(q) !== -1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/server/public/favicon.ico
Normal file
BIN
src/server/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 B |
BIN
src/server/public/images/json.png
Normal file
BIN
src/server/public/images/json.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 278 B |
81
src/server/public/index.html
Normal file
81
src/server/public/index.html
Normal file
@ -0,0 +1,81 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>JSON Server</title>
|
||||
<link href="//netdna.bootstrapcdn.com/bootswatch/3.1.1/flatly/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="stylesheets/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<p>
|
||||
<a href="https://github.com/typicode/json-server" class="logo">
|
||||
<img src="images/json.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
<em>
|
||||
Congrats! You're successfully running JSON Server.
|
||||
</em>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Routes</h4>
|
||||
<p>
|
||||
Here are the resources that JSON Server has loaded:
|
||||
</p>
|
||||
<p>
|
||||
<ul id="resources">loading, please wait...</ul>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can view database current state at any time:
|
||||
<ul>
|
||||
<li>
|
||||
<a href="db">db</a>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can use any HTTP verbs (GET, POST, PUT, PATCH and DELETE) and access your resources from anywhere
|
||||
using CORS and JSONP.
|
||||
</p>
|
||||
|
||||
<h4>Documentation</h4>
|
||||
<p>
|
||||
View
|
||||
<a href="https://github.com/typicode/json-server">README</a>
|
||||
on GitHub.
|
||||
</p>
|
||||
|
||||
<h4>Issues</h4>
|
||||
<p>Please go
|
||||
<a href="https://github.com/typicode/json-server/issues">here</a>.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<p>
|
||||
<i>To replace this page, create an index.html file in ./public, JSON Server will load it.</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>
|
16
src/server/public/stylesheets/style.css
Normal file
16
src/server/public/stylesheets/style.css
Normal file
@ -0,0 +1,16 @@
|
||||
a {
|
||||
color: #1882BC !important;
|
||||
}
|
||||
|
||||
img {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
h4 {
|
||||
padding-top: 20px;
|
||||
}
|
29
src/server/rewriter.js
Normal file
29
src/server/rewriter.js
Normal file
@ -0,0 +1,29 @@
|
||||
var express = require('express')
|
||||
|
||||
module.exports = function (routes) {
|
||||
var router = express.Router()
|
||||
|
||||
Object.keys(routes).forEach(function (route) {
|
||||
|
||||
if (route.indexOf(':') !== -1) {
|
||||
router.all(route, function (req, res, next) {
|
||||
// Rewrite target url using params
|
||||
var target = routes[route]
|
||||
for (var param in req.params) {
|
||||
target = target.replace(':' + param, req.params[param])
|
||||
}
|
||||
req.url = target
|
||||
next()
|
||||
})
|
||||
} else {
|
||||
router.all(route + '*', function (req, res, next) {
|
||||
// Rewrite url by replacing prefix
|
||||
req.url = req.url.replace(route, routes[route])
|
||||
next()
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
263
src/server/router.js
Normal file
263
src/server/router.js
Normal file
@ -0,0 +1,263 @@
|
||||
var express = require('express')
|
||||
var methodOverride = require('method-override')
|
||||
var bodyParser = require('body-parser')
|
||||
var _ = require('lodash')
|
||||
var _db = require('underscore-db')
|
||||
var low = require('lowdb')
|
||||
var pluralize = require('pluralize')
|
||||
var utils = require('./utils')
|
||||
var mixins = require('./mixins')
|
||||
|
||||
module.exports = function (source) {
|
||||
// Create router
|
||||
var router = express.Router()
|
||||
|
||||
// Add middlewares
|
||||
router.use(bodyParser.json({limit: '10mb'}))
|
||||
router.use(bodyParser.urlencoded({extended: false}))
|
||||
router.use(methodOverride())
|
||||
|
||||
// Create database
|
||||
var db
|
||||
if (_.isObject(source)) {
|
||||
db = low()
|
||||
db.object = source
|
||||
} else {
|
||||
db = low(source)
|
||||
}
|
||||
|
||||
// Add underscore-db methods to db
|
||||
db._.mixin(_db)
|
||||
|
||||
// Add specific mixins
|
||||
db._.mixin(mixins)
|
||||
|
||||
// Expose database
|
||||
router.db = db
|
||||
|
||||
// Expose render
|
||||
router.render = function (req, res) {
|
||||
res.jsonp(res.locals.data)
|
||||
}
|
||||
|
||||
// GET /db
|
||||
function showDatabase (req, res, next) {
|
||||
res.locals.data = db.object
|
||||
next()
|
||||
}
|
||||
|
||||
// GET /:resource
|
||||
// GET /:resource?q=
|
||||
// GET /:resource?attr=&attr=
|
||||
// GET /:parent/:parentId/:resource?attr=&attr=
|
||||
// GET /*?*&_end=
|
||||
// GET /*?*&_start=&_end=
|
||||
function list (req, res, next) {
|
||||
// Test if resource exists
|
||||
if (!db.object.hasOwnProperty(req.params.resource)) {
|
||||
res.status(404)
|
||||
return next()
|
||||
}
|
||||
|
||||
// Filters list
|
||||
var filters = {}
|
||||
|
||||
// Result array
|
||||
var array
|
||||
|
||||
// Remove _start, _end and _limit from req.query to avoid filtering using those
|
||||
// parameters
|
||||
var _start = req.query._start
|
||||
var _end = req.query._end
|
||||
var _sort = req.query._sort
|
||||
var _order = req.query._order
|
||||
var _limit = req.query._limit
|
||||
delete req.query._start
|
||||
delete req.query._end
|
||||
delete req.query._sort
|
||||
delete req.query._order
|
||||
delete req.query._limit
|
||||
|
||||
if (req.query.q) {
|
||||
|
||||
// Full-text search
|
||||
var q = req.query.q.toLowerCase()
|
||||
|
||||
array = db(req.params.resource).filter(function (obj) {
|
||||
for (var key in obj) {
|
||||
var value = obj[key]
|
||||
if (db._.deepQuery(value, q)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
|
||||
// Add :parentId filter in case URL is like /:parent/:parentId/:resource
|
||||
if (req.params.parent) {
|
||||
var parent = pluralize.singular(req.params.parent)
|
||||
filters[parent + 'Id'] = +req.params.parentId
|
||||
}
|
||||
|
||||
// Add query parameters filters
|
||||
// Convert query parameters to their native counterparts
|
||||
for (var key in req.query) {
|
||||
// don't take into account JSONP query parameters
|
||||
// jQuery adds a '_' query parameter too
|
||||
if (key !== 'callback' && key !== '_') {
|
||||
filters[key] = utils.toNative(req.query[key])
|
||||
}
|
||||
}
|
||||
|
||||
// Filter
|
||||
if (_(filters).isEmpty()) {
|
||||
array = db(req.params.resource).value()
|
||||
} else {
|
||||
var chain = db(req.params.resource).chain()
|
||||
for (var f in filters) {
|
||||
// This syntax allow for deep filtering using lodash (i.e. a.b.c[0])
|
||||
chain = chain.filter(f, filters[f])
|
||||
}
|
||||
array = chain.value()
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
if (_sort) {
|
||||
_order = _order || 'ASC'
|
||||
|
||||
array = _.sortBy(array, function (element) {
|
||||
return element[_sort]
|
||||
})
|
||||
|
||||
if (_order === 'DESC') {
|
||||
array.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
// Slice result
|
||||
if (_end || _limit) {
|
||||
res.setHeader('X-Total-Count', array.length)
|
||||
res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count')
|
||||
}
|
||||
|
||||
_start = parseInt(_start, 10) || 0
|
||||
|
||||
if (_end) {
|
||||
_end = parseInt(_end, 10)
|
||||
array = array.slice(_start, _end)
|
||||
} else if (_limit) {
|
||||
_limit = parseInt(_limit, 10)
|
||||
array = array.slice(_start, _start + _limit)
|
||||
}
|
||||
|
||||
res.locals.data = array
|
||||
next()
|
||||
}
|
||||
|
||||
// GET /:resource/:id
|
||||
function show (req, res, next) {
|
||||
var _embed = req.query._embed
|
||||
var id = utils.toNative(req.params.id)
|
||||
var resource = db(req.params.resource)
|
||||
.getById(id)
|
||||
|
||||
if (resource) {
|
||||
// Clone resource to avoid making changes to the underlying object
|
||||
resource = _.cloneDeep(resource)
|
||||
// Always use an array
|
||||
_embed = _.isArray(_embed) ? _embed : [_embed]
|
||||
|
||||
// Embed other resources based on resource id
|
||||
_embed.forEach(function (otherResource) {
|
||||
|
||||
if (otherResource
|
||||
&& otherResource.trim().length > 0
|
||||
&& db.object[otherResource]) {
|
||||
var query = {}
|
||||
var prop = pluralize.singular(req.params.resource) + 'Id'
|
||||
query[prop] = id
|
||||
resource[otherResource] = db(otherResource).where(query)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
res.locals.data = resource
|
||||
} else {
|
||||
res.status(404)
|
||||
res.locals.data = {}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// POST /:resource
|
||||
function create (req, res, next) {
|
||||
for (var key in req.body) {
|
||||
req.body[key] = utils.toNative(req.body[key])
|
||||
}
|
||||
|
||||
var resource = db(req.params.resource)
|
||||
.insert(req.body)
|
||||
|
||||
res.status(201)
|
||||
res.locals.data = resource
|
||||
next()
|
||||
}
|
||||
|
||||
// PUT /:resource/:id
|
||||
// PATCH /:resource/:id
|
||||
function update (req, res, next) {
|
||||
for (var key in req.body) {
|
||||
req.body[key] = utils.toNative(req.body[key])
|
||||
}
|
||||
|
||||
var resource = db(req.params.resource)
|
||||
.updateById(utils.toNative(req.params.id), req.body)
|
||||
|
||||
if (resource) {
|
||||
res.locals.data = resource
|
||||
} else {
|
||||
res.status(404)
|
||||
res.locals.data = {}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// DELETE /:resource/:id
|
||||
function destroy (req, res, next) {
|
||||
db(req.params.resource).removeById(utils.toNative(req.params.id))
|
||||
|
||||
// Remove dependents documents
|
||||
var removable = db._.getRemovable(db.object)
|
||||
|
||||
_.each(removable, function (item) {
|
||||
db(item.name).removeById(item.id)
|
||||
})
|
||||
|
||||
res.locals.data = {}
|
||||
next()
|
||||
}
|
||||
|
||||
router.get('/db', showDatabase, router.render)
|
||||
|
||||
router.route('/:resource')
|
||||
.get(list)
|
||||
.post(create)
|
||||
|
||||
router.route('/:resource/:id')
|
||||
.get(show)
|
||||
.put(update)
|
||||
.patch(update)
|
||||
.delete(destroy)
|
||||
|
||||
router.get('/:parent/:parentId/:resource', list)
|
||||
|
||||
router.all('*', function (req, res) {
|
||||
router.render(req, res)
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
22
src/server/utils.js
Normal file
22
src/server/utils.js
Normal file
@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
toNative: toNative
|
||||
}
|
||||
|
||||
// Turns string to native.
|
||||
// Example:
|
||||
// 'true' -> true
|
||||
// '1' -> 1
|
||||
function toNative (value) {
|
||||
if (typeof value === 'string') {
|
||||
if (value === ''
|
||||
|| value.trim() !== value
|
||||
|| (value.length > 1 && value[0] === '0')) {
|
||||
return value
|
||||
} else if (value === 'true' || value === 'false') {
|
||||
return value === 'true'
|
||||
} else if (!isNaN(+value)) {
|
||||
return +value
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
Reference in New Issue
Block a user