diff --git a/lib/collections/resources.js b/lib/collections/resources.js index a1bea3a..cf3728d 100644 --- a/lib/collections/resources.js +++ b/lib/collections/resources.js @@ -20,6 +20,15 @@ module.exports = , errs = [] ; + end(function (req, res, next) { + if(!collectionPath) return next(); + + collection.use(collectionPath).rename(collectionPath.replace('/', ''), function (err) { + next(err); + }); + }) + + properties && Object.keys(properties).forEach(function (key) { prop = properties[key]; rename = prop.$renameFrom; diff --git a/lib/router.js b/lib/router.js index e38182b..0384855 100644 --- a/lib/router.js +++ b/lib/router.js @@ -81,61 +81,3 @@ var router = module.exports = function (req, res, next) { } }) } - - - - - - - - - - - - - - -// module.exports = function (req, res, next) { -// var parsed = url.parse(req.url).path.split('?')[0].replace('/', '').split('/') -// , collections = req.collections = [] -// , references = req.references = [] -// , method = req.method -// , path = '/' -// ; -// -// // parse url into references and collections -// parsed.forEach(function (part, i) { -// (i % 2 ? references : collections).push(part); -// }); -// -// // cat collection reference -// path += collections[0]; -// -// // route to the first collection -// resources.get({path: path}).first(function (err, resource) { -// -// if(internals[path]) { -// if(req.isRemote && !req.isRoot) { -// // remote requests must have a registered key -// return next({status: 401}); -// } else { -// req.resource = { -// require: internals[path], -// path: path -// }; -// -// next(); -// } -// } else { -// // for future reference -// req.resource = resource; -// -// if(!resource) { -// err = {status: 404}; -// } -// -// // continue -// next(err); -// } -// }); -// } \ No newline at end of file diff --git a/lib/server.js b/lib/server.js index 89cd0b5..1b40af0 100644 --- a/lib/server.js +++ b/lib/server.js @@ -44,6 +44,12 @@ middleware.listen = function (callback) { // by default pause the request req.pause(); + // query sugar for JSON based query strings + // eg ?q={"foo": {"bar": true}} + if(req.query && req.query.q && req.query.q[0] === '{') { + req.query = JSON.parse(req.query.q); + } + if(req.method === 'GET' || req.method === 'DELETE') return next(); // check for json content type diff --git a/lib/validation.js b/lib/validation.js index 6a0cc82..1522a00 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -15,6 +15,7 @@ module.exports = function (req, res, next) { , resource = req.resource , validation , err + , sanitized = {} ; // rewrite queries from references @@ -37,15 +38,58 @@ module.exports = function (req, res, next) { } // if trying to write data - if((method === 'POST' || method === 'PUT') && resource && resource.properties) { + if((method === 'POST' || method === 'PUT') && req.body && resource && resource.properties) { + // sanitize data + Object.keys(resource.properties).forEach(function (key) { + sanitized[key] = req.body[key]; + }) + + // replace input with sanitized data + req.body = req.data = sanitized; // validate JSON validation = revalidator.validate(req.body, resource); - err = validation.valid ? err : validation; + err = validation.valid ? err : transform(validation); + next(err); } else { // continue next(err); } -} \ No newline at end of file +} + +/** + * Transform revalidator errors into human redable errors. + */ + +function transform(validation) { + var err = {} + , errors = validation.errors + , e + , prop + ; + + for(var i = 0, len = errors.length; i < len; i++) { + e = errors[i]; + prop = e.property; + + switch(e.attribute) { + case 'type': + err[prop] = 'must be a ' + e.expected; + break; + case 'required': + err[prop] = 'is required'; + break; + default: + err[prop] = 'is not valid' + break; + } + } + + // rename and add human readable errors + validation.validation = validation.errors; + validation.errors = err; + + return validation; +} diff --git a/test/query.test.js b/test/query.test.js new file mode 100644 index 0000000..ce7b1fc --- /dev/null +++ b/test/query.test.js @@ -0,0 +1,43 @@ +describe('Queries', function(){ + describe('GET /todos?title=testing title', function(){ + it('should return the documents that match the query', function(done) { + todos.post({title: 'testing title'}, function (err) { + todos.use('?title=testing%20title').get(function (err, todos) { + expect(todos).to.exist; + expect(todos).to.have.length(1); + done(err); + }) + }) + }) + }) + + describe('GET /todos?q={"title": "testing title"}', function(){ + it('should parse the JSON and return the documents that match the query', function(done) { + todos.post({title: 'testing title'}, function (err) { + todos.use('?q=' + encodeURI(JSON.stringify({title: 'testing title'}))).get(function (err, todos) { + expect(todos).to.exist; + expect(todos).to.have.length(1); + done(err); + }) + }) + }) + }) +}) + +describe('Advanced Queries', function(){ + describe('GET /todos?q={"title": {$regex: "^title"}', function(){ + it('should parse the JSON and return the documents that match the query', function(done) { + todos.post({title: 'title one'}, function (err) { + todos.post({title: 'title two'}, function (err) { + todos.post({title: 'another title'}, function (err) { + todos.use('?q=' + encodeURI(JSON.stringify({title: {$regex: "^title"}}))).get(function (err, todos) { + expect(todos).to.exist; + expect(todos).to.have.length(2); + done(err); + }) + }) + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/resources.test.js b/test/resources.test.js index 06c39df..7630397 100644 --- a/test/resources.test.js +++ b/test/resources.test.js @@ -62,6 +62,26 @@ describe('Application Resource Types', function(){ }) }) + describe('PUT /resources/', function(){ + it('should rename the resource collection', function(done) { + resources.get(function (e, all) { + var res = all[0]; + res.path = '/tasks'; + + todos.post({title: 'foo'}, function () { + resources.use('/' + res._id).put(res, function (err, upd) { + unauthed.use('/tasks').get(function (ee, r) { + expect(r).to.exist; + client.use('/tasks').del(function () { + done(ee || e || err); + }) + }) + }) + }) + }) + }) + }) + describe('PUT /resources/', function(){ it('should rename change properties on any existing data', function(done) { var exTodo = {title: 'feed fido', completed: true}; diff --git a/test/validation.test.js b/test/validation.test.js index 3c8ded2..adc30e6 100644 --- a/test/validation.test.js +++ b/test/validation.test.js @@ -12,15 +12,27 @@ describe('Resource Actions', function(){ describe('POST /todos', function(){ it('should return an error when provided invalid data', function(done) { - todos.post({foo: 'bar', bat: 'baz'}, function (err, todo, req, res) { + todos.post({foo: 123, completed: 'flarg'}, function (err, todo, req, res) { expect(err).to.exist; expect(err.valid).to.equal(false); - expect(err.errors).to.have.length(1); + expect(err.validation).to.have.length(2); + expect(err.errors).to.be.a('object'); expect(todo).to.not.exist; done(); }) }) + it('should ignore properties outside the schema', function(done) { + todos.post({title: 'foo', bat: 'baz'}, function (err, todo, req, res) { + todos.get(function (err, todos) { + var todo = todos[0]; + expect(todo.title).to.equal('foo'); + expect(todo.bat).to.not.exist; + done(err); + }) + }) + }) + it('should save the todo when valid', function(done) { todos.post({title: 'feed the cat'}, function (err, todo) { expect(todo._id).to.exist;