diff --git a/package.json b/package.json index 95a6cff..47cd1d6 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,16 @@ "test": "test" }, "dependencies": { + "body-parser": "^1.8.1", "chalk": "^0.4.0", "cors": "^2.3.0", - "express": "^3.4.8", + "errorhandler": "^1.2.0", + "express": "^4.9.0", "lowdb": "^0.3.0", "method-override": "^2.1.2", "minimist": "0.0.8", + "morgan": "^1.3.1", + "serve-static": "^1.6.1", "superagent": "~0.15.7", "underscore": "~1.5.2", "underscore.inflections": "~0.2.1", diff --git a/src/routes.js b/src/routes.js index aec511e..0b61eba 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,5 +1,5 @@ -var _ = require('underscore') -var low = require('lowdb') +var _ = require('underscore') +var low = require('lowdb') var utils = require('./utils') var routes = {} @@ -9,56 +9,75 @@ routes.db = function(req, res, next) { res.jsonp(low.db) } +// GET /:resource +// GET /:resource?q= // GET /:resource?attr=&attr= -// GET /:parent/:parentId/:resource +// GET /:parent/:parentId/:resource?attr=&attr= +// GET /*?*&limit= +// GET /*?*&offset=&limit= routes.list = function(req, res, next) { - var props = {} - var resource - var _start = req.query._start - var _end = req.query._end + // Filters list + var filters = {} - delete req.query._start - delete req.query._end + // Result array + var array - if (req.params.parent) { - props[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId - } + // Remove offset and limit from req.query to avoid filtering using those + // parameters + var offset = req.query.offset + var limit = req.query.limit - for (var key in req.query) { - if (key !== 'callback' && key != 'q') props[key] = utils.toNative(req.query[key]) - } + delete req.query.offset + delete req.query.limit - if(req.query.q !== undefined) { - var q = req.query.q.toLowerCase(), - keys = _.keys(low(req.params.resource).first()), - callback = function(element) { - for(var i in keys) { - var value = element[keys[i]]; + if (req.query.q) { - if (value === q || (_.isString(value) && value.toLowerCase().indexOf(q) !== -1)) { - return true; - } - } + var q = req.query.q.toLowerCase() - return false; + array = low(req.params.resource).where(function(obj) { + for (var key in obj) { + var value = obj[key] + if (_.isString(value) && value.toLowerCase().indexOf(q) !== -1) { + return true } + } + }).value() - resource = low(req.params.resource).where(callback).value() - } else if (_(props).isEmpty()) { - resource = low(req.params.resource).value() } else { - resource = low(req.params.resource).where(props).value() + + // Add :parentId filter in case URL is like /:parent/:parentId/:resource + if (req.params.parent) { + filters[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId + } + + // Add query parameters filters + // Convert query parameters to their native counterparts + for (var key in req.query) { + if (key !== 'callback') { + filters[key] = utils.toNative(req.query[key]) + } + } + + // Filter + if (_(filters).isEmpty()) { + array = low(req.params.resource).value() + } else { + array = low(req.params.resource).where(filters).value() + } } - if (_start) { - res.setHeader('X-Count', resource.length) - res.setHeader('Access-Control-Expose-Headers', 'X-Count') + // Slicing result + if (limit) { + res.setHeader('X-Total-Count', array.length) + res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count') - resource = resource.slice(_start, _end) + offset = offset || 0 + + array = array.slice(offset, limit) } - res.jsonp(resource) + res.jsonp(array) } // GET /:resource/:id @@ -67,7 +86,11 @@ routes.show = function(req, res, next) { .get(+req.params.id) .value() - res.jsonp(resource) + if (resource) { + res.jsonp(resource) + } else { + res.status(404).jsonp({}) + } } // POST /:resource @@ -94,7 +117,11 @@ routes.update = function(req, res, next) { .update(+req.params.id, req.body) .value() - res.jsonp(resource) + if (resource) { + res.jsonp(resource) + } else { + res.status(404).jsonp({}) + } } // DELETE /:resource/:id @@ -108,7 +135,7 @@ routes.destroy = function(req, res, next) { low(item[0]).remove(item[1]); }) - res.send(204) + res.status(204).end() } module.exports = routes diff --git a/src/server.js b/src/server.js index 81e2ef8..6bdbeb4 100644 --- a/src/server.js +++ b/src/server.js @@ -1,10 +1,15 @@ var fs = require('fs') -var express = require('express') -var cors = require('cors') -var http = require('http') var path = require('path') +var http = require('http') +var express = require('express') +var logger = require('morgan') +var cors = require('cors') var methodOverride = require('method-override') +var bodyParser = require('body-parser') +var serveStatic = require('serve-static') +var errorhandler = require('errorhandler') var low = require('lowdb') + var utils = require('./utils') var routes = require('./routes') @@ -13,36 +18,38 @@ 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(logger('dev')) +server.use(bodyParser.json()) +server.use(bodyParser.urlencoded({ extended: false })) server.use(methodOverride()) if (fs.existsSync(process.cwd() + '/public')) { - server.use(express.static(process.cwd() + '/public')); + server.use(serveStatic(process.cwd() + '/public')); } else { - server.use(express.static(path.join(__dirname, './public'))); + server.use(serveStatic(path.join(__dirname, './public'))); } server.use(cors({ origin: true, credentials: true })) -server.use(server.router) -if ('development' == server.get('env')) { - server.use(express.errorHandler()); +server.get('/db', routes.db) + +server.route('/:resource') + .get(routes.list) + .post(routes.create) + +server.route('/:resource/:id') + .get(routes.show) + .put(routes.update) + .patch(routes.update) + .delete(routes.destroy) + +server.get('/:parent/:parentId/:resource', routes.list) + +if (process.env.NODE_ENV === 'development') { + // only use in development + server.use(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.delete('/:resource/:id' , routes.destroy) - server.low = low module.exports = server \ No newline at end of file diff --git a/test/server.js b/test/server.js index 6bc6e3b..d9a2595 100644 --- a/test/server.js +++ b/test/server.js @@ -61,43 +61,45 @@ describe('Server', function() { }) }) - describe('GET /:resource?q=value', function() { - it('should respond with json and filter all begin of fields of resources', function(done) { + describe('GET /:resource?q=', function() { + it('should respond with json and make a full-text search', function(done) { request(server) - .get('/tags?q=photo') + .get('/tags?q=pho') .expect('Content-Type', /json/) .expect([low.db.tags[1], low.db.tags[2]]) .expect(200, done) }) - it('should respond with json and filter everywhere of all fields of resources', function(done) { + it('should return an empty array when nothing is matched', function(done) { request(server) - .get('/tags?q=t') - .expect('Content-Type', /json/) - .expect(low.db.tags) - .expect(200, done) - }) - - it('should not respond anything when the query does not many any data', function(done) { - request(server) - .get('/tags?q=nope') - .expect('Content-Type', /json/) - .expect([]) - .expect(200, done) + .get('/tags?q=nope') + .expect('Content-Type', /json/) + .expect([]) + .expect(200, done) }) }) - describe('GET /:resource?_start=&_end=', function() { - it('should respond with sliced array', function(done) { + describe('GET /:resource?limit=', function() { + it('should respond with a sliced array', function(done) { request(server) - .get('/comments?_start=1&_end=2') + .get('/comments?limit=2') .expect('Content-Type', /json/) + .expect('x-total-count', low.db.comments.length.toString()) + .expect('Access-Control-Expose-Headers', 'X-Total-Count') + .expect(low.db.comments.slice(0, 2)) + .expect(200, done) + }) + }) + + describe('GET /:resource?offset=&limit=', function() { + it('should respond with a sliced array', function(done) { + request(server) + .get('/comments?offset=1&limit=2') + .expect('Content-Type', /json/) + .expect('x-total-count', low.db.comments.length.toString()) + .expect('Access-Control-Expose-Headers', 'X-Total-Count') .expect(low.db.comments.slice(1, 2)) - .expect(200) - .end(function(err, res){ - assert.equal(res.headers['x-count'], 5) - done() - }) + .expect(200, done) }) }) @@ -122,8 +124,17 @@ describe('Server', function() { .expect(low.db.posts[0]) .expect(200, done) }) + + it('should respond with 404 if resource is not found', function(done) { + request(server) + .get('/posts/9001') + .expect('Content-Type', /json/) + .expect({}) + .expect(404, done) + }) }) + describe('POST /:resource', function() { it('should respond with json and create a resource', function(done) { request(server) @@ -150,10 +161,20 @@ describe('Server', function() { .expect(200) .end(function(err, res){ if (err) return done(err) + // assert it was created in database too assert.deepEqual(low.db.posts[0], {id: 1, body: 'bar', booleanValue: true, integerValue: 1}) done() }) }) + + it('should respond with 404 if resource is not found', function(done) { + request(server) + .put('/posts/9001') + .send({id: 1, body: 'bar', booleanValue: 'true', integerValue: '1'}) + .expect('Content-Type', /json/) + .expect({}) + .expect(404, done) + }) }) describe('PATCH /:resource/:id', function() { @@ -166,10 +187,20 @@ describe('Server', function() { .expect(200) .end(function(err, res){ if (err) return done(err) + // assert it was created in database too assert.deepEqual(low.db.posts[0], {id: 1, body: 'bar'}) done() }) }) + + it('should respond with 404 if resource is not found', function(done) { + request(server) + .patch('/posts/9001') + .send({body: 'bar'}) + .expect('Content-Type', /json/) + .expect({}) + .expect(404, done) + }) }) describe('DELETE /:resource/:id', function() {