Refactor CLI and add tests

This commit is contained in:
Typicode
2015-07-15 18:44:33 +02:00
parent e3a7db949b
commit ccf67724e3
22 changed files with 368 additions and 172 deletions

41
src/server/defaults.js Normal file
View 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
View 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
View 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
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

View 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>

View 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
View 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
View 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
View 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
}