From 2b26630ac6379fba77eb104b22e83b41a004b52e Mon Sep 17 00:00:00 2001 From: typicode Date: Sat, 12 Nov 2016 01:59:43 +0100 Subject: [PATCH] v0.9.0 (#404) * 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 --- .babelrc | 2 +- .gitignore | 1 + .npmignore | 1 + CHANGELOG.md | 12 + README.md | 31 ++- bin/index.js | 2 +- package.json | 42 ++-- src/cli/bin.js | 1 + src/cli/example.json | 9 + src/cli/index.js | 12 +- src/cli/run.js | 138 ++++++----- src/cli/utils/is.js | 16 +- src/cli/utils/load.js | 31 ++- src/server/body-parser.js | 6 + src/server/common.js | 9 - src/server/defaults.js | 43 ++-- src/server/index.js | 11 +- src/server/mixins.js | 51 ++-- src/server/rewriter.js | 20 +- src/server/router/index.js | 58 +++-- src/server/router/nested.js | 21 +- src/server/router/plural.js | 165 +++++++------ src/server/router/singular.js | 6 +- src/server/router/validate-data.js | 26 ++ src/server/utils.js | 24 +- test/cli/index.js | 247 ++++++++++--------- test/mocha.opts | 3 + test/server/mixins.js | 66 +++-- test/server/plural.js | 384 +++++++++++++++-------------- test/server/singular.js | 15 +- test/server/utils.js | 28 +-- test/server/validate-data.js | 24 ++ 32 files changed, 822 insertions(+), 683 deletions(-) create mode 100644 .npmignore create mode 100644 src/cli/bin.js create mode 100644 src/cli/example.json create mode 100644 src/server/body-parser.js delete mode 100644 src/server/common.js create mode 100644 src/server/router/validate-data.js create mode 100644 test/mocha.opts create mode 100644 test/server/validate-data.js diff --git a/.babelrc b/.babelrc index 3c078e9..45001da 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { "presets": [ - "es2015" + ["es2015", { "loose": true }] ] } diff --git a/.gitignore b/.gitignore index 835d515..0c475f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ **/*.log node_modules tmp +lib .DS_Store .idea diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e831038 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +src \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ad26c..1b4cbfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 28bcc58..37c7db9 100644 --- a/README.md +++ b/README.md @@ -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() diff --git a/bin/index.js b/bin/index.js index 6337655..af3551a 100755 --- a/bin/index.js +++ b/bin/index.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('../src/cli')() +require('../lib/cli/bin') diff --git a/package.json b/package.json index f0f2f89..9bab485 100644 --- a/package.json +++ b/package.json @@ -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 + } + } } diff --git a/src/cli/bin.js b/src/cli/bin.js new file mode 100644 index 0000000..2b8efef --- /dev/null +++ b/src/cli/bin.js @@ -0,0 +1 @@ +require('./')() diff --git a/src/cli/example.json b/src/cli/example.json new file mode 100644 index 0000000..4d3941a --- /dev/null +++ b/src/cli/example.json @@ -0,0 +1,9 @@ +{ + "posts": [ + { "id": 1, "title": "json-server", "author": "typicode" } + ], + "comments": [ + { "id": 1, "body": "some comment", "postId": 1 } + ], + "profile": { "name": "typicode" } +} \ No newline at end of file diff --git a/src/cli/index.js b/src/cli/index.js index 29c7f71..8f62da1 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -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] ') .options({ diff --git a/src/cli/run.js b/src/cli/run.js index 975fbce..648974c 100644 --- a/src/cli/run.js +++ b/src/cli/run.js @@ -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( - is.JSON(source) - ? source - : object - ) + let router - var defaultsOpts = { + try { + router = jsonServer.router( + is.JSON(source) + ? source + : object + ) + } catch (e) { + console.log() + console.error(chalk.red(e.message.replace(/^/gm, ' '))) + process.exit(1) + } + + 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,43 +176,45 @@ 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)) - // 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...')) - server && server.destroy() - start() - } - } else { - console.log(chalk.gray(' ' + file + ' has changed, reloading...')) + 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(` ${source} has changed, reloading...`)) server && server.destroy() start() } + } else { + console.log(chalk.gray(` ${source} has changed, reloading...`)) + server && server.destroy() + start() } - }) + } + }) // 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() - }) + } + }) } } }) diff --git a/src/cli/utils/is.js b/src/cli/utils/is.js index a59da21..2f5b8df 100644 --- a/src/cli/utils/is.js +++ b/src/cli/utils/is.js @@ -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) } diff --git a/src/cli/utils/load.js b/src/cli/utils/load.js index 28a1e94..e14a2a5 100644 --- a/src/cli/utils/load.js +++ b/src/cli/utils/load.js @@ -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) diff --git a/src/server/body-parser.js b/src/server/body-parser.js new file mode 100644 index 0000000..eeeda21 --- /dev/null +++ b/src/server/body-parser.js @@ -0,0 +1,6 @@ +const bodyParser = require('body-parser') + +module.exports = [ + bodyParser.json({limit: '10mb', extended: false}), + bodyParser.urlencoded({extended: false}) +] diff --git a/src/server/common.js b/src/server/common.js deleted file mode 100644 index 26577f0..0000000 --- a/src/server/common.js +++ /dev/null @@ -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() -] diff --git a/src/server/defaults.js b/src/server/defaults.js index 6cbd606..44265a3 100644 --- a/src/server/defaults.js +++ b/src/server/defaults.js @@ -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 } diff --git a/src/server/index.js b/src/server/index.js index 9a74caf..dd98979 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -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') } diff --git a/src/server/mixins.js b/src/server/mixins.js index 43dab7f..1f980df 100644 --- a/src/server/mixins.js +++ b/src/server/mixins.js @@ -1,28 +1,28 @@ -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}) + 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 } diff --git a/src/server/rewriter.js b/src/server/rewriter.js index 3549c32..718220f 100644 --- a/src/server/rewriter.js +++ b/src/server/rewriter.js @@ -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() diff --git a/src/server/router/index.js b/src/server/router/index.js index 481d00a..7a7ad6e 100644 --- a/src/server/router/index.js +++ b/src/server/router/index.js @@ -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) }) diff --git a/src/server/router/nested.js b/src/server/router/nested.js index a3edcc2..61a1a59 100644 --- a/src/server/router/nested.js +++ b/src/server/router/nested.js @@ -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() } diff --git a/src/server/router/plural.js b/src/server/router/plural.js index 2b7d266..24ecf85 100644 --- a/src/server/router/plural.js +++ b/src/server/router/plural.js @@ -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) { diff --git a/src/server/router/singular.js b/src/server/router/singular.js index c5c6291..f33bbda 100644 --- a/src/server/router/singular.js +++ b/src/server/router/singular.js @@ -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() diff --git a/src/server/router/validate-data.js b/src/server/router/validate-data.js new file mode 100644 index 0000000..5bb5dbc --- /dev/null +++ b/src/server/router/validate-data.js @@ -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.' + ) + } +} diff --git a/src/server/utils.js b/src/server/utils.js index 8f8307a..f7a5c9a 100644 --- a/src/server/utils.js +++ b/src/server/utils.js @@ -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) { diff --git a/test/cli/index.js b/test/cli/index.js index ce9657c..f822f6d 100644 --- a/test/cli/index.js +++ b/test/cli/index.js @@ -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(() => { + dbFile = tempWrite.sync( + JSON.stringify({ + posts: [ + { id: 1 }, + { _id: 2 } + ] + }), + 'db.json' + ) - beforeEach(function () { - dbFile = tempWrite.sync(JSON.stringify({ - posts: [ - { id: 1 }, - { _id: 2 } - ] - }), '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) { - child = cli([dbFile]) + 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) { - child = cli(['fixtures/seed.js']) + 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 () { - child = cli(['http://localhost:8080/db']) + 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) { - child = cli([dbFile, '-r', routesFile, '-m', middlewareFiles.en, '-i', '_id', '--read-only']) + 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) { - child = cli([dbFile, '-m', middlewareFiles.en, middlewareFiles.jp]) + 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) { - child = cli([dbFile, '-d', 1000]) + 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 () { + child = cli([ dbFile, '-s', publicDir, '-S', snapshotsDir ]) + 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) { - child = cli(['fixtures/seed.js', '--no-cors=true']) + 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) { - child = cli(['fixtures/seed.js', '--no-gzip=true']) + 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')) + }) }) }) }) diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..82a4ddb --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,3 @@ +--compilers js:babel-register +--reporter spec +--timeout 5000 \ No newline at end of file diff --git a/test/server/mixins.js b/test/server/mixins.js index 3952300..8707fd0 100644 --- a/test/server/mixins.js +++ b/test/server/mixins.js @@ -1,34 +1,52 @@ -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 = { - posts: [ - {id: 1, comment: 1} - ], - comments: [ - {id: 1, postId: 1}, - // Comments below references a post that doesn't exist - {id: 2, postId: 2}, - {id: 3, postId: 2} - ] - } + before(() => { + _.mixin(_db) + _.mixin(mixins) + }) - var expected = [ - {name: 'comments', id: 2}, - {name: 'comments', id: 3} + beforeEach(() => { + db = { + posts: [ + { id: 1, comment: 1 } + ], + comments: [ + { id: 1, postId: 1 }, + // Comments below references a post that doesn't exist + { id: 2, postId: 2 }, + { id: 3, postId: 2 } + ], + photos: [ + { id: '1' }, + { id: '2' } ] + } + }) - _.mixin(_db) - _.mixin(mixins) + describe('getRemovable', () => { + it('should return removable documents', () => { + const expected = [ + { name: 'comments', id: 2 }, + { name: 'comments', id: 3 } + ] 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) + }) + }) }) diff --git a/test/server/plural.js b/test/server/plural.js index 7bddd09..1cd4774 100644 --- a/test/server/plural.js +++ b/test/server/plural.js @@ -1,43 +1,46 @@ -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 = [ - {id: 1, body: 'foo'}, - {id: 2, body: 'bar'} + { id: 1, body: 'foo' }, + { id: 2, body: 'bar' } ] db.tags = [ - {id: 1, body: 'Technology'}, - {id: 2, body: 'Photography'}, - {id: 3, body: 'photo'} + { id: 1, body: 'Technology' }, + { id: 2, body: 'Photography' }, + { id: 3, body: 'photo' } ] db.users = [ - {id: 1, username: 'Jim'}, - {id: 2, username: 'George'} + { id: 1, username: 'Jim', tel: '0123' }, + { id: 2, username: 'George', tel: '123' } ] db.comments = [ - {id: 1, body: 'foo', published: true, postId: 1, userId: 1}, - {id: 2, body: 'bar', published: false, postId: 1, userId: 2}, - {id: 3, body: 'baz', published: false, postId: 2, userId: 1}, - {id: 4, body: 'qux', published: true, postId: 2, userId: 2}, - {id: 5, body: 'quux', published: false, postId: 2, userId: 1} + { id: 1, body: 'foo', published: true, postId: 1, userId: 1 }, + { id: 2, body: 'bar', published: false, postId: 1, userId: 2 }, + { id: 3, body: 'baz', published: false, postId: 2, userId: 1 }, + { id: 4, body: 'qux', published: true, postId: 2, userId: 2 }, + { id: 5, body: 'quux', published: false, postId: 2, userId: 1 } ] db.refs = [ - {id: 'abcd-1234', url: 'http://example.com', postId: 1, userId: 1} + { id: 'abcd-1234', url: 'http://example.com', postId: 1, userId: 1 } + ] + + db.stringIds = [ + { id: '1234' } ] db.deep = [ @@ -46,27 +49,27 @@ describe('Server', function () { ] db.nested = [ - {resource: {name: 'dewey'}}, - {resource: {name: 'cheatem'}}, - {resource: {name: 'howe'}} + { resource: {name: 'dewey'} }, + { resource: {name: 'cheatem'} }, + { resource: {name: 'howe'} } ] db.list = [ - {id: 1}, - {id: 2}, - {id: 3}, - {id: 4}, - {id: 5}, - {id: 6}, - {id: 7}, - {id: 8}, - {id: 9}, - {id: 10}, - {id: 11}, - {id: 12}, - {id: 13}, - {id: 14}, - {id: 15} + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + { id: 6 }, + { id: 7 }, + { id: 8 }, + { id: 9 }, + { id: 10 }, + { id: 11 }, + { id: 12 }, + { id: 13 }, + { id: 14 }, + { id: 15 } ] server = jsonServer.create() @@ -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,39 +105,47 @@ 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/) - .expect([db.comments[0]]) + .expect([ db.comments[0] ]) .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/) - .expect([db.comments[0], db.comments[1]]) + .expect([ db.comments[0], db.comments[1] ]) .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/) - .expect([db.deep[0]]) + .expect([ db.deep[0] ]) .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,16 +162,16 @@ 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/) - .expect([db.tags[1], db.tags[2]]) + .expect([ db.tags[1], db.tags[2] ]) .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,25 +179,25 @@ 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/) - .expect([]) + .expect([ ]) .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/) - .expect([db.comments[3]]) + .expect([ db.comments[3] ]) .expect(200, done) }) }) - 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,24 +208,24 @@ 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/) - .expect([db.tags[1], db.tags[0], db.tags[2]]) + .expect([ db.tags[1], db.tags[0], db.tags[2] ]) .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/) - .expect([db.tags[2], db.tags[0], db.tags[1]]) + .expect([ db.tags[2], db.tags[0], db.tags[1] ]) .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,41 +233,41 @@ 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/) - .expect([db.nested[1], db.nested[0], db.nested[2]]) + .expect([ db.nested[1], db.nested[0], db.nested[2] ]) .expect(200, done) }) }) - 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 = [ '; rel="first"', '; rel="prev"', '; 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,11 +378,11 @@ describe('Server', function () { }) }) - describe('GET /:resource?_embed=', function () { - it('should respond with corresponding resources and embedded resources', function (done) { - var 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]] + 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) .get('/posts?_embed=comments') .expect('Content-Type', /json/) @@ -372,12 +391,12 @@ 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) - 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]] + 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] ] posts[1].refs = [] request(server) .get('/posts?_embed=comments&_embed=refs') @@ -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,12 +716,12 @@ 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' }) .expect('Content-Type', /json/) - .expect({_id: 2, body: 'hello'}) + .expect({ _id: 2, body: 'hello' }) .expect(201, done) }) }) diff --git a/test/server/singular.js b/test/server/singular.js index 617bc37..509a478 100644 --- a/test/server/singular.js +++ b/test/server/singular.js @@ -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) diff --git a/test/server/utils.js b/test/server/utils.js index 4036f58..a611db2 100644 --- a/test/server/utils.js +++ b/test/server/utils.js @@ -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( diff --git a/test/server/validate-data.js b/test/server/validate-data.js new file mode 100644 index 0000000..8087215 --- /dev/null +++ b/test/server/validate-data.js @@ -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: [] }) + ) + }) +})