diff --git a/.travis.yml b/.travis.yml index df63076..18ae2d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: node_js node_js: + - "0.11" - "0.10" - - "0.8" diff --git a/README.md b/README.md index 0faddb2..5719e7c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -[![Build Status](https://travis-ci.org/typicode/json-server.png)](https://travis-ci.org/typicode/json-server) -[![NPM version](https://badge.fury.io/js/json-server.png)](http://badge.fury.io/js/json-server) - -![](http://i.imgur.com/dLeJmw6.png) +

+ +

# JSON Server @@ -9,36 +8,43 @@ Give it a JSON or JS seed file and it will serve it through REST routes. Created with :heart: for front-end developers who need a flexible back-end for quick prototyping and mocking. +[![Build Status](https://travis-ci.org/typicode/json-server.png)](https://travis-ci.org/typicode/json-server) +[![NPM version](https://badge.fury.io/js/json-server.png)](http://badge.fury.io/js/json-server) + ## Examples ### Command line interface -```bash -$ cat db.json +```javascript +// db.json { "posts": [ { "id": 1, "body": "foo" } ] } +``` + +```bash $ json-server --file db.json $ curl -i http://localhost:3000/posts/1 ``` -You can type 's' at any moment to save the current live JSON object to timestamped -file. - ### Node module ```javascript var server = require('json-server'); -var db = { +server.low.db = { posts: [ { id: 1, body: 'foo' } ] } -server.run(db); +server.get('/another/route', function(req, res, next) { + // ... +}) + +server.listen(3000); ``` You can find a running demo here: http://jsonplaceholder.typicode.com. @@ -58,38 +64,25 @@ You can find a running demo here: http://jsonplaceholder.typicode.com. $ npm install -g json-server ``` - ## Usage ### Command line interface ```bash -json-server --help - Usage: json-server [options] + Usage: json-server [options] Options: - -h, --help output usage information - -V, --version output the version number - -f --file load db from a js or json file - -u --url load db from a URL - -p --port [port] server port - --read-only read only mode -``` + --version output version + --port set port -JSON Server can load JSON from multiple sources: + Exemples: -```bash -$ json-server --file db.json -$ json-server --file seed.js -$ json-server --url http://example.com/db.json -``` - -And be run in read-only mode (useful if deployed on a public server): - -```bash -$ json-server --file db.json --read-only + json-server db.json + json-server seed.js + json-server http://example.com/db.json + ``` #### Input @@ -129,7 +122,6 @@ JSON Server expects JS files to export a ```run``` method that returns an object Seed files are useful if you need to programmaticaly create a lot of data. - ### Node module #### run(db, [options]) @@ -149,7 +141,7 @@ By default, ```port``` is set to 3000 and ```readOnly``` to false. ``` GET /:resource -GET /:resource?attr=&attr=& +GET /:resource?filter=&filter=& GET /:parent/:parentId/:resource GET /:resource/:id POST /:resource @@ -158,6 +150,8 @@ PATCH /:resource/:id DEL /:resource/:id ``` +To slice resources, add `_start` and `_end` query parameters to route. + For routes usage information, have a look at [JSONPlaceholder](https://github.com/typicode/jsonplaceholder) code examples. ``` @@ -166,27 +160,12 @@ GET /db Returns database state. - ``` GET / ``` Returns default index file or content of ./public/index.html (useful if you need to set a custom home page). - -## Support - -If you like the project, please tell your friends about it, star it or give feedback :) It's very much appreciated! - -For project updates or to get in touch, [@typicode](http://twitter.com/typicode). You can also send me a mail. - -## Test - -```bash -$ npm install -$ npm test -``` - ## Articles -[Fast prototyping using Restangular and Json-server](http://bahmutov.calepin.co/fast-prototyping-using-restangular-and-json-server.html) +* [Fast prototyping using Restangular and Json-server](http://bahmutov.calepin.co/fast-prototyping-using-restangular-and-json-server.html) diff --git a/bin/cli.js b/bin/cli.js deleted file mode 100755 index 8089406..0000000 --- a/bin/cli.js +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env node -var program = require('commander'), - request = require('superagent'), - server = require('../server'), - logger = require('../utils/logger'), - moment = require('moment'), - fs = require('fs'), - pkg = require('../package.json'), - options = {}; - -function loadFile(file, cb) { - var path = process.cwd() + '/' + file, - db; - - if (/\.json$/.test(file)) db = require(path); - if (/\.js$/.test(file)) db = require(path).run(); - - cb(db); -} - -function loadURL(url, cb) { - logger.info('Fetching ' + url + '...') - request - .get(url) - .end(function(error, res) { - if (error) { - logger.error(error); - } else { - cb(JSON.parse(res.text)); - } - }); -} - -function saveDbOnCommand(app) { - console.assert(app, 'expected app object'); - - process.stdin.resume(); - process.stdin.setEncoding('utf8'); - logger.notice('To save live database at any moment, enter `s`'); - - process.stdin.on('data', function (userInput) { - if (userInput.trim().toLowerCase() == 's') { - var liveDB = app.db(); - var now = moment().format('YYYY-MM-DD-HH:mm:ss') - var filename = 'json-server.' + now + '.json'; - console.assert(liveDB, 'expected live db object'); - fs.writeFileSync(filename, - JSON.stringify(liveDB, null, 2), - 'utf-8'); - console.log('saved db to', filename); - } - }); -} - -function onDatabaseLoaded(db) { - var app = server.run(db, options); - saveDbOnCommand(app); - return app; -} - -program - .version(pkg.version) - .option('-f --file ', 'load db from a js or json file') - .option('-u --url ', 'load db from a URL') - .option('-p --port [port]', 'server port') - .option('--read-only', 'read only mode'); - -program.on('--help', function () { - var examples = - ' Examples:\n\n' + - ' json-server --file db.json\n' + - ' json-server --file seed.js\n' + - ' json-server --url http://example.com/db.json\n' - console.log(examples); -}); - -program.parse(process.argv); - -if (program.port) options.port = program.port; -if (program.readOnly) options.readOnly = true; -if (program.file) loadFile(program.file, onDatabaseLoaded); -if (program.url) loadURL(program.url, onDatabaseLoaded); diff --git a/bin/index.js b/bin/index.js new file mode 100644 index 0000000..b4fd9a8 --- /dev/null +++ b/bin/index.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node +var minimist = require('minimist') +var updateNotifier = require('update-notifier') +var cli = require('../src/cli') + +var notifier = updateNotifier({packagePath: '../package'}) +if (notifier.update) notifier.notify() + +var argv = minimist(process.argv.slice(2)) + +cli.run(argv) \ No newline at end of file diff --git a/db.json b/db.json new file mode 100644 index 0000000..3641986 --- /dev/null +++ b/db.json @@ -0,0 +1,20 @@ +{ + "posts": [ + { + "id": 2, + "body": "bar" + } + ], + "comments": [ + { + "id": 1, + "published": true, + "postId": 1 + }, + { + "id": 2, + "published": false, + "postId": 1 + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 06f7d5b..4866fbd 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,23 @@ { "name": "json-server", - "version": "0.2.0", + "version": "0.3.0", "description": "Serves JSON files through REST routes.", - "main": "server.js", - "bin": "./bin/cli.js", + "main": "./src/server.js", + "bin": "./bin/index.js", "directories": { "test": "test" }, "dependencies": { - "commander": "~2.0.0", "cors": "~2.1.0", - "express": "~3.4.4", - "logan": "~0.0.2", + "express": "^3.4.8", "superagent": "~0.15.7", "underscore": "~1.5.2", "underscore.inflections": "~0.2.1", - "moment": "~2.4.0" + "lowdb": "^0.1.0", + "yargs": "^1.2.1", + "chalk": "^0.4.0", + "minimist": "0.0.8", + "update-notifier": "^0.1.8" }, "devDependencies": { "supertest": "~0.8.1", @@ -42,7 +44,8 @@ "testing", "rest", "data", - "dummy" + "dummy", + "sandbox" ], "author": "Typicode ", "license": "MIT", diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..bfdd40d Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/images/json.png b/public/images/json.png new file mode 100644 index 0000000..5717659 Binary files /dev/null and b/public/images/json.png differ diff --git a/public/images/logo.png b/public/images/logo.png deleted file mode 100644 index 8fd3cc1..0000000 Binary files a/public/images/logo.png and /dev/null differ diff --git a/public/index.html b/public/index.html index 8f8b172..025f97f 100644 --- a/public/index.html +++ b/public/index.html @@ -1,28 +1,37 @@ JSON Server - +
- - -

- Congrats! You're successfully running JSON Server. +

+


- -

Resources

+ +

+ + Congrats! You're successfully running JSON Server. + +

+ +
+ +

Routes

+

+ Here are the resources that JSON Server has loaded: +

    loading, please wait...

- To view database current state: + You can view database current state at any time:

  • db @@ -30,26 +39,19 @@

-

Requests

- Resources can be accessed in various ways. -

-

- JSON Server supports: -

    -
  • GET, POST, PUT, PATCH, DESTROY and OPTIONS verbs.
  • -
  • JSONP or CORS cross domain requests.
  • -
+ You can use GET, POST, PUT... and access your resources from anywhere + using CORS and JSONP.

-

Documentation

+

Documentation

View README on GitHub.

-

Issues

+

Issues

Please go here.

diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index 1464e6c..e46e5a1 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -1,43 +1,12 @@ -body { - padding: 50px; - font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +img { + padding-top: 50px; + padding-bottom: 20px; } -h1 { - font-weight: lighter; +li { + list-style-type: square; } -a { - /*color: #00B7FF;*/ - color: #3D72D1; -} - -hr { - border: 1px #EEE solid; -} - -a.logo { - display: block; - text-align: center; - margin-bottom: 50px; -} - -.alert-success { - font-size: 18px; -} - -/*.logo { - background: #00B7FF; - color: white; - font-size: 48px; - width: 100px; - height: 100px; - text-align: center; - vertical-align: middle; - line-height: 90px; - border-radius: 100px; - transform:rotate(90deg); - -ms-transform:rotate(90deg); - -webkit-transform:rotate(90deg); - margin-bottom: 50px; -}*/ \ No newline at end of file +h4 { + padding-top: 20px; +} \ No newline at end of file diff --git a/routes/read-only.js b/routes/read-only.js deleted file mode 100644 index da0b2f7..0000000 --- a/routes/read-only.js +++ /dev/null @@ -1,71 +0,0 @@ -var _ = require('underscore'), - utils = require('../utils/utils'), - db = {}; - -_.mixin(require('../utils/db-mixins')); - -exports.setDatabase = function(object) { - db = object; -} - -exports.database = function(req, res) { - res.jsonp(db) -} - -// GET /:resource?attr=&attr= -exports.list = function(req, res) { - var collection = db[req.params.resource], - properties = {}, - result; - - Object.keys(req.query).forEach(function (key) { - var value = req.query[key]; - properties[key] = utils.toNative(value); - }); - - if (_(properties).isEmpty()) { - result = collection; - } else { - result = _(collection).where(properties); - } - - res.jsonp(result); -} - -// GET /:parent/:parentId/:resource -exports.nestedList = function(req, res) { - var properties = {}, - resource; - - // Set parentID - properties[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId; - - // Filter using parentID - resource = _.where(db[req.params.resource], properties); - - res.jsonp(resource); -} - -// GET /:resource/:id -exports.show = function(req, res) { - var resource = _.get(db, req.params.resource, +req.params.id); - - res.jsonp(resource); -} - -exports.create = function(req, res) { - req.body.id = Math.round(new Date().getTime() / 1000); - res.jsonp(req.body); -} - -exports.update = function(req, res) { - var resource = _.get(db, req.params.resource, +req.params.id), - clonedResource = _.clone(resource), - result = _.extend(clonedResource, req.body); - - res.jsonp(result); -} - -exports.destroy = function(req, res) { - res.send(204) -} diff --git a/routes/read-write.js b/routes/read-write.js deleted file mode 100644 index 61f89a8..0000000 --- a/routes/read-write.js +++ /dev/null @@ -1,83 +0,0 @@ -var _ = require('underscore'), - utils = require('../utils/utils'), - db = {}; - -_.mixin(require('../utils/db-mixins')); - -exports.setDatabase = function(object) { - db = object; -} - -exports.database = function(req, res) { - res.jsonp(db) -} - -exports.db = function() { - return db; -} - -// GET /:resource?attr=&attr= -exports.list = function(req, res) { - var collection = db[req.params.resource], - properties = {}, - result; - - Object.keys(req.query).forEach(function (key) { - var value = req.query[key]; - properties[key] = utils.toNative(value); - }); - - if (_(properties).isEmpty()) { - result = collection; - } else { - result = _(collection).where(properties); - } - - res.jsonp(result); -} - -// GET /:parent/:parentId/:resource -exports.nestedList = function(req, res) { - var properties = {}, - resource; - - // Set parentID - properties[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId; - - // Filter using parentID - resource = _.where(db[req.params.resource], properties); - - res.jsonp(resource); -} - -// GET /:resource/:id -exports.show = function(req, res) { - var resource = _.get(db, req.params.resource, +req.params.id); - - res.jsonp(resource); -} - -// POST /:resource -exports.create = function(req, res) { - var resource = _.create(db, req.params.resource, req.body); - - res.jsonp(resource); -} - -// PUT /:resource/:id -// PATCH /:resource/:id -exports.update = function(req, res) { - _.update(db, req.params.resource, +req.params.id, req.body); - var resource = _.get(db, req.params.resource, +req.params.id); - - res.jsonp(resource); -} - -// DELETE /:resource/:id -exports.destroy = function(req, res) { - _.remove(db, req.params.resource, +req.params.id); - _.clean(db); - - res.send(204); -} - diff --git a/server.js b/server.js deleted file mode 100644 index f1dc744..0000000 --- a/server.js +++ /dev/null @@ -1,96 +0,0 @@ -var express = require('express'), - cors = require('cors'), - http = require('http'), - path = require('path'), - fs = require('fs'), - _ = require('underscore'), - logger = require('./utils/logger'); - - -var defaultOptions = { - port: process.env.PORT || 3000, - readOnly: false -} - -function createApp(db, options) { - // Create app - var app = express(), - options = options || {}, - routes; - - // Configure all environments - app.use(express.favicon()); - app.use(express.logger('dev')); - app.use(express.json()); - app.use(express.urlencoded()); - app.use(express.methodOverride()); - - - // Configure development - if ('development' == app.get('env')) { - app.use(express.errorHandler()); - } - - // Configure using options provided - app.set('port', options.port); - routes = options.readOnly ? './routes/read-only' : './routes/read-write'; - routes = require(routes); - - // Use default or user public directory - // Note: should be done before CORS and app.router setting - if (fs.existsSync(process.cwd() + '/public')) { - app.use(express.static(process.cwd() + '/public')); - } else { - app.use(express.static(path.join(__dirname, './public'))); - } - - // Enable CORS for everything - app.use(cors()); - app.options('*', cors()); - - // Set app.router - app.use(app.router); - - // Set API entry points - app.get('/db', routes.database) - app.get('/:resource', routes.list); - app.get('/:parent/:parentId/:resource', routes.nestedList); - app.get('/:resource/:id', routes.show); - app.post('/:resource', routes.create); - app.put('/:resource/:id', routes.update); - app.patch('/:resource/:id', routes.update); - app.del('/:resource/:id', routes.destroy); - - // Set database - routes.setDatabase(db); - app.db = routes.db; - - // And done! Ready to serve JSON! - return app; -} - -function run(db, options) { - options = _.defaults(options || {}, defaultOptions); - - var app = createApp(db, options); - - if (_.isEmpty(db)) { - logger.error('No resources found!'); - } else { - logger.success('Available resources'); - for (var prop in db) { - logger.url(options.port, prop); - } - } - - http - .createServer(app) - .listen((options.port), function(){ - logger.success('Express server listening on port ' + options.port); - logger.success('Congrats! Open http://localhost:' + options.port); - }); - return app; -} - -exports.createApp = createApp; -exports.run = run; diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..b090fff --- /dev/null +++ b/src/cli.js @@ -0,0 +1,87 @@ +var fs = require('fs') +var chalk = require('chalk') +var minimist = require('minimist') +var request = require('superagent') +var low = require('low') +var server = require('./server') + +// Output version +function version() { + var pkg = require('../package.json') + console.log(pkg.version) +} + +// Output help.txt with some colors +function help() { + var txt = fs.readFileSync(__dirname + '/help.txt').toString() + txt = txt.replace(/json-server/g, chalk.green('json-server')) + console.log(txt) +} + +// Start server +function start(port) { + for (var prop in low.db) { + console.log('http://localhost:' + port + '/' + chalk.green(prop)) + } + + console.log( + '\nEnter ' + chalk.green('`s`') + ' at any time to create a snapshot of the db\n' + ) + process.stdin.resume() + process.stdin.setEncoding('utf8') + process.stdin.on('data', function (chunk) { + if (chunk.trim().toLowerCase() === 's') { + var file = 'db-' + Date.now() + '.json' + low.save(file) + console.log('\nSaved snapshot to ' + chalk.green(file) + '\n') + } + }) + + server.listen(port) +} + +// Load source +function load(source, port) { + console.log(chalk.green('\n{^ ^} Heya!\n')) + + console.log('Loading database from ' + source + '\n') + + if (/\.json$/.test(source)) { + var path = process.cwd() + '/' + source + low.path = path + low.db = require(path); + start(port) + } + + if (/\.js$/.test(source)) { + var path = process.cwd() + '/' + source + low.db = require(path).run(); + start(port) + } + + if (/^http/.test(source)) { + request + .get(source) + .end(function(err, res) { + if (err) { + console.error(err) + } else { + low.db = JSON.parse(res.text) + start(port) + } + }) + } +} + +// Uses minimist parsed argv +function run(argv) { + var source = argv._[0] + var port = argv.port || 3000 + + if (argv.version) return version() + if (source) return load(source, port) + + help() +} + +module.exports.run = run diff --git a/src/help.txt b/src/help.txt new file mode 100644 index 0000000..06fb12b --- /dev/null +++ b/src/help.txt @@ -0,0 +1,14 @@ + + Usage: json-server [options] + + Options: + + --version output version + --port set port + + Exemples: + + json-server db.json + json-server seed.js + json-server http://example.com/db.json + \ No newline at end of file diff --git a/src/routes.js b/src/routes.js new file mode 100644 index 0000000..3546ba2 --- /dev/null +++ b/src/routes.js @@ -0,0 +1,81 @@ +var _ = require('underscore') +var low = require('low') +var utils = require('./utils') + +var routes = {} + +// GET /db +routes.db = function(req, res, next) { + res.jsonp(low.db) +} + +// GET /:resource?attr=&attr= +// GET /:parent/:parentId/:resource +routes.list = function(req, res, next) { + var props = {} + var resource + + var _start = req.query._start + var _end = req.query._end + + delete req.query._start + delete req.query._end + + if (req.params.parent) { + props[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId + } + + for (var key in req.query) { + props[key] = utils.toNative(req.query[key]) + } + + if (_(props).isEmpty()) { + resource = low(req.params.resource).value() + } else { + resource = low(req.params.resource).where(props).value() + } + + if (_start) { + resource = resource.slice(_start, _end) + } + + res.jsonp(resource) +} + +// GET /:resource/:id +routes.show = function(req, res, next) { + var resource = low(req.params.resource) + .get(+req.params.id) + .value() + + res.jsonp(resource) +} + +// POST /:resource +routes.create = function(req, res, next) { + var resource = low(req.params.resource) + .insert(req.body) + .value() + + res.jsonp(resource) +} + +// PUT /:resource/:id +// PATCH /:resource/:id +routes.update = function(req, res, next) { + var resource = low(req.params.resource) + .update(+req.params.id, req.body) + .value() + + res.jsonp(resource) +} + +// DELETE /:resource/:id +routes.destroy = function(req, res, next) { + low(req.params.resource).remove(+req.params.id) + utils.clean() + + res.send(204) +} + +module.exports = routes \ No newline at end of file diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..c8ec484 --- /dev/null +++ b/src/server.js @@ -0,0 +1,47 @@ +var fs = require('fs') +var express = require('express') +var cors = require('cors') +var http = require('http') +var path = require('path') +var low = require('low') +var utils = require('./utils') +var routes = require('./routes') + +low._.createId = utils.createId + +var server = express() + +server.set('port', process.env.PORT || 3000) +server.use(express.logger('dev')) +server.use(express.json()) +server.use(express.urlencoded()) +server.use(express.methodOverride()) + +if (fs.existsSync(process.cwd() + '/public')) { + server.use(express.static(process.cwd() + '/public')); +} else { + server.use(express.static(path.join(__dirname, './public'))); +} + +server.use(cors()) +server.use(server.router) + +if ('development' == server.get('env')) { + server.use(express.errorHandler()); +} + +server.get( '/db' , routes.db) +server.get( '/:resource' , routes.list) +server.get( '/:parent/:parentId/:resource' , routes.list) +server.get( '/:resource/:id' , routes.show) + +server.post( '/:resource' , routes.create) + +server.put( '/:resource/:id' , routes.update) +server.patch('/:resource/:id' , routes.update) + +server.del( '/:resource/:id' , routes.destroy) + +server.low = low + +module.exports = server \ No newline at end of file diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..45bc2aa --- /dev/null +++ b/src/utils.js @@ -0,0 +1,60 @@ +var low = require('low') +var _ = require('underscore') +_.mixin(require('underscore.inflections')) + + +// Turns string to native. +// Example: +// 'true' -> true +// '1' -> 1 +function toNative(value) { + if (value === 'true' || value === 'false') { + return value === 'true' + } else if (!isNaN(+value)) { + return +value + } else { + return value + } +} + +// Creates incremental id. +function createId(coll) { + if (_.isEmpty(coll)) { + return 1 + } else { + return _.max(coll, function(doc) { + return doc.id + }).id + 1 + } +} + +// Removes empty relations +function clean() { + var toBeRemoved = [] + + _(low.db).each(function(coll, collName) { + _(coll).each(function(doc) { + _(doc).each(function(value, key) { + if (/Id$/.test(key)) { + var reference = _.pluralize(key.slice(0, - 2)) + if (!_.isUndefined(low(reference).get(doc[key]).value())) { + toBeRemoved.push({ + collName: collName, + id: doc.id + }) + } + } + }) + }) + }) + + _(toBeRemoved).each(function(item) { + low(item.collName).remove(item.id); + }) +} + +module.exports = { + toNative: toNative, + createId: createId, + clean: clean +} \ No newline at end of file diff --git a/test/fixture.js b/test/fixture.js deleted file mode 100644 index 453be2c..0000000 --- a/test/fixture.js +++ /dev/null @@ -1,18 +0,0 @@ -// Small database to be used during tests -module.exports = function() { - var db = {}; - - db.posts = [ - {id: 1, body: 'foo'}, - {id: 2, body: 'bar'} - ] - - db.comments = [ - {id: 1, published: true, postId: 1}, - {id: 2, published: false, postId: 1}, - {id: 3, published: false, postId: 2}, - {id: 4, published: false, postId: 2}, - ] - - return db; -} \ No newline at end of file diff --git a/test/read-only.js b/test/read-only.js deleted file mode 100644 index aca0e33..0000000 --- a/test/read-only.js +++ /dev/null @@ -1,138 +0,0 @@ -var request = require('supertest'), - assert = require('assert'), - server = require('../server'), - fixture = require('./fixture'), - db, - app; - -describe('Read only routes', function() { - - beforeEach(function() { - db = fixture(); - app = server.createApp(db, { readOnly: true }); - }); - - describe('GET /:resource', function() { - it('should respond with json and resources and corresponding resources', function(done) { - request(app) - .get('/posts') - .expect('Content-Type', /json/) - .expect(db.posts) - .expect(200, done); - }); - }); - - describe('GET /:resource?attr=&attr=', function() { - it('should respond with json and filter resources', function(done) { - request(app) - .get('/comments?postId=1&published=true') - .expect('Content-Type', /json/) - .expect([db.comments[0]]) - .expect(200, done); - }); - }); - - describe('GET /:parent/:parentId/:resource', function() { - it('should respond with json and corresponding nested resources', function(done) { - request(app) - .get('/posts/1/comments') - .expect('Content-Type', /json/) - .expect([db.comments[0], db.comments[1]]) - .expect(200, done); - }); - }); - - - describe('GET /:resource/:id', function() { - it('should respond with json and corresponding resource', function(done) { - request(app) - .get('/posts/1') - .expect('Content-Type', /json/) - .expect(db.posts[0]) - .expect(200, done); - }); - }); - - describe('GET /db', function() { - it('should respond with json and full database', function(done) { - request(app) - .get('/db') - .expect('Content-Type', /json/) - .expect(db) - .expect(200, done); - }); - }); - - describe('POST /:resource', function() { - it('should respond with fake json and not create a resource', function(done) { - request(app) - .post('/posts') - .send({body: '...'}) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res){ - if (err) return done(err); - assert(res.body.hasOwnProperty('id')); - assert.equal(res.body.body, '...'); - assert.equal(db.posts.length, 2); - done() - }); - }); - }); - - describe('PUT /:resource/:id', function() { - it('should respond with fake json and not update resource', function(done) { - request(app) - .put('/posts/1') - .send({id: 999, body: '...'}) - .expect('Content-Type', /json/) - .expect({id: 999, body: '...'}) - .expect(200) - .end(function(err, res){ - if (err) return done(err); - // Checking that first post wasn't updated - assert.deepEqual(db.posts[0], {id: 1, body: 'foo'}); - done() - }); - }); - }); - - describe('PATCH /:resource/:id', function() { - it('should respond with fake json and not update resource', function(done) { - request(app) - .patch('/posts/1') - .send({body: '...'}) - .expect('Content-Type', /json/) - .expect({id: 1, body: '...'}) - .expect(200) - .end(function(err, res){ - if (err) return done(err); - // Checking that first post wasn't updated - assert.deepEqual(db.posts[0], {id: 1, body: 'foo'}); - done() - }); - }); - }); - - describe('DELETE /:resource/:id', function() { - it('should respond with empty data and not destroy resource', function(done) { - request(app) - .del('/posts/1') - .expect(204) - .end(function(err, res){ - if (err) return done(err); - assert.equal(db.posts.length, 2); - assert.equal(db.comments.length, 4); - done() - }); - }); - }); - - describe('OPTIONS /:resource/:id', function() { - it('should respond with empty data and not destroy resource', function(done) { - request(app) - .options('/posts/1') - .expect(204, done); - }); - }); -}); \ No newline at end of file diff --git a/test/read-write.js b/test/read-write.js deleted file mode 100644 index 388d4da..0000000 --- a/test/read-write.js +++ /dev/null @@ -1,130 +0,0 @@ -var request = require('supertest'), - assert = require('assert'), - server = require('../server'), - routes = require('../routes/read-write'), - fixture = require('./fixture'), - db, - app; - -describe('Read write routes', function() { - - beforeEach(function() { - db = fixture(); - app = server.createApp(db); - }); - - describe('GET /:resource', function() { - it('should respond with json and corresponding resources', function(done) { - request(app) - .get('/posts') - .expect('Content-Type', /json/) - .expect(db.posts) - .expect(200, done); - }); - }); - - describe('GET /:resource?attr=&attr=', function() { - it('should respond with json and filter resources', function(done) { - request(app) - .get('/comments?postId=1&published=true') - .expect('Content-Type', /json/) - .expect([db.comments[0]]) - .expect(200, done); - }); - }); - - describe('GET /:parent/:parentId/:resource', function() { - it('should respond with json and corresponding nested resources', function(done) { - request(app) - .get('/posts/1/comments') - .expect('Content-Type', /json/) - .expect([ - db.comments[0], - db.comments[1] - ]) - .expect(200, done); - }); - }); - - describe('GET /:resource/:id', function() { - it('should respond with json and corresponding resource', function(done) { - request(app) - .get('/posts/1') - .expect('Content-Type', /json/) - .expect(db.posts[0]) - .expect(200, done); - }); - }); - - describe('GET /db', function() { - it('should respond with json and full database', function(done) { - request(app) - .get('/db') - .expect('Content-Type', /json/) - .expect(db) - .expect(200, done); - }); - }); - - describe('POST /:resource', function() { - it('should respond with json and create a resource', function(done) { - request(app) - .post('/posts') - .send({body: 'foo'}) - .expect('Content-Type', /json/) - .expect({id: 3, body: 'foo'}) - .expect(200) - .end(function(err, res){ - if (err) return done(err); - assert.equal(db.posts.length, 3); - done(); - }); - }); - }); - - describe('PUT /:resource/:id', function() { - it('should respond with json and update resource', function(done) { - request(app) - .put('/posts/1') - .send({id: 1, body: 'foo'}) - .expect('Content-Type', /json/) - .expect({id: 1, body: 'foo'}) - .expect(200) - .end(function(err, res){ - if (err) return done(err); - assert.deepEqual(db.posts[0], {id: 1, body: 'foo'}); - done(); - }); - }); - }); - - describe('PATCH /:resource/:id', function() { - it('should respond with json and update resource', function(done) { - request(app) - .patch('/posts/1') - .send({body: 'bar'}) - .expect('Content-Type', /json/) - .expect({id: 1, body: 'bar'}) - .expect(200) - .end(function(err, res){ - if (err) return done(err); - assert.deepEqual(db.posts[0], {id: 1, body: 'bar'}); - done(); - }); - }); - }); - - describe('DELETE /:resource/:id', function() { - it('should respond with empty data, destroy resource and dependent resources', function(done) { - request(app) - .del('/posts/1') - .expect(204) - .end(function(err, res){ - if (err) return done(err); - assert.equal(db.posts.length, 1); - assert.equal(db.comments.length, 2); - done(); - }); - }); - }); -}); \ No newline at end of file diff --git a/test/server.js b/test/server.js new file mode 100644 index 0000000..56f462c --- /dev/null +++ b/test/server.js @@ -0,0 +1,171 @@ +var request = require('supertest') +var assert = require('assert') +var low = require('low') +var server = require('../src/server') +var db + +describe('Server', function() { + + beforeEach(function() { + low.db = {} + + low.db.posts = [ + {id: 1, body: 'foo'}, + {id: 2, body: 'bar'} + ] + + low.db.comments = [ + {id: 1, published: true, postId: 1}, + {id: 2, published: false, postId: 1}, + {id: 3, published: false, postId: 2}, + {id: 4, published: false, postId: 2}, + ] + }) + + describe('GET /db', function() { + it('should respond with json and full database', function(done) { + request(server) + .get('/db') + .expect('Content-Type', /json/) + .expect(low.db) + .expect(200, done) + }) + }) + + describe('GET /:resource', function() { + it('should respond with json and corresponding resources', function(done) { + request(server) + .get('/posts') + .expect('Content-Type', /json/) + .expect(low.db.posts) + .expect(200, done) + }) + }) + + describe('GET /:resource?attr=&attr=', function() { + it('should respond with json and filter resources', function(done) { + request(server) + .get('/comments?postId=1&published=true') + .expect('Content-Type', /json/) + .expect([low.db.comments[0]]) + .expect(200, done) + }) + }) + + describe('GET /:resource?_start=&_end=', function() { + it('should respond with sliced array', function(done) { + request(server) + .get('/comments?_start=1&_end=2') + .expect('Content-Type', /json/) + .expect(low.db.comments.slice(1, 2)) + .expect(200, done) + }) + }) + + describe('GET /:parent/:parentId/:resource', function() { + it('should respond with json and corresponding nested resources', function(done) { + request(server) + .get('/posts/1/comments') + .expect('Content-Type', /json/) + .expect([ + low.db.comments[0], + low.db.comments[1] + ]) + .expect(200, done) + }) + }) + + describe('GET /:resource/:id', function() { + it('should respond with json and corresponding resource', function(done) { + request(server) + .get('/posts/1') + .expect('Content-Type', /json/) + .expect(low.db.posts[0]) + .expect(200, done) + }) + }) + + describe('POST /:resource', function() { + it('should respond with json and create a resource', function(done) { + request(server) + .post('/posts') + .send({body: 'foo'}) + .expect('Content-Type', /json/) + .expect({id: 3, body: 'foo'}) + .expect(200) + .end(function(err, res){ + if (err) return done(err) + assert.equal(low.db.posts.length, 3) + done() + }) + }) + }) + + describe('PUT /:resource/:id', function() { + it('should respond with json and update resource', function(done) { + request(server) + .put('/posts/1') + .send({id: 1, body: 'foo'}) + .expect('Content-Type', /json/) + .expect({id: 1, body: 'foo'}) + .expect(200) + .end(function(err, res){ + if (err) return done(err) + assert.deepEqual(low.db.posts[0], {id: 1, body: 'foo'}) + done() + }) + }) + }) + + describe('PATCH /:resource/:id', function() { + it('should respond with json and update resource', function(done) { + request(server) + .patch('/posts/1') + .send({body: 'bar'}) + .expect('Content-Type', /json/) + .expect({id: 1, body: 'bar'}) + .expect(200) + .end(function(err, res){ + if (err) return done(err) + assert.deepEqual(low.db.posts[0], {id: 1, body: 'bar'}) + done() + }) + }) + }) + + describe('DELETE /:resource/:id', function() { + it('should respond with empty data, destroy resource and dependent resources', function(done) { + request(server) + .del('/posts/1') + .expect(204) + .end(function(err, res){ + if (err) return done(err) + assert.equal(low.db.posts.length, 1) + assert.equal(low.db.comments.length, 2) + done() + }) + }) + }) + + describe('Static routes', function() { + + describe('GET /', function() { + it('should respond with html', function(done) { + request(server) + .get('/') + .expect('Content-Type', /html/) + .expect(200, done); + }); + }); + + describe('GET /stylesheets/style.css', function() { + it('should respond with css', function(done) { + request(server) + .get('/stylesheets/style.css') + .expect('Content-Type', /css/) + .expect(200, done); + }); + }); + + }) +}) \ No newline at end of file diff --git a/test/static.js b/test/static.js deleted file mode 100644 index f623d76..0000000 --- a/test/static.js +++ /dev/null @@ -1,31 +0,0 @@ -var request = require('supertest'), - assert = require('assert'), - server = require('../server'), - routes = require('../routes/read-write'), - app; - -describe('Static routes', function() { - - beforeEach(function() { - app = server.createApp({}, routes); - }); - - describe('GET /', function() { - it('should respond with html', function(done) { - request(app) - .get('/') - .expect('Content-Type', /html/) - .expect(200, done); - }); - }); - - describe('GET /stylesheets/style.css', function() { - it('should respond with css', function(done) { - request(app) - .get('/stylesheets/style.css') - .expect('Content-Type', /css/) - .expect(200, done); - }); - }); - -}); \ No newline at end of file diff --git a/utils/db-mixins.js b/utils/db-mixins.js deleted file mode 100644 index d06a395..0000000 --- a/utils/db-mixins.js +++ /dev/null @@ -1,86 +0,0 @@ -(function(root) { - - var _ = root._ || require('underscore'); - - if (!root._) { - _.mixin(require('underscore.inflections')); - } - - function get(db, table, id) { - return _.find(db[table], function (row) { - return row.id === id - }); - } - - function exist(db, table, id) { - return !_.isUndefined(_.get(db, table, id)); - } - - function createId(db, table) { - if (_.isEmpty(db[table])) { - return 1; - } else { - return _.max(db[table], function(row) { - return row.id; - }).id + 1; - } - } - - function create(db, table, obj) { - var clone = _.clone(obj); - - if (_.isUndefined(clone.id)) clone.id = _.createId(db, table); - - db[table].push(clone); - - return clone; - } - - function update(db, table, id, attrs) { - var row = get(db, table, id), - updatedRow = _.extend(row, attrs), - index = _.indexOf(db[table], row); - - db[table][index] = updatedRow; - } - - function clean(db) { - var toBeRemoved = []; - - _(db).each(function(table, tableName) { - _(table).each(function(row) { - _(row).each(function(value, key) { - if (/Id$/.test(key)) { - var reference = _.pluralize(key.slice(0, - 2)); - if (!_.exist(db, reference, row[key])) { - toBeRemoved.push({ - tableName: tableName, - id: row.id - }); - } - } - }); - }); - }); - - _(toBeRemoved).each(function(row) { - _.remove(db, row.tableName, row.id); - }); - } - - function remove(db, table, id) { - var newTable = _.reject(db[table], function(row) { - return row.id === id; - }); - - db[table] = newTable; - } - - _.get = get; - _.exist = exist; - _.createId = createId; - _.create = create; - _.update = update; - _.clean = clean; - _.remove = remove; -})(this); \ No newline at end of file diff --git a/utils/logger.js b/utils/logger.js deleted file mode 100644 index 20a1200..0000000 --- a/utils/logger.js +++ /dev/null @@ -1,11 +0,0 @@ -var logan = require('logan'); - -logan.set({ - error: ['%', 'red'], - success: ['%', 'green'], - info: ['%', 'grey'], - notice: ['%', 'yellow'], - url: [' http://localhost:%/'.grey + '%'.cyan, '.'] -}) - -module.exports = logan \ No newline at end of file diff --git a/utils/utils.js b/utils/utils.js deleted file mode 100644 index c70615f..0000000 --- a/utils/utils.js +++ /dev/null @@ -1,11 +0,0 @@ -function toNative(value) { - if (value === 'true' || value === 'false') { - return value === 'true'; - } else if (!isNaN(+value)) { - return +value; - } else { - return value; - } -} - -exports.toNative = toNative; \ No newline at end of file