* Remove automatic type conversion

* Remove body-parser from default middlewares

* Ignore lib

* ES2015

* Use shortid

* Add babel-register

* Update paths to ./lib

* Add .npmignore

* Update bin

* temp fix

* Fix bin

* Add message when creating default db

* Use fs.watch

* Fix operator existence test

* Fix 0.12 tests

* Update dependencies

* Update

* Increase timeout

* Fix 0.12 support

* 0.9.0-beta.1

* Fix missing example.json issue

* 0.9.0-beta.2

* Update message

* Update CHANGELOG.md

* Update lowdb dependency

* Add error message

* Update README.md

* Add database validation

* Update

* Update

* Fix tests

* Update
This commit is contained in:
typicode
2016-11-12 01:59:43 +01:00
committed by GitHub
parent 44bdfb490a
commit 2b26630ac6
32 changed files with 822 additions and 683 deletions

View File

@ -1,5 +1,5 @@
{
"presets": [
"es2015"
["es2015", { "loose": true }]
]
}

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
**/*.log
node_modules
tmp
lib
.DS_Store
.idea

1
.npmignore Normal file
View File

@ -0,0 +1 @@
src

View File

@ -1,5 +1,17 @@
# Change Log
## [0.9.0][2016-11-11]
* Shorter `uuid`
* No automatic conversion of strings to boolean or integer
* Create a default `db.json` file if it doesn't exist
* Fix
* [#361](https://github.com/typicode/json-server/issues/361)
* [#363](https://github.com/typicode/json-server/issues/363) [#365](https://github.com/typicode/json-server/issues/365)
* [#374](https://github.com/typicode/json-server/issues/374)
* [#383](https://github.com/typicode/json-server/issues/383)
* Updated dependencies and codebase to ES6
## [0.8.23][2016-11-03]
* Fix `Links` header

View File

@ -97,10 +97,14 @@ GET /comments?author.name=typicode
### Paginate
Add `_page` and in the `Link` header you'll get `first`, `prev`, `next` and `last` links
Use `_page` and optionally `_limit` to paginate returned data.
In the `Link` header you'll get `first`, `prev`, `next` and `last` links.
```
GET /posts?_page=7
GET /posts?_page=7&_limit=20
```
_10 items are returned by default_
@ -247,16 +251,21 @@ module.exports = function() {
$ json-server index.js
```
__Tip__ use modules like [faker](https://github.com/Marak/faker.js), [casual](https://github.com/boo1ean/casual) or [chance](https://github.com/victorquinn/chancejs).
__Tip__ use modules like [Faker](https://github.com/Marak/faker.js), [Casual](https://github.com/boo1ean/casual), [Chance](https://github.com/victorquinn/chancejs) or [JSON Schema Faker](https://github.com/json-schema-faker/json-schema-faker).
### Add routes
### HTTPS
Create a `routes.json` file. Pay attention to start every route with /.
There's many way to set up SSL in development. One simple way though is to use [hotel](https://github.com/typicode/hotel).
### Add custom routes
Create a `routes.json` file. Pay attention to start every route with `/`.
```json
{
"/api/": "/",
"/blog/:resource/:id/show": "/:resource/:id"
"/blog/:resource/:id/show": "/:resource/:id",
"/blog/:category": "/posts/:id?category=:category"
}
```
@ -268,10 +277,11 @@ json-server db.json --routes routes.json
Now you can access resources using additional routes.
```bash
/api/posts
/api/posts/1
/blog/posts/1/show
```sh
/api/posts # → /posts
/api/posts/1 # → /posts/1
/blog/posts/1/show # → /posts/1
/blog/javascript # → /posts?category=javascript
```
### Add middlewares
@ -383,6 +393,9 @@ server.get('/echo', function (req, res) {
res.jsonp(req.query)
})
// To handle POST, PUT and PATCH you need to use a body-parser
// You can use the one used by JSON Server
server.use(jsonServer.bodyParser)
server.use(function (req, res, next) {
if (req.method === 'POST') {
req.body.createdAt = Date.now()

View File

@ -1,2 +1,2 @@
#!/usr/bin/env node
require('../src/cli')()
require('../lib/cli/bin')

View File

@ -1,8 +1,8 @@
{
"name": "json-server",
"version": "0.8.23",
"version": "0.9.0-beta.2",
"description": "Serves JSON files through REST routes.",
"main": "./src/server/index.js",
"main": "./lib/server/index.js",
"bin": "./bin/index.js",
"directories": {
"test": "test"
@ -10,46 +10,46 @@
"dependencies": {
"body-parser": "^1.15.2",
"chalk": "^1.1.3",
"chokidar": "^1.6.0",
"compression": "^1.6.0",
"connect-pause": "^0.1.0",
"cors": "^2.3.0",
"errorhandler": "^1.2.0",
"express": "^4.9.5",
"lodash": "^4.11.2",
"lowdb": "^0.13.1",
"lowdb": "^0.14.0",
"method-override": "^2.1.2",
"morgan": "^1.3.1",
"node-uuid": "^1.4.2",
"object-assign": "^4.0.1",
"pluralize": "^3.0.0",
"request": "^2.72.0",
"server-destroy": "^1.0.1",
"underscore-db": "^0.10.0",
"shortid": "^2.2.6",
"underscore-db": "^0.12.0",
"update-notifier": "^1.0.2",
"yargs": "^4.2.0"
"yargs": "^6.0.0"
},
"devDependencies": {
"babel-cli": "^6.10.1",
"babel-preset-es2015": "^6.9.0",
"cross-env": "^1.0.8",
"babel-preset-es2015": "^6.16.0",
"babel-register": "^6.16.3",
"cross-env": "^2.0.1",
"husky": "^0.11.4",
"mkdirp": "^0.5.1",
"mocha": "^2.2.4",
"mocha": "^3.1.2",
"os-tmpdir": "^1.0.1",
"rimraf": "^2.5.2",
"server-ready": "^0.2.0",
"standard": "^7.1.2",
"supertest": "^1.2.0",
"server-ready": "^0.3.1",
"standard": "^8.3.0",
"supertest": "^2.0.0",
"temp-write": "^2.1.0"
},
"scripts": {
"test": "npm run test:cli && npm run test:server && standard",
"test:cli": "cross-env NODE_ENV=test mocha -R spec test/cli/*.js",
"test:server": "cross-env NODE_ENV=test mocha -R spec test/server/*.js",
"test": "npm run test:cli && npm run test:server && standard --fix",
"test:cli": "npm run build && cross-env NODE_ENV=test mocha test/cli/*.js",
"test:server": "cross-env NODE_ENV=test mocha test/server/*.js",
"start": "node bin",
"prepush": "npm t",
"build": "babel src -d lib"
"build": "babel src -d lib --copy-files"
},
"repository": {
"type": "git",
@ -76,5 +76,11 @@
"bugs": {
"url": "https://github.com/typicode/json-server/issues"
},
"homepage": "https://github.com/typicode/json-server"
"homepage": "https://github.com/typicode/json-server",
"standard": {
"fix": true,
"env": {
"mocha": true
}
}
}

1
src/cli/bin.js Normal file
View File

@ -0,0 +1 @@
require('./')()

9
src/cli/example.json Normal file
View File

@ -0,0 +1,9 @@
{
"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
],
"comments": [
{ "id": 1, "body": "some comment", "postId": 1 }
],
"profile": { "name": "typicode" }
}

View File

@ -1,12 +1,12 @@
var updateNotifier = require('update-notifier')
var yargs = require('yargs')
var run = require('./run')
var pkg = require('../../package.json')
const updateNotifier = require('update-notifier')
const yargs = require('yargs')
const run = require('./run')
const pkg = require('../../package.json')
module.exports = function () {
updateNotifier({ pkg: pkg }).notify()
updateNotifier({ pkg }).notify()
var argv = yargs
const argv = yargs
.config('config')
.usage('$0 [options] <source>')
.options({

View File

@ -1,22 +1,22 @@
var fs = require('fs')
var path = require('path')
var _ = require('lodash')
var chalk = require('chalk')
var chokidar = require('chokidar')
var enableDestroy = require('server-destroy')
var pause = require('connect-pause')
var is = require('./utils/is')
var load = require('./utils/load')
var jsonServer = require('../server')
const fs = require('fs')
const path = require('path')
const _ = require('lodash')
const chalk = require('chalk')
const enableDestroy = require('server-destroy')
const pause = require('connect-pause')
const is = require('./utils/is')
const load = require('./utils/load')
const example = require('./example.json')
const jsonServer = require('../server')
function prettyPrint (argv, object, rules) {
var host = argv.host === '0.0.0.0' ? 'localhost' : argv.host
var port = argv.port
var root = 'http://' + host + ':' + port
const host = argv.host === '0.0.0.0' ? 'localhost' : argv.host
const port = argv.port
const root = `http://${host}:${port}`
console.log()
console.log(chalk.bold(' Resources'))
for (var prop in object) {
for (let prop in object) {
console.log(' ' + root + '/' + prop)
}
@ -35,15 +35,23 @@ function prettyPrint (argv, object, rules) {
}
function createApp (source, object, routes, middlewares, argv) {
var app = jsonServer.create()
const app = jsonServer.create()
var router = jsonServer.router(
let router
try {
router = jsonServer.router(
is.JSON(source)
? source
: object
)
} catch (e) {
console.log()
console.error(chalk.red(e.message.replace(/^/gm, ' ')))
process.exit(1)
}
var defaultsOpts = {
const defaultsOpts = {
logger: !argv.quiet,
readOnly: argv.readOnly,
noCors: argv.noCors,
@ -54,11 +62,11 @@ function createApp (source, object, routes, middlewares, argv) {
defaultsOpts.static = path.join(process.cwd(), argv.static)
}
var defaults = jsonServer.defaults(defaultsOpts)
const defaults = jsonServer.defaults(defaultsOpts)
app.use(defaults)
if (routes) {
var rewriter = jsonServer.rewriter(routes)
const rewriter = jsonServer.rewriter(routes)
app.use(rewriter)
}
@ -78,18 +86,18 @@ function createApp (source, object, routes, middlewares, argv) {
}
module.exports = function (argv) {
var source = argv._[0]
var app
var server
const source = argv._[0]
let app
let server
if (!fs.existsSync(argv.snapshots)) {
console.log('Error: snapshots directory ' + argv.snapshots + ' doesn\'t exist')
console.log(`Error: snapshots directory ${argv.snapshots} doesn't exist`)
process.exit(1)
}
// noop log fn
if (argv.quiet) {
console.log = function () {}
console.log = () => {}
}
console.log()
@ -97,20 +105,30 @@ module.exports = function (argv) {
function start (cb) {
console.log()
// Be nice and create a default db.json if it doesn't exist
if (is.JSON(source) && !fs.existsSync(source)) {
console.log(chalk.yellow(` Oops, ${source} doesn't seem to exist`))
console.log(chalk.yellow(` Creating ${source} with some default data`))
console.log()
fs.writeFileSync(source, JSON.stringify(example, null, 2))
}
console.log(chalk.gray(' Loading', source))
// Load JSON, JS or HTTP database
load(source, function (err, data) {
load(source, (err, data) => {
if (err) throw err
// Load additional routes
let routes
if (argv.routes) {
console.log(chalk.gray(' Loading', argv.routes))
var routes = JSON.parse(fs.readFileSync(argv.routes))
routes = JSON.parse(fs.readFileSync(argv.routes))
}
// Load middlewares
var middlewares
let middlewares
if (argv.middlewares) {
middlewares = argv.middlewares.map(function (m) {
console.log(chalk.gray(' Loading', m))
@ -136,7 +154,7 @@ module.exports = function (argv) {
}
// Start server
start(function () {
start(() => {
// Snapshot
console.log(
chalk.gray(' Type s + enter at any time to create a snapshot of the database')
@ -144,13 +162,13 @@ module.exports = function (argv) {
process.stdin.resume()
process.stdin.setEncoding('utf8')
process.stdin.on('data', function (chunk) {
process.stdin.on('data', (chunk) => {
if (chunk.trim().toLowerCase() === 's') {
var filename = 'db-' + Date.now() + '.json'
var file = path.join(argv.snapshots, filename)
var state = app.db.getState()
const filename = 'db-' + Date.now() + '.json'
const file = path.join(argv.snapshots, filename)
const state = app.db.getState()
fs.writeFileSync(file, JSON.stringify(state, null, 2), 'utf-8')
console.log(' Saved snapshot to ' + path.relative(process.cwd(), file) + '\n')
console.log(` Saved snapshot to ${path.relative(process.cwd(), file)}\n`)
}
})
@ -158,28 +176,28 @@ module.exports = function (argv) {
if (argv.watch) {
console.log(chalk.gray(' Watching...'))
console.log()
var source = argv._[0]
const source = argv._[0]
// Can't watch URL
if (is.URL(source)) throw new Error('Can\'t watch URL')
// Watch .js or .json file
// Since lowdb uses atomic writing, directory is watched instead of file
chokidar
.watch(path.dirname(source))
.on('change', function (file) {
if (path.resolve(file) === path.resolve(source)) {
if (is.JSON(file)) {
var obj = JSON.parse(fs.readFileSync(file))
const watchedDir = path.dirname(source)
fs.watch(watchedDir, (event, file) => {
const watchedFile = path.resolve(watchedDir, file)
if (watchedFile === path.resolve(source)) {
if (is.JSON(watchedFile)) {
var obj = JSON.parse(fs.readFileSync(watchedFile))
// Compare .json file content with in memory database
var isDatabaseDifferent = !_.isEqual(obj, app.db.getState())
if (isDatabaseDifferent) {
console.log(chalk.gray(' ' + file + ' has changed, reloading...'))
console.log(chalk.gray(` ${source} has changed, reloading...`))
server && server.destroy()
start()
}
} else {
console.log(chalk.gray(' ' + file + ' has changed, reloading...'))
console.log(chalk.gray(` ${source} has changed, reloading...`))
server && server.destroy()
start()
}
@ -188,12 +206,14 @@ module.exports = function (argv) {
// Watch routes
if (argv.routes) {
chokidar
.watch(argv.routes)
.on('change', function (file) {
console.log(chalk.gray(' ' + file + ' has changed, reloading...'))
const watchedDir = path.dirname(argv.routes)
fs.watch(watchedDir, (event, file) => {
const watchedFile = path.resolve(watchedDir, file)
if (watchedFile === path.resolve(argv.routes)) {
console.log(chalk.gray(` ${argv.routes} has changed, reloading...`))
server && server.destroy()
start()
}
})
}
}

View File

@ -1,17 +1,17 @@
module.exports = {
JSON: isJSON,
JS: isJS,
URL: isURL
JSON,
JS,
URL
}
function isJSON (s) {
return !isURL(s) && /\.json$/.test(s)
function JSON (s) {
return !URL(s) && /\.json$/.test(s)
}
function isJS (s) {
return !isURL(s) && /\.js$/.test(s)
function JS (s) {
return !URL(s) && /\.js$/.test(s)
}
function isURL (s) {
function URL (s) {
return /^(http|https):/.test(s)
}

View File

@ -1,30 +1,37 @@
var path = require('path')
var request = require('request')
var low = require('lowdb')
var fileAsync = require('lowdb/lib/file-async')
var is = require('./is')
const path = require('path')
const request = require('request')
const low = require('lowdb')
const fileAsync = require('lowdb/lib/file-async')
const is = require('./is')
module.exports = function (source, cb) {
var data
if (is.URL(source)) {
request({ url: source, json: true }, function (err, response) {
// Load remote data
const opts = {
url: source,
json: true
}
request(opts, (err, response) => {
if (err) return cb(err)
cb(null, response.body)
})
} else if (is.JS(source)) {
var filename = path.resolve(source)
// Clear cache
const filename = path.resolve(source)
delete require.cache[filename]
var dataFn = require(filename)
const dataFn = require(filename)
if (typeof dataFn !== 'function') {
throw new Error('The database is a JavaScript file but the export is not a function.')
}
data = dataFn()
// Run dataFn to generate data
const data = dataFn()
cb(null, data)
} else if (is.JSON(source)) {
data = low(source, { storage: fileAsync }).getState()
// Load JSON using lowdb
const data = low(source, { storage: fileAsync }).getState()
cb(null, data)
} else {
throw new Error('Unsupported source ' + source)

View File

@ -0,0 +1,6 @@
const bodyParser = require('body-parser')
module.exports = [
bodyParser.json({limit: '10mb', extended: false}),
bodyParser.urlencoded({extended: false})
]

View File

@ -1,9 +0,0 @@
var bodyParser = require('body-parser')
var methodOverride = require('method-override')
// common middlewares used in ./defaults.js and ./router/index.js
module.exports = [
bodyParser.json({limit: '10mb', extended: false}),
bodyParser.urlencoded({extended: false}),
methodOverride()
]

View File

@ -1,23 +1,22 @@
var fs = require('fs')
var path = require('path')
var express = require('express')
var logger = require('morgan')
var cors = require('cors')
var compression = require('compression')
var errorhandler = require('errorhandler')
var objectAssign = require('object-assign')
var common = require('./common')
const fs = require('fs')
const path = require('path')
const express = require('express')
const logger = require('morgan')
const cors = require('cors')
const compression = require('compression')
const errorhandler = require('errorhandler')
const objectAssign = require('object-assign')
module.exports = function (opts) {
var userDir = path.join(process.cwd(), 'public')
var defaultDir = path.join(__dirname, 'public')
var staticDir = fs.existsSync(userDir)
const userDir = path.join(process.cwd(), 'public')
const defaultDir = path.join(__dirname, 'public')
const staticDir = fs.existsSync(userDir)
? userDir
: defaultDir
opts = objectAssign({ logger: true, static: staticDir }, opts)
var arr = []
const arr = []
// Compress all requests
if (!opts.noGzip) {
@ -26,12 +25,14 @@ module.exports = function (opts) {
// Logger
if (opts.logger) {
arr.push(logger('dev', {
skip: function (req, res) {
return process.env.NODE_ENV === 'test' ||
arr.push(
logger('dev', {
skip: (req) => (
process.env.NODE_ENV === 'test' ||
req.path === '/favicon.ico'
}
}))
)
})
)
}
// Enable CORS for all the requests, including static files
@ -49,7 +50,7 @@ module.exports = function (opts) {
// No cache for IE
// https://support.microsoft.com/en-us/kb/234067
arr.push(function (req, res, next) {
arr.push((req, res, next) => {
res.header('Cache-Control', 'no-cache')
res.header('Pragma', 'no-cache')
res.header('Expires', '-1')
@ -58,7 +59,7 @@ module.exports = function (opts) {
// Read-only
if (opts.readOnly) {
arr.push(function (req, res, next) {
arr.push((req, res, next) => {
if (req.method === 'GET') {
next() // Continue
} else {
@ -67,5 +68,5 @@ module.exports = function (opts) {
})
}
return arr.concat(common)
return arr
}

View File

@ -1,12 +1,9 @@
var express = require('express')
const express = require('express')
module.exports = {
create: function () {
var server = express()
server.set('json spaces', 2)
return server
},
create: () => express().set('json spaces', 2),
defaults: require('./defaults'),
router: require('./router'),
rewriter: require('./rewriter')
rewriter: require('./rewriter'),
bodyParser: require('./body-parser')
}

View File

@ -1,26 +1,26 @@
var uuid = require('node-uuid')
var pluralize = require('pluralize')
const shortid = require('shortid')
const pluralize = require('pluralize')
module.exports = {
getRemovable: getRemovable,
createId: createId,
deepQuery: deepQuery
getRemovable,
createId,
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) {
const _ = this
const removable = []
_.each(db, (coll, collName) => {
_.each(coll, (doc) => {
_.each(doc, (value, key) => {
if (/Id$/.test(key)) {
var refName = pluralize.plural(key.slice(0, -2))
const 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)
const ref = _.getById(db[refName], value)
if (_.isUndefined(ref)) {
removable.push({ name: collName, id: doc.id })
}
@ -36,36 +36,31 @@ function getRemovable (db) {
// Return incremented id or uuid
// Used to override underscore-db's createId with utils.createId
function createId (coll) {
var _ = this
var idProperty = _.__id()
const _ = this
const idProperty = _.__id()
if (_.isEmpty(coll)) {
return 1
} else {
var id = _.maxBy(coll, function (doc) {
return doc[idProperty]
})[idProperty]
let id = _(coll).maxBy(idProperty)[idProperty]
if (_.isFinite(id)) {
// Increment integer id
return ++id
} else {
// Generate string id
return uuid()
}
// Increment integer id or generate string id
return _.isFinite(id)
? ++id
: shortid.generate()
}
}
function deepQuery (value, q) {
var _ = this
const _ = this
if (value && q) {
if (_.isArray(value)) {
for (var i = 0; i < value.length; i++) {
for (let i = 0; i < value.length; i++) {
if (_.deepQuery(value[i], q)) {
return true
}
}
} else if (_.isObject(value) && !_.isArray(value)) {
for (var k in value) {
for (let k in value) {
if (_.deepQuery(value[k], q)) {
return true
}

View File

@ -1,16 +1,16 @@
var express = require('express')
var url = require('url')
var _ = require('lodash')
const express = require('express')
const url = require('url')
const _ = require('lodash')
module.exports = function (routes) {
var router = express.Router()
module.exports = (routes) => {
const router = express.Router()
Object.keys(routes).forEach(function (route) {
Object.keys(routes).forEach((route) => {
if (route.indexOf(':') !== -1) {
router.all(route, function (req, res, next) {
router.all(route, (req, res, next) => {
// Rewrite target url using params
var target = routes[route]
for (var param in req.params) {
let target = routes[route]
for (let param in req.params) {
target = target.replace(':' + param, req.params[param])
}
req.url = target
@ -21,7 +21,7 @@ module.exports = function (routes) {
next()
})
} else {
router.all(route + '*', function (req, res, next) {
router.all(route + '*', (req, res, next) => {
// Rewrite url by replacing prefix
req.url = req.url.replace(route, routes[route])
next()

View File

@ -1,23 +1,26 @@
var express = require('express')
var _ = require('lodash')
var _db = require('underscore-db')
var low = require('lowdb')
var fileAsync = require('lowdb/lib/file-async')
var plural = require('./plural')
var nested = require('./nested')
var singular = require('./singular')
var mixins = require('../mixins')
var common = require('../common')
const express = require('express')
const methodOverride = require('method-override')
const _ = require('lodash')
const _db = require('underscore-db')
const low = require('lowdb')
const fileAsync = require('lowdb/lib/file-async')
const bodyParser = require('../body-parser')
const validateData = require('./validate-data')
const plural = require('./plural')
const nested = require('./nested')
const singular = require('./singular')
const mixins = require('../mixins')
module.exports = function (source) {
module.exports = (source) => {
// Create router
var router = express.Router()
const router = express.Router()
// Add middlewares
router.use(common)
router.use(methodOverride())
router.use(bodyParser)
// Create database
var db
let db
if (_.isObject(source)) {
db = low()
db.setState(source)
@ -25,6 +28,8 @@ module.exports = function (source) {
db = low(source, { storage: fileAsync })
}
validateData(db.getState())
// Add underscore-db methods to db
db._.mixin(_db)
@ -35,40 +40,39 @@ module.exports = function (source) {
router.db = db
// Expose render
router.render = function (req, res) {
router.render = (req, res) => {
res.jsonp(res.locals.data)
}
// GET /db
function showDatabase (req, res, next) {
router.get('/db', (req, res) => {
res.jsonp(db.getState())
}
router.get('/db', showDatabase)
})
// Handle /:parent/:parentId/:resource
router.use(nested())
// Create routes
db.forEach(function (value, key) {
db.forEach((value, key) => {
if (_.isPlainObject(value)) {
router.use('/' + key, singular(db, key))
router.use(`/${key}`, singular(db, key))
return
}
if (_.isArray(value)) {
router.use('/' + key, plural(db, key))
router.use(`/${key}`, plural(db, key))
return
}
var msg =
'Type of "' + key + '" (' + typeof value + ') ' +
(_.isObject(source) ? '' : 'in ' + source) + ' is not supported. ' +
const msg =
`Type of "${key}" (${typeof value}) ` +
(_.isObject(source) ? '' : `in ${source}`) + ' is not supported. ' +
'Use objects or arrays of objects.'
throw new Error(msg)
}).value()
router.use(function (req, res) {
router.use((req, res) => {
if (!res.locals.data) {
res.status(404)
res.locals.data = {}
@ -77,7 +81,7 @@ module.exports = function (source) {
router.render(req, res)
})
router.use(function (err, req, res, next) {
router.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).send(err.stack)
})

View File

@ -1,23 +1,22 @@
var express = require('express')
var pluralize = require('pluralize')
var utils = require('../utils')
const express = require('express')
const pluralize = require('pluralize')
module.exports = function () {
var router = express.Router()
module.exports = () => {
const router = express.Router()
// Rewrite URL (/:resource/:id/:nested -> /:nested) and request query
function get (req, res, next) {
var prop = pluralize.singular(req.params.resource)
req.query[prop + 'Id'] = utils.toNative(req.params.id)
req.url = '/' + req.params.nested
const prop = pluralize.singular(req.params.resource)
req.query[`${prop}Id`] = req.params.id
req.url = `/${req.params.nested}`
next()
}
// Rewrite URL (/:resource/:id/:nested -> /:nested) and request body
function post (req, res, next) {
var prop = pluralize.singular(req.params.resource)
req.body[prop + 'Id'] = utils.toNative(req.params.id)
req.url = '/' + req.params.nested
const prop = pluralize.singular(req.params.resource)
req.body[`${prop}Id`] = req.params.id
req.url = `/${req.params.nested}`
next()
}

View File

@ -1,20 +1,21 @@
var express = require('express')
var _ = require('lodash')
var pluralize = require('pluralize')
var utils = require('../utils')
const url = require('url')
const express = require('express')
const _ = require('lodash')
const pluralize = require('pluralize')
const utils = require('../utils')
module.exports = function (db, name) {
module.exports = (db, name) => {
// Create router
var router = express.Router()
const router = express.Router()
// Embed function used in GET /name and GET /name/id
function embed (resource, e) {
e && [].concat(e)
.forEach(function (externalResource) {
.forEach((externalResource) => {
if (db.get(externalResource).value) {
var query = {}
var singularResource = pluralize.singular(name)
query[singularResource + 'Id'] = resource.id
const query = {}
const singularResource = pluralize.singular(name)
query[`${singularResource}Id`] = resource.id
resource[externalResource] = db.get(externalResource).filter(query).value()
}
})
@ -23,17 +24,22 @@ module.exports = function (db, name) {
// Expand function used in GET /name and GET /name/id
function expand (resource, e) {
e && [].concat(e)
.forEach(function (innerResource) {
var plural = pluralize(innerResource)
.forEach((innerResource) => {
const plural = pluralize(innerResource)
if (db.get(plural).value()) {
var prop = innerResource + 'Id'
const prop = `${innerResource}Id`
resource[innerResource] = db.get(plural).getById(resource[prop]).value()
}
})
}
function getFullURL (req) {
return req.protocol + '://' + req.get('host') + req.originalUrl
const root = url.format({
protocol: req.protocol,
host: req.get('host')
})
return `${root}${req.originalUrl}`
}
// GET /name
@ -44,19 +50,19 @@ module.exports = function (db, name) {
// GET /name?_embed=&_expand=
function list (req, res, next) {
// Resource chain
var chain = db.get(name)
let chain = db.get(name)
// Remove q, _start, _end, ... from req.query to avoid filtering using those
// parameters
var q = req.query.q
var _start = req.query._start
var _end = req.query._end
var _page = req.query._page
var _sort = req.query._sort
var _order = req.query._order
var _limit = req.query._limit
var _embed = req.query._embed
var _expand = req.query._expand
let q = req.query.q
let _start = req.query._start
let _end = req.query._end
let _page = req.query._page
let _sort = req.query._sort
let _order = req.query._order
let _limit = req.query._limit
let _embed = req.query._embed
let _expand = req.query._expand
delete req.query.q
delete req.query._start
delete req.query._end
@ -68,17 +74,17 @@ module.exports = function (db, name) {
// Automatically delete query parameters that can't be found
// in the database
Object.keys(req.query).forEach(function (query) {
var arr = db.get(name).value()
for (var i in arr) {
Object.keys(req.query).forEach((query) => {
const arr = db.get(name).value()
for (let i in arr) {
if (
_.has(arr[i], query) ||
query === 'callback' ||
query === '_' ||
query.indexOf('_lte') !== -1 ||
query.indexOf('_gte') !== -1 ||
query.indexOf('_ne') !== -1 ||
query.indexOf('_like') !== -1
/_lte$/.test(query) ||
/_gte$/.test(query) ||
/_ne$/.test(query) ||
/_like$/.test(query)
) return
}
delete req.query[query]
@ -88,9 +94,9 @@ module.exports = function (db, name) {
// Full-text search
q = q.toLowerCase()
chain = chain.filter(function (obj) {
for (var key in obj) {
var value = obj[key]
chain = chain.filter((obj) => {
for (let key in obj) {
const value = obj[key]
if (db._.deepQuery(value, q)) {
return true
}
@ -98,41 +104,41 @@ module.exports = function (db, name) {
})
}
Object.keys(req.query).forEach(function (key) {
Object.keys(req.query).forEach((key) => {
// Don't take into account JSONP query parameters
// jQuery adds a '_' query parameter too
if (key !== 'callback' && key !== '_') {
// Always use an array, in case req.query is an array
var arr = [].concat(req.query[key])
const arr = [].concat(req.query[key])
chain = chain.filter(function (element) {
chain = chain.filter((element) => {
return arr
.map(utils.toNative)
.map(function (value) {
var isDifferent = key.indexOf('_ne') !== -1
var isRange = key.indexOf('_lte') !== -1 || key.indexOf('_gte') !== -1
var isLike = key.indexOf('_like') !== -1
var path = key.replace(/(_lte|_gte|_ne|_like)$/, '')
var elementValue = _.get(element, path)
const isDifferent = /_ne$/.test(key)
const isRange = /_lte$/.test(key) || /_gte$/.test(key)
const isLike = /_like$/.test(key)
const path = key.replace(/(_lte|_gte|_ne|_like)$/, '')
const elementValue = _.get(element, path)
if (!elementValue) {
return
}
if (isRange) {
var isLowerThan = key.indexOf('_gte') !== -1
const isLowerThan = /_gte$/.test(key)
if (isLowerThan) {
return value <= elementValue
} else {
return value >= elementValue
}
return isLowerThan
? value <= elementValue
: value >= elementValue
} else if (isDifferent) {
return value !== elementValue
return value !== elementValue.toString()
} else if (isLike) {
return new RegExp(value, 'i').test(elementValue)
return new RegExp(value, 'i').test(elementValue.toString())
} else {
return _.matchesProperty(key, value)(element)
return value === elementValue.toString()
}
}).reduce(function (a, b) {
return a || b
})
.reduce((a, b) => a || b)
})
}
})
@ -160,9 +166,9 @@ module.exports = function (db, name) {
_page = parseInt(_page, 10)
_page = _page >= 1 ? _page : 1
_limit = parseInt(_limit, 10) || 10
var page = utils.getPage(chain.value(), _page, _limit)
var links = {}
var fullURL = getFullURL(req)
const page = utils.getPage(chain.value(), _page, _limit)
const links = {}
const fullURL = getFullURL(req)
if (page.first) {
links.first = fullURL.replace('page=' + page.current, 'page=' + page.first)
@ -207,24 +213,25 @@ module.exports = function (db, name) {
// GET /name/:id
// GET /name/:id?_embed=&_expand
function show (req, res, next) {
var _embed = req.query._embed
var _expand = req.query._expand
var id = utils.toNative(req.params.id)
var resource = db.get(name).getById(id).value()
const _embed = req.query._embed
const _expand = req.query._expand
const resource = db.get(name)
.getById(req.params.id)
.value()
if (resource) {
// Clone resource to avoid making changes to the underlying object
resource = _.cloneDeep(resource)
const clone = _.cloneDeep(resource)
// Embed other resources based on resource id
// /posts/1?_embed=comments
embed(resource, _embed)
embed(clone, _embed)
// Expand inner resources based on id
// /posts/1?_expand=user
expand(resource, _expand)
expand(clone, _expand)
res.locals.data = resource
res.locals.data = clone
}
next()
@ -232,11 +239,7 @@ module.exports = function (db, name) {
// POST /name
function create (req, res, next) {
for (var key in req.body) {
req.body[key] = utils.toNative(req.body[key])
}
var resource = db.get(name)
const resource = db.get(name)
.insert(req.body)
.value()
@ -248,18 +251,14 @@ module.exports = function (db, name) {
// PUT /name/:id
// PATCH /name/:id
function update (req, res, next) {
for (var key in req.body) {
req.body[key] = utils.toNative(req.body[key])
}
var id = utils.toNative(req.params.id)
var chain = db.get(name)
const id = req.params.id
let chain = db.get(name)
chain = req.method === 'PATCH'
? chain.updateById(id, req.body)
: chain.replaceById(id, req.body)
var resource = chain.value()
const resource = chain.value()
if (resource) {
res.locals.data = resource
@ -270,13 +269,17 @@ module.exports = function (db, name) {
// DELETE /name/:id
function destroy (req, res, next) {
var resource = db.get(name).removeById(utils.toNative(req.params.id)).value()
const resource = db.get(name)
.removeById(req.params.id)
.value()
// Remove dependents documents
var removable = db._.getRemovable(db.getState())
const removable = db._.getRemovable(db.getState())
_.each(removable, function (item) {
db.get(item.name).removeById(item.id).value()
removable.forEach((item) => {
db.get(item.name)
.removeById(item.id)
.value()
})
if (resource) {

View File

@ -1,7 +1,7 @@
var express = require('express')
const express = require('express')
module.exports = function (db, name) {
var router = express.Router()
module.exports = (db, name) => {
const router = express.Router()
function show (req, res, next) {
res.locals.data = db.get(name).value()

View File

@ -0,0 +1,26 @@
const _ = require('lodash')
function validateKey (key) {
if (key.indexOf('/') !== -1) {
const msg = [
`Oops, found / character in database property '${key}'.`,
'',
'/ aren\'t supported, if you want to tweak default routes, see',
'https://github.com/typicode/json-server/tree/next#add-custom-routes'
].join('\n')
throw new Error(msg)
}
}
module.exports = (obj) => {
if (_.isPlainObject(obj)) {
Object
.keys(obj)
.forEach(validateKey)
} else {
throw new Error(
`Data must be an object. Found ${typeof obj}.` +
'See https://github.com/typicode/json-server for example.'
)
}
}

View File

@ -1,27 +1,5 @@
module.exports = {
toNative: toNative,
getPage: getPage
}
// 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
getPage
}
function getPage (array, page, perPage) {

View File

@ -1,70 +1,73 @@
var fs = require('fs')
var path = require('path')
var cp = require('child_process')
var assert = require('assert')
var supertest = require('supertest')
var osTmpdir = require('os-tmpdir')
var tempWrite = require('temp-write')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var express = require('express')
var serverReady = require('server-ready')
var pkg = require('../../package.json')
const fs = require('fs')
const path = require('path')
const cp = require('child_process')
const assert = require('assert')
const supertest = require('supertest')
const osTmpdir = require('os-tmpdir')
const tempWrite = require('temp-write')
const mkdirp = require('mkdirp')
const rimraf = require('rimraf')
const express = require('express')
const serverReady = require('server-ready')
var PORT = 3100
let PORT = 3100
const middlewareFiles = {
en: './fixtures/middlewares/en.js',
jp: './fixtures/middlewares/jp.js'
}
const bin = path.join(__dirname, '../../lib/cli/bin')
function cli (args) {
var bin = path.join(__dirname, '../..', pkg.bin)
return cp.spawn('node', [bin, '-p', PORT].concat(args), {
return cp.spawn('node', ['--', bin, '-p', PORT].concat(args), {
cwd: __dirname,
stdio: ['pipe', process.stdout, process.stderr]
})
}
/* global beforeEach, afterEach, describe, it */
describe('cli', () => {
let child
let request
let dbFile
let routesFile
describe('cli', function () {
var child
var request
var dbFile
var routesFile
var middlewareFiles = {
en: './fixtures/middlewares/en.js',
jp: './fixtures/middlewares/jp.js'
}
beforeEach(function () {
dbFile = tempWrite.sync(JSON.stringify({
beforeEach(() => {
dbFile = tempWrite.sync(
JSON.stringify({
posts: [
{ id: 1 },
{ _id: 2 }
]
}), 'db.json')
}),
'db.json'
)
routesFile = tempWrite.sync(JSON.stringify({
'/blog/': '/'
}), 'routes.json')
routesFile = tempWrite.sync(
JSON.stringify({ '/blog/': '/' }),
'routes.json'
)
++PORT
request = supertest('http://localhost:' + PORT)
request = supertest(`http://localhost:${PORT}`)
})
afterEach(function () {
child.kill()
afterEach(() => {
child.kill('SIGKILL')
})
describe('db.json', function () {
beforeEach(function (done) {
describe('db.json', () => {
beforeEach((done) => {
child = cli([ dbFile ])
serverReady(PORT, done)
})
it('should support JSON file', function (done) {
it('should support JSON file', (done) => {
request.get('/posts').expect(200, done)
})
it('should send CORS headers', function (done) {
var origin = 'http://example.com'
it('should send CORS headers', (done) => {
const origin = 'http://example.com'
request.get('/posts')
.set('Origin', origin)
@ -72,12 +75,12 @@ describe('cli', function () {
.expect(200, done)
})
it('should update JSON file', function (done) {
it('should update JSON file', (done) => {
request.post('/posts')
.send({ title: 'hello' })
.end(function () {
setTimeout(function () {
var str = fs.readFileSync(dbFile, 'utf8')
.end(() => {
setTimeout(() => {
const str = fs.readFileSync(dbFile, 'utf8')
assert(str.indexOf('hello') !== -1)
done()
}, 1000)
@ -85,118 +88,122 @@ describe('cli', function () {
})
})
describe('seed.js', function () {
beforeEach(function (done) {
describe('seed.js', () => {
beforeEach((done) => {
child = cli([ 'fixtures/seed.js' ])
serverReady(PORT, done)
})
it('should support JS file', function (done) {
it('should support JS file', (done) => {
request.get('/posts').expect(200, done)
})
})
describe('http://localhost:8080/db', function () {
beforeEach(function (done) {
var fakeServer = express()
fakeServer.get('/db', function (req, res) {
describe('http://localhost:8080/db', () => {
beforeEach((done) => {
const fakeServer = express()
fakeServer.get('/db', (req, res) => {
res.jsonp({ posts: [] })
})
fakeServer.listen(8080, function () {
fakeServer.listen(8080, () => {
child = cli([ 'http://localhost:8080/db' ])
serverReady(PORT, done)
})
})
it('should support URL file', function (done) {
it('should support URL file', (done) => {
request.get('/posts').expect(200, done)
})
})
describe('db.json -r routes.json -m middleware.js -i _id --read-only', function () {
beforeEach(function (done) {
describe('db.json -r routes.json -m middleware.js -i _id --read-only', () => {
beforeEach((done) => {
child = cli([ dbFile, '-r', routesFile, '-m', middlewareFiles.en, '-i', '_id', '--read-only' ])
serverReady(PORT, done)
})
it('should use routes.json and _id as the identifier', function (done) {
it('should use routes.json and _id as the identifier', (done) => {
request.get('/blog/posts/2').expect(200, done)
})
it('should apply middlewares', function (done) {
it('should apply middlewares', (done) => {
request.get('/blog/posts/2').expect('X-Hello', 'World', done)
})
it('should allow only GET requests', function (done) {
it('should allow only GET requests', (done) => {
request.post('/blog/posts').expect(403, done)
})
})
describe('db.json -m first-middleware.js second-middleware.js', function () {
beforeEach(function (done) {
describe('db.json -m first-middleware.js second-middleware.js', () => {
beforeEach((done) => {
child = cli([ dbFile, '-m', middlewareFiles.en, middlewareFiles.jp ])
serverReady(PORT, done)
})
it('should apply all middlewares', function (done) {
it('should apply all middlewares', (done) => {
request.get('/posts')
.expect('X-Hello', 'World')
.expect('X-Konnichiwa', 'Sekai', done)
})
})
describe('db.json -d 1000', function () {
beforeEach(function (done) {
describe('db.json -d 1000', () => {
beforeEach((done) => {
child = cli([ dbFile, '-d', 1000 ])
serverReady(PORT, done)
})
it('should delay response', function (done) {
var start = new Date()
it('should delay response', (done) => {
const start = new Date()
request.get('/posts').expect(200, function (err) {
var end = new Date()
done(end - start > 1000 ? err : new Error('Request wasn\'t delayed'))
const end = new Date()
done(
end - start > 1000
? err
: new Error('Request wasn\'t delayed')
)
})
})
})
describe('db.json -s fixtures/public -S /some/path/snapshots', function () {
var snapshotsDir = path.join(osTmpdir(), 'snapshots')
var publicDir = 'fixtures/public'
describe('db.json -s fixtures/public -S /some/path/snapshots', () => {
const snapshotsDir = path.join(osTmpdir(), 'snapshots')
const publicDir = 'fixtures/public'
beforeEach(function (done) {
beforeEach((done) => {
rimraf.sync(snapshotsDir)
mkdirp.sync(snapshotsDir)
child = cli([ dbFile, '-s', publicDir, '-S', snapshotsDir ])
serverReady(PORT, function () {
serverReady(PORT, () => {
child.stdin.write('s\n')
setTimeout(done, 100)
})
})
it('should serve fixtures/public', function (done) {
it('should serve fixtures/public', (done) => {
request.get('/').expect(/Hello/, done)
})
it('should save a snapshot in snapshots dir', function () {
it('should save a snapshot in snapshots dir', () => {
assert.equal(fs.readdirSync(snapshotsDir).length, 1)
})
})
describe('fixtures/seed.json --no-cors=true', function () {
beforeEach(function (done) {
describe('fixtures/seed.json --no-cors=true', () => {
beforeEach((done) => {
child = cli([ 'fixtures/seed.js', '--no-cors=true' ])
serverReady(PORT, done)
})
it('should not send Access-Control-Allow-Origin headers', function (done) {
var origin = 'http://example.com'
it('should not send Access-Control-Allow-Origin headers', (done) => {
const origin = 'http://example.com'
request.get('/posts')
.set('Origin', origin)
.expect(200)
.end(function (err, res) {
.end((err, res) => {
if (err) {
done(err)
} if ('access-control-allow-origin' in res.headers) {
@ -208,13 +215,13 @@ describe('cli', function () {
})
})
describe('fixtures/seed.json --no-gzip=true', function () {
beforeEach(function (done) {
describe('fixtures/seed.json --no-gzip=true', () => {
beforeEach((done) => {
child = cli([ 'fixtures/seed.js', '--no-gzip=true' ])
serverReady(PORT, done)
})
it('should not set Content-Encoding to gzip', function (done) {
it('should not set Content-Encoding to gzip', (done) => {
request.get('/posts')
.expect(200)
.end(function (err, res) {
@ -229,39 +236,55 @@ describe('cli', function () {
})
})
describe('--watch db.json -r routes.json', function () {
beforeEach(function (done) {
child = cli(['--watch', dbFile, '-r', routesFile])
describe('--watch db.json -r routes.json', () => {
beforeEach((done) => {
child = cli([ dbFile, '-r', routesFile, '--watch' ])
serverReady(PORT, done)
})
it('should watch db file', function (done) {
it('should watch db file', (done) => {
fs.writeFileSync(dbFile, JSON.stringify({ foo: [] }))
setTimeout(function () {
setTimeout(() => {
request.get('/foo').expect(200, done)
}, 1000)
})
it('should watch routes file', function (done) {
// Can be very slow
this.timeout(10000)
it('should watch routes file', (done) => {
fs.writeFileSync(routesFile, JSON.stringify({ '/api/': '/' }))
setTimeout(function () {
setTimeout(() => {
request.get('/api/posts').expect(200, done)
}, 9000)
}, 1000)
})
})
describe('db.json --config some-config.json', function (done) {
beforeEach(function (done) {
child = cli([dbFile, '--config', 'fixtures/config.json'])
describe('non existent db.json', () => {
beforeEach((done) => {
fs.unlinkSync(dbFile)
child = cli([ dbFile ])
serverReady(PORT, done)
})
it('should apply all middlewares', function (done) {
request.get('/posts')
.expect('X-Hello', 'World')
.expect('X-Konnichiwa', 'Sekai', done)
it('should create JSON file if it doesn\'t exist', (done) => {
request.get('/posts').expect(200, done)
})
})
describe('db.json with error', () => {
beforeEach(() => {
dbFile = tempWrite.sync(
JSON.stringify({ 'a/b': [] }),
'db-error.json'
)
})
it('should exit with an error', (done) => {
child = cli([ dbFile ])
child.on('exit', (code) => {
if (code === 1) {
return done()
}
return done(new Error('should exit with error code'))
})
})
})
})

3
test/mocha.opts Normal file
View File

@ -0,0 +1,3 @@
--compilers js:babel-register
--reporter spec
--timeout 5000

View File

@ -1,14 +1,18 @@
var assert = require('assert')
var _ = require('lodash')
var _db = require('underscore-db')
var mixins = require('../../src/server/mixins')
const assert = require('assert')
const _ = require('lodash')
const _db = require('underscore-db')
const mixins = require('../../src/server/mixins')
/* global describe, it */
describe('mixins', () => {
let db
describe('mixins', function () {
describe('getRemovable', function () {
it('should return removable documents', function () {
var db = {
before(() => {
_.mixin(_db)
_.mixin(mixins)
})
beforeEach(() => {
db = {
posts: [
{ id: 1, comment: 1 }
],
@ -17,18 +21,32 @@ describe('mixins', function () {
// Comments below references a post that doesn't exist
{ id: 2, postId: 2 },
{ id: 3, postId: 2 }
],
photos: [
{ id: '1' },
{ id: '2' }
]
}
})
var expected = [
describe('getRemovable', () => {
it('should return removable documents', () => {
const expected = [
{ name: 'comments', id: 2 },
{ name: 'comments', id: 3 }
]
_.mixin(_db)
_.mixin(mixins)
assert.deepEqual(_.getRemovable(db), expected)
})
})
describe('createId', () => {
it('should return a new id', () => {
assert.equal(_.createId(db.comments), 4)
})
it('should return a new uuid', () => {
assert.notEqual(_.createId(db.photos), 3)
})
})
})

View File

@ -1,15 +1,14 @@
var assert = require('assert')
var _ = require('lodash')
var request = require('supertest')
var jsonServer = require('../../src/server')
const assert = require('assert')
const _ = require('lodash')
const request = require('supertest')
const jsonServer = require('../../src/server')
/* global beforeEach, describe, it */
describe('Server', function () {
var server
var router
var db
describe('Server', () => {
let server
let router
let db
beforeEach(function () {
beforeEach(() => {
db = {}
db.posts = [
@ -24,8 +23,8 @@ describe('Server', function () {
]
db.users = [
{id: 1, username: 'Jim'},
{id: 2, username: 'George'}
{ id: 1, username: 'Jim', tel: '0123' },
{ id: 2, username: 'George', tel: '123' }
]
db.comments = [
@ -40,6 +39,10 @@ describe('Server', function () {
{ id: 'abcd-1234', url: 'http://example.com', postId: 1, userId: 1 }
]
db.stringIds = [
{ id: '1234' }
]
db.deep = [
{ a: { b: 1 } },
{ a: 1 }
@ -80,8 +83,8 @@ describe('Server', function () {
server.use(router)
})
describe('GET /db', function () {
it('should respond with json and full database', function (done) {
describe('GET /db', () => {
it('should respond with json and full database', (done) => {
request(server)
.get('/db')
.expect('Content-Type', /json/)
@ -90,8 +93,8 @@ describe('Server', function () {
})
})
describe('GET /:resource', function () {
it('should respond with json and corresponding resources', function (done) {
describe('GET /:resource', () => {
it('should respond with json and corresponding resources', (done) => {
request(server)
.get('/posts')
.set('Origin', 'http://example.com')
@ -102,15 +105,15 @@ describe('Server', function () {
.expect(200, done)
})
it('should respond with 404 if resource is not found', function (done) {
it('should respond with 404 if resource is not found', (done) => {
request(server)
.get('/undefined')
.expect(404, done)
})
})
describe('GET /:resource?attr=&attr=', function () {
it('should respond with json and filter resources', function (done) {
describe('GET /:resource?attr=&attr=', () => {
it('should respond with json and filter resources', (done) => {
request(server)
.get('/comments?postId=1&published=true')
.expect('Content-Type', /json/)
@ -118,7 +121,15 @@ describe('Server', function () {
.expect(200, done)
})
it('should support multiple filters', function (done) {
it('should be strict', (done) => {
request(server)
.get('/users?tel=123')
.expect('Content-Type', /json/)
.expect([ db.users[1] ])
.expect(200, done)
})
it('should support multiple filters', (done) => {
request(server)
.get('/comments?id=1&id=2')
.expect('Content-Type', /json/)
@ -126,7 +137,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should support deep filter', function (done) {
it('should support deep filter', (done) => {
request(server)
.get('/deep?a.b=1')
.expect('Content-Type', /json/)
@ -134,7 +145,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should ignore JSONP query parameters callback and _ ', function (done) {
it('should ignore JSONP query parameters callback and _ ', (done) => {
request(server)
.get('/comments?callback=1&_=1')
.expect('Content-Type', /text/)
@ -142,7 +153,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should ignore unknown query parameters', function (done) {
it('should ignore unknown query parameters', (done) => {
request(server)
.get('/comments?foo=1&bar=2')
.expect('Content-Type', /json/)
@ -151,8 +162,8 @@ describe('Server', function () {
})
})
describe('GET /:resource?q=', function () {
it('should respond with json and make a full-text search', function (done) {
describe('GET /:resource?q=', () => {
it('should respond with json and make a full-text search', (done) => {
request(server)
.get('/tags?q=pho')
.expect('Content-Type', /json/)
@ -160,7 +171,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should respond with json and make a deep full-text search', function (done) {
it('should respond with json and make a deep full-text search', (done) => {
request(server)
.get('/deep?q=1')
.expect('Content-Type', /json/)
@ -168,7 +179,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should return an empty array when nothing is matched', function (done) {
it('should return an empty array when nothing is matched', (done) => {
request(server)
.get('/tags?q=nope')
.expect('Content-Type', /json/)
@ -176,7 +187,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should support other query parameters', function (done) {
it('should support other query parameters', (done) => {
request(server)
.get('/comments?q=qu&published=true')
.expect('Content-Type', /json/)
@ -185,8 +196,8 @@ describe('Server', function () {
})
})
describe('GET /:resource?_end=', function () {
it('should respond with a sliced array', function (done) {
describe('GET /:resource?_end=', () => {
it('should respond with a sliced array', (done) => {
request(server)
.get('/comments?_end=2')
.expect('Content-Type', /json/)
@ -197,8 +208,8 @@ describe('Server', function () {
})
})
describe('GET /:resource?_sort=', function () {
it('should respond with json and sort on a field', function (done) {
describe('GET /:resource?_sort=', () => {
it('should respond with json and sort on a field', (done) => {
request(server)
.get('/tags?_sort=body')
.expect('Content-Type', /json/)
@ -206,7 +217,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should reverse sorting with _order=DESC', function (done) {
it('should reverse sorting with _order=DESC', (done) => {
request(server)
.get('/tags?_sort=body&_order=DESC')
.expect('Content-Type', /json/)
@ -214,7 +225,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should sort on numerical field', function (done) {
it('should sort on numerical field', (done) => {
request(server)
.get('/posts?_sort=id&_order=DESC')
.expect('Content-Type', /json/)
@ -222,7 +233,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should sort on nested field', function (done) {
it('should sort on nested field', (done) => {
request(server)
.get('/nested?_sort=resource.name')
.expect('Content-Type', /json/)
@ -231,32 +242,32 @@ describe('Server', function () {
})
})
describe('GET /:resource?_start=&_end=', function () {
it('should respond with a sliced array', function (done) {
describe('GET /:resource?_start=&_end=', () => {
it('should respond with a sliced array', (done) => {
request(server)
.get('/comments?_start=1&_end=2')
.expect('Content-Type', /json/)
.expect('x-total-count', db.comments.length.toString())
.expect('X-Total-Count', db.comments.length.toString())
.expect('Access-Control-Expose-Headers', 'X-Total-Count')
.expect(db.comments.slice(1, 2))
.expect(200, done)
})
})
describe('GET /:resource?_start=&_limit=', function () {
it('should respond with a limited array', function (done) {
describe('GET /:resource?_start=&_limit=', () => {
it('should respond with a limited array', (done) => {
request(server)
.get('/comments?_start=1&_limit=1')
.expect('Content-Type', /json/)
.expect('x-total-count', db.comments.length.toString())
.expect('X-Total-Count', db.comments.length.toString())
.expect('Access-Control-Expose-Headers', 'X-Total-Count')
.expect(db.comments.slice(1, 2))
.expect(200, done)
})
})
describe('GET /:resource?_page=', function () {
it('should paginate', function (done) {
describe('GET /:resource?_page=', () => {
it('should paginate', (done) => {
request(server)
.get('/list?_page=2')
.expect('Content-Type', /json/)
@ -267,9 +278,9 @@ describe('Server', function () {
})
})
describe('GET /:resource?_page=&_limit=', function () {
it('should paginate with a custom limit', function (done) {
var link = [
describe('GET /:resource?_page=&_limit=', () => {
it('should paginate with a custom limit', (done) => {
const link = [
'<http://localhost/list?_page=1&_limit=1>; rel="first"',
'<http://localhost/list?_page=1&_limit=1>; rel="prev"',
'<http://localhost/list?_page=3&_limit=1>; rel="next"',
@ -287,8 +298,8 @@ describe('Server', function () {
})
})
describe('GET /:resource?attr_gte=&attr_lte=', function () {
it('should respond with a limited array', function (done) {
describe('GET /:resource?attr_gte=&attr_lte=', () => {
it('should respond with a limited array', (done) => {
request(server)
.get('/comments?id_gte=2&id_lte=3')
.expect('Content-Type', /json/)
@ -297,8 +308,8 @@ describe('Server', function () {
})
})
describe('GET /:resource?attr_ne=', function () {
it('should respond with a limited array', function (done) {
describe('GET /:resource?attr_ne=', () => {
it('should respond with a limited array', (done) => {
request(server)
.get('/comments?id_ne=1')
.expect('Content-Type', /json/)
@ -307,8 +318,8 @@ describe('Server', function () {
})
})
describe('GET /:resource?attr_like=', function () {
it('should respond with an array that matches the like operator (case insensitive)', function (done) {
describe('GET /:resource?attr_like=', () => {
it('should respond with an array that matches the like operator (case insensitive)', (done) => {
request(server)
.get('/tags?body_like=photo')
.expect('Content-Type', /json/)
@ -320,8 +331,8 @@ describe('Server', function () {
})
})
describe('GET /:parent/:parentId/:resource', function () {
it('should respond with json and corresponding nested resources', function (done) {
describe('GET /:parent/:parentId/:resource', () => {
it('should respond with json and corresponding nested resources', (done) => {
request(server)
.get('/posts/1/comments')
.expect('Content-Type', /json/)
@ -333,8 +344,8 @@ describe('Server', function () {
})
})
describe('GET /:resource/:id', function () {
it('should respond with json and corresponding resource', function (done) {
describe('GET /:resource/:id', () => {
it('should respond with json and corresponding resource', (done) => {
request(server)
.get('/posts/1')
.expect('Content-Type', /json/)
@ -342,7 +353,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should support string id, respond with json and corresponding resource', function (done) {
it('should support string id, respond with json and corresponding resource', (done) => {
request(server)
.get('/refs/abcd-1234')
.expect('Content-Type', /json/)
@ -350,7 +361,15 @@ describe('Server', function () {
.expect(200, done)
})
it('should respond with 404 if resource is not found', function (done) {
it('should support integer id as string', (done) => {
request(server)
.get('/stringIds/1234')
.expect('Content-Type', /json/)
.expect(db.stringIds[0])
.expect(200, done)
})
it('should respond with 404 if resource is not found', (done) => {
request(server)
.get('/posts/9001')
.expect('Content-Type', /json/)
@ -359,9 +378,9 @@ describe('Server', function () {
})
})
describe('GET /:resource?_embed=', function () {
it('should respond with corresponding resources and embedded resources', function (done) {
var posts = _.cloneDeep(db.posts)
describe('GET /:resource?_embed=', () => {
it('should respond with corresponding resources and embedded resources', (done) => {
const posts = _.cloneDeep(db.posts)
posts[0].comments = [ db.comments[0], db.comments[1] ]
posts[1].comments = [ db.comments[2], db.comments[3], db.comments[4] ]
request(server)
@ -372,9 +391,9 @@ describe('Server', function () {
})
})
describe('GET /:resource?_embed&_embed=', function () {
it('should respond with corresponding resources and embedded resources', function (done) {
var posts = _.cloneDeep(db.posts)
describe('GET /:resource?_embed&_embed=', () => {
it('should respond with corresponding resources and embedded resources', (done) => {
const posts = _.cloneDeep(db.posts)
posts[0].comments = [ db.comments[0], db.comments[1] ]
posts[0].refs = [ db.refs[0] ]
posts[1].comments = [ db.comments[2], db.comments[3], db.comments[4] ]
@ -387,34 +406,34 @@ describe('Server', function () {
})
})
describe('GET /:resource/:id?_embed=', function () {
it('should respond with corresponding resources and embedded resources', function (done) {
var posts = db.posts[0]
posts.comments = [db.comments[0], db.comments[1]]
describe('GET /:resource/:id?_embed=', () => {
it('should respond with corresponding resources and embedded resources', (done) => {
const post = _.cloneDeep(db.posts[0])
post.comments = [ db.comments[0], db.comments[1] ]
request(server)
.get('/posts/1?_embed=comments')
.expect('Content-Type', /json/)
.expect(posts)
.expect(post)
.expect(200, done)
})
})
describe('GET /:resource/:id?_embed=&_embed=', function () {
it('should respond with corresponding resource and embedded resources', function (done) {
var posts = db.posts[0]
posts.comments = [db.comments[0], db.comments[1]]
posts.refs = [db.refs[0]]
describe('GET /:resource/:id?_embed=&_embed=', () => {
it('should respond with corresponding resource and embedded resources', (done) => {
const post = _.cloneDeep(db.posts[0])
post.comments = [ db.comments[0], db.comments[1] ]
post.refs = [db.refs[0]]
request(server)
.get('/posts/1?_embed=comments&_embed=refs')
.expect('Content-Type', /json/)
.expect(posts)
.expect(post)
.expect(200, done)
})
})
describe('GET /:resource?_expand=', function () {
it('should respond with corresponding resource and expanded inner resources', function (done) {
var refs = _.cloneDeep(db.refs)
describe('GET /:resource?_expand=', () => {
it('should respond with corresponding resource and expanded inner resources', (done) => {
const refs = _.cloneDeep(db.refs)
refs[0].post = db.posts[0]
request(server)
.get('/refs?_expand=post')
@ -424,21 +443,21 @@ describe('Server', function () {
})
})
describe('GET /:resource/:id?_expand=', function () {
it('should respond with corresponding resource and expanded inner resources', function (done) {
var comments = db.comments[0]
comments.post = db.posts[0]
describe('GET /:resource/:id?_expand=', () => {
it('should respond with corresponding resource and expanded inner resources', (done) => {
const comment = _.cloneDeep(db.comments[0])
comment.post = db.posts[0]
request(server)
.get('/comments/1?_expand=post')
.expect('Content-Type', /json/)
.expect(comments)
.expect(comment)
.expect(200, done)
})
})
describe('GET /:resource?_expand=&_expand', function () {
it('should respond with corresponding resource and expanded inner resources', function (done) {
var refs = _.cloneDeep(db.refs)
describe('GET /:resource?_expand=&_expand', () => {
it('should respond with corresponding resource and expanded inner resources', (done) => {
const refs = _.cloneDeep(db.refs)
refs[0].post = db.posts[0]
refs[0].user = db.users[0]
request(server)
@ -449,9 +468,9 @@ describe('Server', function () {
})
})
describe('GET /:resource/:id?_expand=&_expand=', function () {
it('should respond with corresponding resource and expanded inner resources', function (done) {
var comments = db.comments[0]
describe('GET /:resource/:id?_expand=&_expand=', () => {
it('should respond with corresponding resource and expanded inner resources', (done) => {
const comments = db.comments[0]
comments.post = db.posts[0]
comments.user = db.users[0]
request(server)
@ -462,46 +481,49 @@ describe('Server', function () {
})
})
describe('POST /:resource', function () {
describe('POST /:resource', () => {
it('should respond with json, create a resource and increment id',
function (done) {
(done) => {
request(server)
.post('/posts')
.send({body: 'foo', booleanValue: 'true', integerValue: '1'})
.send({body: 'foo', booleanValue: true, integerValue: 1})
.expect('Content-Type', /json/)
.expect({id: 3, body: 'foo', booleanValue: true, integerValue: 1})
.expect(201)
.end(function (err, res) {
.end((err, res) => {
if (err) return done(err)
assert.equal(db.posts.length, 3)
done()
})
})
}
)
it('should support x-www-form-urlencoded',
function (done) {
(done) => {
request(server)
.post('/posts')
.type('form')
.send({body: 'foo'})
.send({body: 'foo', booleanValue: true, integerValue: 1})
.expect('Content-Type', /json/)
.expect({id: 3, body: 'foo'})
// x-www-form-urlencoded will convert to string
.expect({id: 3, body: 'foo', booleanValue: 'true', integerValue: '1'})
.expect(201)
.end(function (err, res) {
.end((err, res) => {
if (err) return done(err)
assert.equal(db.posts.length, 3)
done()
})
})
}
)
it('should respond with json, create a resource and generate string id',
function (done) {
(done) => {
request(server)
.post('/refs')
.send({url: 'http://foo.com', postId: '1'})
.expect('Content-Type', /json/)
.expect(201)
.end(function (err, res) {
.end((err, res) => {
if (err) return done(err)
assert.equal(db.refs.length, 2)
done()
@ -509,8 +531,8 @@ describe('Server', function () {
})
})
describe('POST /:parent/:parentId/:resource', function () {
it('should respond with json and set parentId', function (done) {
describe('POST /:parent/:parentId/:resource', () => {
it('should respond with json and set parentId', (done) => {
request(server)
.post('/posts/1/comments')
.send({body: 'foo'})
@ -520,17 +542,17 @@ describe('Server', function () {
})
})
describe('PUT /:resource/:id', function () {
it('should respond with json and replace resource', function (done) {
describe('PUT /:resource/:id', () => {
it('should respond with json and replace resource', (done) => {
var post = {id: 1, booleanValue: true, integerValue: 1}
request(server)
.put('/posts/1')
// body property omitted to test that the resource is replaced
.send({id: 1, booleanValue: 'true', integerValue: '1'})
.send(post)
.expect('Content-Type', /json/)
.expect(post)
.expect(200)
.end(function (err, res) {
.end((err, res) => {
if (err) return done(err)
// assert it was created in database too
assert.deepEqual(db.posts[0], post)
@ -538,25 +560,25 @@ describe('Server', function () {
})
})
it('should respond with 404 if resource is not found', function (done) {
it('should respond with 404 if resource is not found', (done) => {
request(server)
.put('/posts/9001')
.send({id: 1, body: 'bar', booleanValue: 'true', integerValue: '1'})
.send({id: 1, body: 'bar'})
.expect('Content-Type', /json/)
.expect({})
.expect(404, done)
})
})
describe('PATCH /:resource/:id', function () {
it('should respond with json and update resource', function (done) {
describe('PATCH /:resource/:id', () => {
it('should respond with json and update resource', (done) => {
request(server)
.patch('/posts/1')
.send({body: 'bar'})
.expect('Content-Type', /json/)
.expect({id: 1, body: 'bar'})
.expect(200)
.end(function (err, res) {
.end((err, res) => {
if (err) return done(err)
// assert it was created in database too
assert.deepEqual(db.posts[0], {id: 1, body: 'bar'})
@ -564,7 +586,7 @@ describe('Server', function () {
})
})
it('should respond with 404 if resource is not found', function (done) {
it('should respond with 404 if resource is not found', (done) => {
request(server)
.patch('/posts/9001')
.send({body: 'bar'})
@ -574,13 +596,13 @@ describe('Server', function () {
})
})
describe('DELETE /:resource/:id', function () {
it('should respond with empty data, destroy resource and dependent resources', function (done) {
describe('DELETE /:resource/:id', () => {
it('should respond with empty data, destroy resource and dependent resources', (done) => {
request(server)
.del('/posts/1')
.expect({})
.expect(200)
.end(function (err, res) {
.end((err, res) => {
if (err) return done(err)
assert.equal(db.posts.length, 1)
assert.equal(db.comments.length, 3)
@ -588,7 +610,7 @@ describe('Server', function () {
})
})
it('should respond with 404 if resource is not found', function (done) {
it('should respond with 404 if resource is not found', (done) => {
request(server)
.del('/posts/9001')
.expect('Content-Type', /json/)
@ -597,9 +619,9 @@ describe('Server', function () {
})
})
describe('Static routes', function () {
describe('GET /', function () {
it('should respond with html', function (done) {
describe('Static routes', () => {
describe('GET /', () => {
it('should respond with html', (done) => {
request(server)
.get('/')
.expect(/You're successfully running JSON Server/)
@ -607,8 +629,8 @@ describe('Server', function () {
})
})
describe('GET /stylesheets/style.css', function () {
it('should respond with css', function (done) {
describe('GET /stylesheets/style.css', () => {
it('should respond with css', (done) => {
request(server)
.get('/stylesheets/style.css')
.expect('Content-Type', /css/)
@ -617,14 +639,14 @@ describe('Server', function () {
})
})
describe('Database state', function () {
it('should be accessible', function () {
describe('Database state', () => {
it('should be accessible', () => {
assert(router.db.getState())
})
})
describe('Responses', function () {
it('should have no cache headers (for IE)', function (done) {
describe('Responses', () => {
it('should have no cache headers (for IE)', (done) => {
request(server)
.get('/db')
.expect('Cache-Control', 'no-cache')
@ -634,15 +656,15 @@ describe('Server', function () {
})
})
describe('Rewriter', function () {
it('should rewrite using prefix', function (done) {
describe('Rewriter', () => {
it('should rewrite using prefix', (done) => {
request(server)
.get('/api/posts/1')
.expect(db.posts[0])
.end(done)
})
it('should rewrite using params', function (done) {
it('should rewrite using params', (done) => {
request(server)
.get('/blog/posts/1/show')
.expect(db.posts[0])
@ -657,16 +679,16 @@ describe('Server', function () {
})
})
describe('router.render', function (done) {
beforeEach(function () {
router.render = function (req, res) {
describe('router.render', (done) => {
beforeEach(() => {
router.render = (req, res) => {
res.jsonp({
data: res.locals.data
})
}
})
it('should be possible to wrap response', function (done) {
it('should be possible to wrap response', (done) => {
request(server)
.get('/posts/1')
.expect('Content-Type', /json/)
@ -675,8 +697,8 @@ describe('Server', function () {
})
})
describe('router.db._.id', function (done) {
beforeEach(function () {
describe('router.db._.id', (done) => {
beforeEach(() => {
router.db.setState({
posts: [
{ _id: 1 }
@ -686,7 +708,7 @@ describe('Server', function () {
router.db._.id = '_id'
})
it('should be possible to GET using a different id property', function (done) {
it('should be possible to GET using a different id property', (done) => {
request(server)
.get('/posts/1')
.expect('Content-Type', /json/)
@ -694,7 +716,7 @@ describe('Server', function () {
.expect(200, done)
})
it('should be possible to POST using a different id property', function (done) {
it('should be possible to POST using a different id property', (done) => {
request(server)
.post('/posts')
.send({ body: 'hello' })

View File

@ -1,11 +1,10 @@
var request = require('supertest')
var jsonServer = require('../../src/server')
const request = require('supertest')
const jsonServer = require('../../src/server')
/* global beforeEach, describe, it */
describe('Server', function () {
var server
var router
var db
let server
let router
let db
beforeEach(function () {
db = {}
@ -32,7 +31,7 @@ describe('Server', function () {
describe('POST /:resource', function () {
it('should create resource', function (done) {
var user = { name: 'bar' }
const user = { name: 'bar' }
request(server)
.post('/user')
.send(user)
@ -43,7 +42,7 @@ describe('Server', function () {
describe('PUT /:resource', function () {
it('should update resource', function (done) {
var user = { name: 'bar' }
const user = { name: 'bar' }
request(server)
.put('/user')
.send(user)

View File

@ -1,30 +1,10 @@
var assert = require('assert')
var utils = require('../../src/server/utils')
/* global describe, it */
const assert = require('assert')
const utils = require('../../src/server/utils')
describe('utils', function () {
describe('toNative', function () {
it('should convert string to native type', function () {
// should convert
assert.strictEqual(utils.toNative('1'), 1)
assert.strictEqual(utils.toNative('0'), 0)
assert.strictEqual(utils.toNative('true'), true)
// should not convert
assert.strictEqual(utils.toNative(''), '')
assert.strictEqual(utils.toNative('\t\n'), '\t\n')
assert.strictEqual(utils.toNative('1 '), '1 ')
assert.strictEqual(utils.toNative('01'), '01')
assert.strictEqual(utils.toNative(' 1'), ' 1')
assert.strictEqual(utils.toNative('string'), 'string')
assert.strictEqual(utils.toNative(1), 1)
assert.strictEqual(utils.toNative(true), true)
})
})
describe('getPage', function () {
var array = [1, 2, 3, 4, 5]
var perPage = 2
const array = [1, 2, 3, 4, 5]
const perPage = 2
it('should return first page', function () {
assert.deepEqual(

View File

@ -0,0 +1,24 @@
const assert = require('assert')
const validateData = require('../../src/server/router/validate-data')
describe('validateData', () => {
it('should throw an error if data contains /', () => {
assert.throws(
() => validateData({ 'a/b': [] }),
/found \//
)
})
it('should throw an error if data is an array', () => {
assert.throws(
() => validateData([]),
/must be an object/
)
})
it('shouldn\'t throw an error', () => {
assert.doesNotThrow(
() => validateData({ a: [] })
)
})
})