From 83712fc6c7565cbfc0cdc3322bd2b30fb365d9ad Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Wed, 12 Apr 2017 13:42:31 +0200 Subject: [PATCH 01/27] v2 - basic REST --- Gruntfile.js | 26 -- package.json | 14 +- src/index.js | 569 ++++-------------------- test/db.coffee | 24 + test/del.coffee | 29 ++ test/delete-spec.js | 153 ------- test/get-collection-spec.js | 243 ---------- test/get-relation-spec.js | 138 ------ test/get-spec.js | 202 --------- test/get.coffee | 27 ++ test/helpers/helpers.js | 13 - test/helpers/hook-test-helpers.js | 145 ------ test/helpers/mock-app-helpers.js | 29 -- test/helpers/mock-collection-helpers.js | 6 - test/helpers/mock-model-helpers.js | 63 --- test/helpers/mock-promise-helpers.js | 43 -- test/helpers/mock-request-helpers.js | 9 - test/helpers/mock-response-helpers.js | 9 - test/initialization-spec.js | 186 -------- test/main.js | 66 +++ test/migrations/create_tables.coffee | 22 + test/post-relation-spec.js | 271 ----------- test/post-spec.js | 156 ------- test/post.coffee | 33 ++ test/put-spec.js | 171 ------- test/put.coffee | 33 ++ 26 files changed, 329 insertions(+), 2351 deletions(-) delete mode 100644 Gruntfile.js create mode 100644 test/db.coffee create mode 100644 test/del.coffee delete mode 100644 test/delete-spec.js delete mode 100644 test/get-collection-spec.js delete mode 100644 test/get-relation-spec.js delete mode 100644 test/get-spec.js create mode 100644 test/get.coffee delete mode 100644 test/helpers/helpers.js delete mode 100644 test/helpers/hook-test-helpers.js delete mode 100644 test/helpers/mock-app-helpers.js delete mode 100644 test/helpers/mock-collection-helpers.js delete mode 100644 test/helpers/mock-model-helpers.js delete mode 100644 test/helpers/mock-promise-helpers.js delete mode 100644 test/helpers/mock-request-helpers.js delete mode 100644 test/helpers/mock-response-helpers.js delete mode 100644 test/initialization-spec.js create mode 100644 test/main.js create mode 100644 test/migrations/create_tables.coffee delete mode 100644 test/post-relation-spec.js delete mode 100644 test/post-spec.js create mode 100644 test/post.coffee delete mode 100644 test/put-spec.js create mode 100644 test/put.coffee diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 58ed846..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = function(grunt) { - - require('load-grunt-tasks')(grunt); - - grunt.initConfig({ - - jasmine_node: { - options: { - - }, - all: ['test/'] - }, - - watch: { - js: { - files: ['src/**/*', 'test/**/*.js'], - tasks: 'test' - } - } - - }); - - grunt.registerTask('default', ['test']); - grunt.registerTask('test', ['jasmine_node']); - -}; \ No newline at end of file diff --git a/package.json b/package.json index b26aa75..760e585 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kalamata", - "version": "0.1.3", + "version": "2.0.0", "description": "Extensible REST API for Express + Bookshelf.js", "homepage": "https://github.com/mikec/kalamata", "bugs": "https://github.com/mikec/kalamata/issues", @@ -22,17 +22,19 @@ "bookshelf": "^0.10" }, "devDependencies": { - "grunt": "0.4.5", - "grunt-contrib-watch": "0.6.1", - "grunt-jasmine-node": "git://github.com/fiznool/grunt-jasmine-node.git#c773421b608ce944454cb540a6e66575d2df09c6", - "load-grunt-tasks": "0.6.0" + "chai": "^3.5.0", + "chai-http": "^3.0.0", + "coffee-script": "^1.12.5", + "knex": "^0.12.6", + "mocha": "^2.3.4", + "sqlite3": "^3.1.8" }, "dependencies": { "body-parser": "^1.9.0", "bluebird": "^3" }, "scripts": { - "test": "grunt" + "test": "mocha --compilers coffee:coffee-script/register test" }, "main": "src/index.js" } diff --git a/src/index.js b/src/index.js index d6fe4a6..709b9c0 100644 --- a/src/index.js +++ b/src/index.js @@ -1,483 +1,88 @@ -var bodyParser = require('body-parser'); -var Promise = require("bluebird"); -var app, options; -var hooks = {}; -var modelMap = {}; -var identifierMap = {}; -var modelNameMap = {}; -var collectionNameMap = {}; -var kalamata = module.exports = function(_app_, _options_) { - app = _app_; - options = _options_; - - app.use(bodyParser.json()); - - if(!options) options = {}; - if(!options.apiRoot) options.apiRoot = '/'; - else options.apiRoot = '/' + options.apiRoot.replace(/^\/|\/$/g, '') + '/'; - - return kalamata; -}; - -kalamata.expose = function(model, _opts_) { - - var validOpts = { - identifier: true, - endpointName: true, - modelName: true, - collectionName: true - }; - - var opts = {}; - if(!opts.identifier) opts.identifier = 'id'; - if(!opts.endpointName) opts.endpointName = model.forge().tableName; - - for(var p in _opts_) { - if(validOpts[p]) { - opts[p] = _opts_[p]; - } else { - throw new Error('Invalid option: ' + p); - } - } - - modelMap[opts.endpointName] = model; - identifierMap[opts.endpointName] = opts.identifier; - - hooks[opts.endpointName] = { - before: hookArrays(), - after: hookArrays() - }; - - var beforeHooks = hooks[opts.endpointName].before; - var afterHooks = hooks[opts.endpointName].after; - - opts.collectionName = opts.collectionName ? - capitalize(opts.collectionName) : - collectionName(opts.endpointName); - opts.modelName = opts.modelName ? - capitalize(opts.modelName) : - modelName(opts.endpointName); - - var modelNameLower = decapitalize(opts.modelName); - var collectionNameLower = decapitalize(opts.collectionName); - - modelMap[modelNameLower] = - modelMap[collectionNameLower] = model; - identifierMap[modelNameLower] = - identifierMap[collectionNameLower] = opts.identifier; - modelNameMap[collectionNameLower] = modelNameLower; - collectionNameMap[modelNameLower] = collectionNameLower; - - hooks[modelNameLower] = hooks[collectionNameLower] = { - before: hookArrays(), - after: hookArrays() - }; - - var beforeHooks = hooks[modelNameLower].before; - var afterHooks = hooks[modelNameLower].after; - - createHookFunctions(); - configureEndpoints(); - - function configureEndpoints() { - - app.get(options.apiRoot + opts.endpointName, function(req, res, next) { - var mod; - if(req.query.where) { - var w; - try { - w = parseJSON(req.query.where); - } catch(err) { - throw new Error('Could not parse JSON: ' + req.query.where); - } - mod = new model().where(w); - } else { - mod = new model(); - } - - var beforeResult = runHooks(beforeHooks.getCollection, [req, res, mod]); - if(res.headersSent) return; - - var promise = beforeResult.promise || mod.fetchAll(getFetchParams(req, res)); - promise.then(function(collection) { - var afterResult = runHooks(afterHooks.getCollection, [req, res, collection]); - return afterResult.promise || collection; - }).then(function(collection) { - sendResponse(res, collection.toJSON()); - }).catch(next); - }); - - app.get(options.apiRoot + opts.endpointName + '/:identifier', - function(req, res, next) { - var mod = new model(getModelAttrs(req)); - - var beforeResult = runHooks(beforeHooks.get, [req, res, mod]); - if(res.headersSent) return; - - var promise = beforeResult.promise || mod.fetch(getFetchParams(req, res)); - promise.then(function(m) { - return checkModelFetchSuccess(req, m); - }).then(function(m) { - var afterResult = runHooks(afterHooks.get, [req, res, m]); - return afterResult.promise || m; - }).then(function(m) { - sendResponse(res, m); - }).catch(next); - }); - - app.get(options.apiRoot + opts.endpointName + '/:identifier/:relation', - function(req, res, next) { - var mod = new model(getModelAttrs(req)); - mod.fetch({ - withRelated: getWithRelatedArray([req.params.relation], req, res) - }).then(function(m) { - return checkModelFetchSuccess(req, m); - }).then(function(m) { - return m.related(req.params.relation); - }).then(function(related) { - var afterResult = {}; - var relHooks = hooks[req.params.relation]; - if(relHooks) { - afterResult = runHooks( - hooks[req.params.relation].after.getRelated, - [req, res, related, mod]); - } - return afterResult.promise || related; - }).then(function(related) { - sendResponse(res, related); - }).catch(next); - }); - - app.post(options.apiRoot + opts.endpointName, function(req, res, next) { - var mod = new model(req.body); - - var beforeResult = runHooks(beforeHooks.create, [req, res, mod]); - if(res.headersSent) return; - - var promise = beforeResult.promise || mod.save(); - promise.then(function(m) { - if(m) { - var afterResult = runHooks(afterHooks.create, [req, res, m]); - return afterResult.promise || m; - } - }).then(function(m) { - if(m) { - sendResponse(res, m.toJSON()); - } - }).catch(next); - }); - - app.post(options.apiRoot + opts.endpointName + '/:identifier/:relation', - function(req, res, next) { - var rel = req.params.relation; - var rModel = modelMap[rel]; - var rId = identifierMap[rel]; - var mod = new model(getModelAttrs(req)); - var relMod; - - var beforeResult = runMultiHooks( - [hooks[req.params.relation].before.relate, - [req, res, mod]], - [beforeHooks.relate, - [req, res, mod]]); - if(res.headersSent) return; - - var promise; - if(beforeResult.promise) { - promise = beforeResult.promise; - } else { - promise = mod.fetch(); - } - - promise.then(function(m) { - if(req.body[rId]) { - // fetch and add an existing model - return (new rModel(req.body)).fetch().then(function(rMod) { - if(rMod) { - relMod = rMod; - var relCollection = m.related(rel); - if(relCollection.create) { - // for hasMany relations - return relCollection.create(rMod); - } else { - // for belongsTo relations, reverse it - return rMod.related(opts.endpointName).create(m); - } - } else { - throw new Error('Create relationship failed: ' + - 'Could not find ' + rel + - ' model ' + JSON.stringify(req.body)); - } - }); - } else { - throw new Error('Create relationship failed: ' + - rId + ' property not provided'); - } - }).then(function() { - var afterResult = runMultiHooks( - [hooks[req.params.relation].after.relate, - [req, res, mod, relMod]], - [afterHooks.relate, - [req, res, mod, relMod]]); - return afterResult.promise || null; - return null; - }).then(function() { - sendResponse(res, null); - }).catch(next); - }); - - app.put(options.apiRoot + opts.endpointName + '/:identifier', - function(req, res, next) { - new model(getModelAttrs(req)).fetch().then(function(m) { - if(m) m.set(req.body); - var beforeResult = runHooks(beforeHooks.update, [req, res, m]); - if(!res.headersSent) { - if(m) { - return beforeResult.promise || m.save(); - } else { - return checkModelFetchSuccess(req, m); - } - } - }).then(function(m) { - if(m) { - var afterResult = runHooks(afterHooks.update, [req, res, m]); - return afterResult.promise || m; - } - }).then(function(m) { - if(m) { - sendResponse(res, m.toJSON()); - } - }).catch(next); - }); - - app.delete(options.apiRoot + opts.endpointName + '/:identifier', - function(req, res, next) { - new model(getModelAttrs(req)).fetch().then(function(m) { - var beforeResult = runHooks(beforeHooks.del, [req, res, m]); - if(!res.headersSent) { - if(m) { - return beforeResult.promise || m.destroy(); - } else { - return checkModelFetchSuccess(req, m); - } - } - }).then(function(m) { - if(m) { - var afterResult = runHooks(afterHooks.del, [req, res, m]); - return afterResult.promise || m; - } - }).then(function() { - sendResponse(res, true); - }).catch(next); - }); - - app.delete(options.apiRoot + opts.endpointName + '/:identifier/:relation', - function(req, res, next) { - var rel = req.params.relation; - var mod = new model(getModelAttrs(req)); - mod.fetch().then(function(m) { - var fKey = m[rel]().relatedData.foreignKey; - return m.set(fKey, null).save(); - }).then(function() { - sendResponse(res, true); - }).catch(next); - }); - - app.delete(options.apiRoot + opts.endpointName + '/:identifier/:relation/:rIdentifier', - function(req, res, next) { - var rel = req.params.relation; - var rModel = modelMap[rel]; - var rId = identifierMap[rel]; - var mod = new model(getModelAttrs(req)); - var relMod; - - mod.fetch().then(function(m) { - var rModAttrs = {}; - rModAttrs[rId] = req.params.rIdentifier; - return (new rModel(rModAttrs)).fetch().then(function(rMod) { - if(rMod) { - var modelName = modelNameMap[opts.endpointName]; - var fKey = rMod[modelName]().relatedData.foreignKey; - return rMod.set(fKey, null).save(); - } else { - throw new Error('Delete relationship failed: ' + - 'Could not find ' + rel + - ' model ' + JSON.stringify(rModAttrs)); - } - }); - }).then(function() { - sendResponse(res, true); - }).catch(next); - - }); - } - - function runMultiHooks() { - var promiseResults = []; - for(var i in arguments) { - var res = runHooks.apply(null, arguments[i]); - if(res.promise) { - promiseResults.push(res.promise); - } - } - if(promiseResults.length > 0) { - var ret = Promise.all(promiseResults).then(function() { - var args = arguments[0]; - return new Promise(function(resolve) { - resolve.apply(null, args); - }); - }); - return { promise: ret }; - } else { - return {}; - } - } - - function runHooks(fnArray, args) { - var result; - for(var i in fnArray) { - result = fnArray[i].apply(null, args); - } - if(result && result.then) { - return { - promise: result - }; - } else { - return {}; - } - } - - function hookArrays() { - return { - get: [], - getCollection: [], - getRelated: [], - create: [], - update: [], - del: [], - relate: [] - }; - } - - function createHookFunctions() { - createHookFunction('beforeGet' + opts.collectionName, - 'before', 'getCollection'); - createHookFunction('beforeGetRelated' + opts.collectionName, - 'before', 'getRelated'); - createHookFunction('beforeGet' + opts.modelName, 'before', 'get'); - createHookFunction('beforeCreate' + opts.modelName, 'before', 'create'); - createHookFunction('beforeUpdate' + opts.modelName, 'before', 'update'); - createHookFunction('beforeDelete' + opts.modelName, 'before', 'del'); - createHookFunction('beforeRelate' + opts.modelName, 'before', 'relate'); - createHookFunction('afterGet' + opts.collectionName, - 'after', 'getCollection'); - createHookFunction('afterGetRelated' + opts.collectionName, - 'after', 'getRelated'); - createHookFunction('afterGet' + opts.modelName, 'after', 'get'); - createHookFunction('afterCreate' + opts.modelName, 'after', 'create'); - createHookFunction('afterUpdate' + opts.modelName, 'after', 'update'); - createHookFunction('afterDelete' + opts.modelName, 'after', 'del'); - createHookFunction('afterRelate' + opts.modelName, 'after', 'relate'); - } - - function createHookFunction(fnName, prefix, type) { - kalamata[fnName] = hookFn(prefix, type, fnName); - } - - function hookFn(prefix, type, fnName) { - if(type) { - return function(fn) { - fn.__name = fnName; - hooks[opts.endpointName][prefix][type].push(fn); - }; - } else { - return function(fn) { - fn.__name = fnName; - for(var i in hooks[prefix]) { - hooks[opts.endpointName][prefix][i].push(fn); - } - }; - } - } - - function checkModelFetchSuccess(req, m) { - if(!m) { - throw new Error( - req.method + ' ' + req.url + ' failed: ' + - opts.identifier + ' = ' + req.params.identifier + - ' not found' - ); - } - return m; - } - - function getModelAttrs(req) { - var attrs; - if(req.params.identifier) { - attrs = {}; - attrs[opts.identifier] = req.params.identifier; - } - return attrs; - } - - function getWithRelatedArray(related, req, res) { - var relArray = []; - for(var i in related) { - var r = related[i]; - var relHooks = hooks[r]; - if(relHooks) { - var relObj = {}; - relObj[r] = function(qb) { - runHooks(relHooks.before.getRelated, [req, res, qb]); - }; - relArray.push(relObj); - } else { - relArray.push(r); - } - } - return relArray; - } - - function getFetchParams(req, res) { - return req.query.load ? { - withRelated: getWithRelatedArray(req.query.load.split(','), req, res) - } : null; - } - - function sendResponse(response, sendData) { - if(!response.headersSent) response.send(sendData); - } - - function collectionName(endpointName) { - endpointName = capitalize(endpointName); - endpointName += (endpointName.slice(-1) == 's' ? '' : 'Collection'); - return endpointName; - } - - function modelName(endpointName) { - endpointName = (endpointName.slice(-1) == 's' ? - endpointName.substr(0,endpointName.length - 1) : - endpointName); - return capitalize(endpointName); - } - - function decapitalize(str) { - return str.charAt(0).toLowerCase() + str.slice(1); - } - - function capitalize(str) { - return str.charAt(0).toUpperCase() + str.slice(1); - } - - function parseJSON(str) { - return JSON.parse(fixJSONString(str)); - } - - function fixJSONString(str) { - return str.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2": '); - } - - return kalamata; - -}; \ No newline at end of file +module.exports = function(model, opts) { + + opts = opts || {} + + function _list_query(req, res, next) { + if(req.query.where) { + try { + req.listquery = parseJSON(req.query.where) + } catch(err) { + return next(new Error('Could not parse JSON: ' + req.query.where)) + } + } + next() + } + + function _list_middleware(req, res, next) { + let mod = new model() + if(req.listquery) { + mod = mod.where(req.listquery) + } + mod.fetchAll() + .then(function(collection) { + res.json(collection) + next() + }) + .catch(next) + } + + function _detail_middleware(req, res, next) { + res.json(req.fetched) // just send the fetched + next() + } + + function _create_middleware(req, res, next) { + var newitem = new model(req.body) + newitem.save() + .then(function(savedModel) { + req.saveditem = savedModel + res.status(201).json(savedModel) + next() + }) + .catch(next) + } + + function _fetch_middleware(req, res, next) { + var mod = new model({id: req.params.id}) + mod.fetch() + .then(function(fetched) { + if(!fetched) { + return next(new Error(404)) + } + req.fetched = fetched + next() + }) + .catch(next) + } + + function _update_middleware(req, res, next) { + req.fetched.save(req.body) + .then(function(saved) { + res.status(200).json(saved) + }) + .catch(next) + } + + function _delete_middleware(req, res, next) { + req.fetched.destroy() + .then(function(saved) { + res.status(200).send('deleted') + next() + }) + .catch(next) + } + + function _init_app(app) { + app.get('/', _list_query, _list_middleware) + app.get('/:id', _fetch_middleware, _detail_middleware) + app.post('/', _create_middleware) + app.put('/:id', _fetch_middleware, _update_middleware) + app.delete('/:id', _fetch_middleware, _delete_middleware) + } + + return { + init_app: _init_app + } + +} diff --git a/test/db.coffee b/test/db.coffee new file mode 100644 index 0000000..bb09331 --- /dev/null +++ b/test/db.coffee @@ -0,0 +1,24 @@ +Knex = require('knex') + +debugopts = + client: 'sqlite3' + connection: + filename: ':memory:' + debug: true + migrations: + directory: __dirname + '/migrations' + +knex = Knex(debugopts) +bookshelf = require('bookshelf')(knex) + +User = bookshelf.Model.extend + tableName: 'users' + +Tool = bookshelf.Model.extend + tableName: 'tools' + +knex.models = + User: User + Tool: Tool + +module.exports = knex diff --git a/test/del.coffee b/test/del.coffee new file mode 100644 index 0000000..98d042e --- /dev/null +++ b/test/del.coffee @@ -0,0 +1,29 @@ + +chai = require('chai') +should = chai.should() + +module.exports = (g)-> + + addr = g.baseurl + + describe 'delete routes', -> + + it 'must delete user', (done) -> + chai.request(g.baseurl) + .delete("/#{g.gandalfID}") + .end (err, res) -> + return done(err) if err + res.should.have.status(200) + chai.request(g.baseurl).get("/#{g.gandalfID}").end (err, res) -> + return done('still exist') if not err + res.should.have.status(404) + done() + return + + it 'must 404 on notexisting user', (done) -> + chai.request(g.baseurl) + .delete("/hovno") + .end (err, res) -> + return done('shall 404 but have not') if not err + res.should.have.status(404) + done() diff --git a/test/delete-spec.js b/test/delete-spec.js deleted file mode 100644 index 2d0bc4b..0000000 --- a/test/delete-spec.js +++ /dev/null @@ -1,153 +0,0 @@ -describe('DELETE request to delete an item', function() { - - beforeEach(function() { - this.mockApp = new MockApp(); - this.k = requireKalamata()(this.mockApp); - }); - - describe('for an item that exists', function() { - - beforeEach(function() { - this.mockRequest = new MockRequest({ - params: { identifier: '1' } - }); - this.mockResponse = new MockResponse(); - var m = MockModel.get(); - mockModel = MockModel.get('items', { - fetch: function() { - return new MockPromise([new m()]); - } - }); - spyOn(this.mockResponse, 'send'); - this.k.expose(mockModel); - this.mockApp.deleteHandlers['/items/:identifier']( - this.mockRequest, - this.mockResponse - ); - }); - - it('should set attributes on the model that will be fetched', function() { - expect(mockModel.modelInstances[0].attributes) - .toEqual({ id : '1' }); - }); - - it('should respond with true', function() { - expect(this.mockResponse.send.calls.argsFor(0)[0]) - .toEqual(true); - }); - - }); - - describe('for an item that does not exist', function() { - - beforeEach(function() { - var $this = this; - this.mockNextFn = function() {}; - spyOn(this, 'mockNextFn'); - this.p = new MockPromise(); - this.k.expose(MockModel.get('items', { - fetch: function() { - return $this.p; - } - })); - this.mockApp.deleteHandlers['/items/:identifier']( - new MockRequest({ - params: { identifier: 1 }, - method: 'DELETE', - url: 'mock.com/items/1' - }), - new MockResponse(), - this.mockNextFn - ); - }); - - it('should call next with an item not found error', function() { - expect(this.mockNextFn).toHaveBeenCalled(); - expect(this.mockNextFn.calls.argsFor(0)[0].message) - .toEqual('DELETE mock.com/items/1 failed: id = 1 not found'); - }); - - }); - - describe('with a before hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('before', 'DeleteItem', '/items/:identifier'); - - it('should pass the fetch result argument to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockFetchResult); - }); - - it('should call destroy on the fetch result', function() { - expect(this.mockFetchResult.destroy).toHaveBeenCalled(); - }); - - }); - - describe('that throws an error', function() { - - hookErrorTest('before', 'DeleteItem', '/items/:identifier', true); - - it('should not call destroy on the fetch result', function() { - expect(this.mockFetchResult.destroy).not.toHaveBeenCalled(); - }); - - }); - - describe('that sends a response', function() { - - beforeEach(function() { - setupHook.call( - this, 'before', 'DeleteItem', '/items/:identifier', - function(req, res) { res.send(true); } - ); - }); - - it('should not call destroy on the fetch result', function() { - expect(this.mockFetchResult.destroy).not.toHaveBeenCalled(); - }); - - }); - - describe('that returns a promise', function() { - - hookPromiseTest('before', 'DeleteItem', '/items/:identifier'); - - it('should not call destroy on the fetch result', function() { - expect(this.mockFetchResult.destroy).not.toHaveBeenCalled(); - }); - - }); - - }); - - describe('with an after hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('after', 'DeleteItem', '/items/:identifier'); - - it('should pass the fetch result to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockFetchResult); - }); - - }); - - describe('that throws an error', function() { - hookErrorTest('after', 'DeleteItem', '/items/:identifier', true); - }); - - describe('that sends a response', function() { - singleResponseHookTest('after', 'DeleteItem', '/items/:identifier'); - }); - - describe('that returns a promise', function() { - hookPromiseTest('after', 'DeleteItem', '/items/:identifier'); - }); - - }); - -}); \ No newline at end of file diff --git a/test/get-collection-spec.js b/test/get-collection-spec.js deleted file mode 100644 index b2b9228..0000000 --- a/test/get-collection-spec.js +++ /dev/null @@ -1,243 +0,0 @@ -describe('GET request for collection', function() { - - beforeEach(function() { - this.mockApp = new MockApp(); - this.k = requireKalamata()(this.mockApp); - }); - - describe('without any query params', function() { - - beforeEach(function() { - var $this = this; - this.mockCollectionJSON = ['one','two','three']; - this.mockCollection = { - toJSON: function() { return $this.mockCollectionJSON; } - }; - this.mockModel = MockModel.get('items', { - fetchAll: function() { - return new MockPromise([$this.mockCollection]); - } - }); - this.k.expose(this.mockModel); - this.mockResponse = new MockResponse(); - spyOn(this.mockResponse, 'send'); - this.mockApp.getHandlers['/items']( - new MockRequest(), - this.mockResponse - ); - }); - - it('should instantiate a new model', function() { - expect(this.mockModel.modelInstances.length).toEqual(1); - }); - - it('should respond with the result of the fetchAll promise', function() { - expect(this.mockResponse.send.calls.argsFor(0)[0]) - .toEqual(this.mockCollectionJSON); - }); - - }); - - describe('when fetchAll throws an error', function() { - - beforeEach(function() { - var $this = this; - this.mockErr = new Error('mock error'); - this.mockModel = MockModel.get('items', { - fetchAll: function() { - return new MockFailPromise($this.mockErr); - } - }); - this.k.expose(this.mockModel); - this.mockNextFn = function() {}; - spyOn(this, 'mockNextFn'); - this.mockApp.getHandlers['/items']( - new MockRequest(), - new MockResponse(), - this.mockNextFn - ); - }); - - it('should call next with the error', function() { - expect(this.mockNextFn).toHaveBeenCalled(); - expect(this.mockNextFn.calls.argsFor(0)[0]).toBe(this.mockErr); - }); - - }); - - describe('with the \'where\' query param', function() { - - describe('set to a valid JSON object', function() { - - beforeEach(function() { - setupWhereQueryTests - .call(this, '{"firstname":"mike","lastname":"c"}'); - }); - - it('should call \'where\' on the model and pass the parsed query value', - function() { - expect(this.w.firstname).toEqual('mike'); - expect(this.w.lastname).toEqual('c'); - }); - - }); - - describe('set to a JSON object without quotes around property names', - function() { - - beforeEach(function() { - setupWhereQueryTests.call(this, '{firstname:"mike",lastname:"c"}'); - }); - - it('should call \'where\' on the model and pass the parsed query value', - function() { - expect(this.w.firstname).toEqual('mike'); - expect(this.w.lastname).toEqual('c'); - }); - - }); - - describe('set to an invalid JSON object', function() { - - beforeEach(function() { - setupWhereQueryTests.call(this, '{firstname}'); - }); - - it('should throw a \'Could not parse JSON\' error', function() { - expect(this.error.message).toEqual('Could not parse JSON: {firstname}'); - }); - - }); - - function setupWhereQueryTests(whereQueryVal) { - var $this = this; - this.w = null; - var mockModel = MockModel.get('items', { - where: function(_w) { - $this.w = _w; - return { - fetchAll: function() { - return new MockPromise(['items']); - } - } - } - }); - this.k.expose(mockModel); - this.mockResponse = new MockResponse(); - spyOn(this.mockResponse, 'send'); - try { - this.mockApp.getHandlers['/items'](new MockRequest({ - query: { - where: whereQueryVal - } - }), this.mockResponse); - } catch (err) { - this.error = err; - } - } - - }); - - describe('with a \'load\' query param', function() { - - beforeEach(function() { - this.mockFetchAllFn = function() {}; - spyOn(this, 'mockFetchAllFn').and.returnValue(new MockPromise()); - this.mockModel = MockModel.get('items', { - fetchAll: this.mockFetchAllFn - }); - this.k.expose(this.mockModel); - this.mockApp.getHandlers['/items']( - new MockRequest({ - query: { - load: 'orders,companies' - } - }), - new MockResponse() - ); - }); - - it('should call load and pass an array of relation objects', function() { - expect(this.mockFetchAllFn).toHaveBeenCalled(); - expect(this.mockFetchAllFn.calls.argsFor(0)[0].withRelated) - .toEqual(['orders','companies']); - }); - - }); - - describe('with a before hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('before', 'GetItems', '/items'); - - it('should pass model argument to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockModel.modelInstances[0]); - }); - - it('should call fetchAll', function() { - expect(this.mockFetchAll).toHaveBeenCalled(); - }); - - }); - - describe('that throws an error', function() { - hookErrorTest('before', 'GetItems', '/items'); - }); - - describe('that sends a response', function() { - - beforeEach(function() { - setupHook.call( - this, 'before', 'GetItems', '/items', - function(req, res) { res.send(true); } - ); - }); - - it('should not call fetchAll', function() { - expect(this.mockFetchAll).not.toHaveBeenCalled(); - }); - - }); - - describe('that returns a promise', function() { - - hookPromiseTest('before', 'GetItems', '/items'); - - it('should not call fetchAll', function() { - expect(this.mockFetchAll).not.toHaveBeenCalled(); - }); - - }); - - }); - - describe('with an after hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('after', 'GetItems', '/items'); - - it('should pass the result of fetchAll to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockFetchAllResult); - }); - - }); - - describe('that throws an error', function() { - hookErrorTest('after', 'GetItems', '/items', true); - }); - - describe('that sends a response', function() { - singleResponseHookTest('after', 'GetItems', '/items'); - }); - - describe('that returns a promise', function() { - hookPromiseTest('after', 'GetItems', '/items'); - }); - - }); - -}); \ No newline at end of file diff --git a/test/get-relation-spec.js b/test/get-relation-spec.js deleted file mode 100644 index 7b55fb7..0000000 --- a/test/get-relation-spec.js +++ /dev/null @@ -1,138 +0,0 @@ -describe('GET request for a relation', function() { - - beforeEach(function() { - this.mockApp = new MockApp(); - this.k = requireKalamata()(this.mockApp); - }); - - describe('for a model that exists', function() { - - beforeEach(function() { - var $this = this; - this.mockCollection = {}; - this.mockModel = MockModel.get('items', { - related: function() { - return new MockPromise([$this.mockCollection]); - } - }); - this.k.expose(this.mockModel); - this.mockResponse = new MockResponse(); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.getHandlers['/items/:identifier/:relation']; - fn(new MockRequest(), this.mockResponse); - }); - - it('should respond with a collection of related models', function() { - expect(this.mockResponse.send).toHaveBeenCalled(); - expect(this.mockResponse.send.calls.argsFor(0)[0]) - .toBe(this.mockCollection); - }); - - }); - - describe('for a model that does not exist', function() { - - beforeEach(function() { - var $this = this; - this.mockNextFn = function() {}; - spyOn(this, 'mockNextFn'); - this.mockModel = MockModel.get('items', { - fetch: function() { - return new MockPromise([null]); - } - }); - this.k.expose(this.mockModel); - this.mockResponse = new MockResponse(); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.getHandlers['/items/:identifier/:relation']; - fn( - new MockRequest({ - params: { identifier: '1' }, - method: 'GET', - url: 'mock.com/items/1/things' - }), - this.mockResponse, - this.mockNextFn - ); - }); - - it('should call next with a not found error', function() { - expect(this.mockNextFn).toHaveBeenCalled(); - expect(this.mockNextFn.calls.argsFor(0)[0].message) - .toEqual('GET mock.com/items/1/things failed: id = 1 not found'); - }); - - it('should not send a response', function() { - expect(this.mockResponse.send).not.toHaveBeenCalled(); - }); - - }); - - describe('with a before hook', function() { - - describe('that runs without executing any code', function() { - - beforeEach(function() { - var $this = this; - this.hookFn = function() {} - this.k.expose(MockModel.get('items')); - this.k.expose(MockModel.get('things')); - this.k.beforeGetRelatedThings(function() { - $this.hookFn(); - }); - this.mockResponse = new MockResponse(); - spyOn(this.mockResponse, 'send'); - spyOn(this, 'hookFn'); - var fn = this.mockApp.getHandlers['/items/:identifier/:relation']; - fn(new MockRequest({ - params: { identifier: '1', relation: 'things' } - }), this.mockResponse); - }); - - it('should call the hook', function() { - expect(this.hookFn).toHaveBeenCalled(); - }); - - it('should send a response', function() { - expect(this.mockResponse.send).toHaveBeenCalled(); - }); - - }); - - describe('that throws an error', function() { - - beforeEach(function() { - var $this = this; - this.mockNextFn = function() {}; - this.hookFn = function() { - $this.mockError = new Error('mock error'); - throw $this.mockError; - } - this.k.expose(MockModel.get('items')); - this.k.expose(MockModel.get('things')); - this.k.beforeGetRelatedThings(function() { - $this.hookFn(); - }); - this.mockResponse = new MockResponse(); - spyOn(this.mockResponse, 'send'); - spyOn(this, 'mockNextFn'); - var fn = this.mockApp.getHandlers['/items/:identifier/:relation']; - try { - fn(new MockRequest({ - params: { identifier: '1', relation: 'things' } - }), this.mockResponse, this.mockNextFn); - } catch(err) { - this.error = err; - } - }); - - it('should throw an error', function() { - expect(this.error).toBeDefined(); - expect(this.error).toBe(this.mockError); - }); - - }); - - }); - -}); \ No newline at end of file diff --git a/test/get-spec.js b/test/get-spec.js deleted file mode 100644 index 9750155..0000000 --- a/test/get-spec.js +++ /dev/null @@ -1,202 +0,0 @@ -describe('GET request for single item', function() { - - beforeEach(function() { - this.mockApp = new MockApp(); - this.k = requireKalamata()(this.mockApp); - }); - - describe('with an identifier for an item that exists', function() { - - beforeEach(function() { - var $this = this; - this.mockFetchedModel = { name: 'mock' }; - this.mockModel = MockModel.get('items', { - fetch: function() { - return new MockPromise([$this.mockFetchedModel]); - } - }); - this.k.expose(this.mockModel); - this.mockResponse = new MockResponse(); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.getHandlers['/items/:identifier'] - fn(new MockRequest(), this.mockResponse); - }); - - it('should instantiate a model instance', function() { - expect(this.mockModel.modelInstances.length).toEqual(1); - }); - - it('should respond with the fetched model', function() { - expect(this.mockResponse.send.calls.argsFor(0)[0]) - .toEqual(this.mockFetchedModel); - }); - - }); - - describe('with a \'load\' query param', function() { - - beforeEach(function() { - this.mockFetchFn = function() {}; - spyOn(this, 'mockFetchFn').and.returnValue(new MockPromise()); - this.mockModel = MockModel.get('items', { - fetch: this.mockFetchFn - }); - this.k.expose(this.mockModel); - this.mockApp.getHandlers['/items/:identifier']( - new MockRequest({ - query: { - load: 'users,things' - } - }), - new MockResponse() - ); - }); - - it('should call load and pass an array of relations', function() { - expect(this.mockFetchFn).toHaveBeenCalled(); - expect(this.mockFetchFn.calls.argsFor(0)[0].withRelated) - .toEqual(['users','things']); - }); - - }); - - describe('with an identifier for an item that does not exist', function() { - - beforeEach(function() { - var $this = this; - this.p = new MockPromise(); - var mockModel = MockModel.get('items', { - fetch: function() { - return new MockPromise([null], $this.p); - } - }); - this.mockNextFn = function() {}; - spyOn(this, 'mockNextFn'); - this.k.expose(mockModel); - var fn = this.mockApp.getHandlers['/items/:identifier']( - new MockRequest({ - params: { identifier: '1' }, - method: 'GET', - url: 'mock.com/items/1' - }), - new MockResponse(), - this.mockNextFn - ); - }); - - it('should call next with a not found error', function() { - expect(this.mockNextFn).toHaveBeenCalled(); - expect(this.mockNextFn.calls.argsFor(0)[0].message) - .toEqual('GET mock.com/items/1 failed: id = 1 not found'); - }); - - }); - - describe('when the call to fetch() throws an error', function() { - - beforeEach(function() { - var $this = this; - this.mockErr = new Error('mock error'); - var mockModel = MockModel.get('items', { - fetch: function() { - return new MockFailPromise($this.mockErr); - } - }); - this.mockNextFn = function() {}; - spyOn(this, 'mockNextFn'); - this.k.expose(mockModel); - var fn = this.mockApp.getHandlers['/items/:identifier']( - new MockRequest(), new MockResponse(), this.mockNextFn - ); - }); - - it('should call next with the error', function() { - expect(this.mockNextFn).toHaveBeenCalled(); - expect(this.mockNextFn.calls.argsFor(0)[0]) - .toBe(this.mockErr); - }); - - }); - - describe('with a before hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('before', 'GetItem', '/items/:identifier'); - - it('should pass model argument to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockModel.modelInstances[0]); - }); - - it('should call fetch', function() { - expect(this.mockFetch).toHaveBeenCalled(); - }); - - }); - - describe('that throws an error', function() { - - hookErrorTest('before', 'GetItem', '/items/:identifier'); - - it('should not call fetch', function() { - expect(this.mockFetch).not.toHaveBeenCalled(); - }); - - }); - - describe('that sends a response', function() { - - beforeEach(function() { - setupHook.call( - this, 'before', 'GetItem', '/items/:identifier', - function(req, res) { res.send(true); } - ); - }); - - it('should not call fetch', function() { - expect(this.mockFetch).not.toHaveBeenCalled(); - }); - - }); - - describe('that returns a promise', function() { - - hookPromiseTest('before', 'GetItem', '/items/:identifier'); - - it('should not call fetch', function() { - expect(this.mockFetch).not.toHaveBeenCalled(); - }); - - }); - - }); - - describe('with an after hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('after', 'GetItem', '/items/:identifier'); - - it('should pass the result of fetch to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockFetchResult); - }); - - }); - - describe('that throws an error', function() { - hookErrorTest('after', 'GetItem', '/items/:identifier', true); - }); - - describe('that sends a response', function() { - singleResponseHookTest('after', 'GetItem', '/items/:identifier'); - }); - - describe('that returns a promise', function() { - hookPromiseTest('after', 'GetItem', '/items/:identifier'); - }); - - }); - -}); \ No newline at end of file diff --git a/test/get.coffee b/test/get.coffee new file mode 100644 index 0000000..8fc3486 --- /dev/null +++ b/test/get.coffee @@ -0,0 +1,27 @@ + +chai = require('chai') +should = chai.should() + +module.exports = (g)-> + + addr = g.baseurl + + describe 'post routes', -> + + it 'must get all', (done) -> + chai.request(g.baseurl).get('/').end (err, res) -> + return done(err) if err + res.should.have.status(200) + res.should.be.json + res.body[0].name.should.eql 'gandalfek' + done() + return + + it 'must get gandalf', (done) -> + chai.request(g.baseurl).get("/#{g.gandalfID}").end (err, res) -> + return done(err) if err + res.should.have.status(200) + res.should.be.json + res.body.name.should.eql 'gandalfek' + done() + return diff --git a/test/helpers/helpers.js b/test/helpers/helpers.js deleted file mode 100644 index 9f6174b..0000000 --- a/test/helpers/helpers.js +++ /dev/null @@ -1,13 +0,0 @@ - -global.requireKalamata = function() { - deleteFromRequireCache('kalamata/src/index.js'); - return require('../../src/index.js'); -} - -global.deleteFromRequireCache = function(filePath) { - for(var n in require.cache) { - if(n.indexOf(filePath) != -1) { - delete require.cache[n]; - } - } -} diff --git a/test/helpers/hook-test-helpers.js b/test/helpers/hook-test-helpers.js deleted file mode 100644 index 432efcb..0000000 --- a/test/helpers/hook-test-helpers.js +++ /dev/null @@ -1,145 +0,0 @@ -global.singleResponseHookTest = function(prefix, postfix, endpoint, fn) { - - beforeEach(function() { - setupHook.call(this, prefix, postfix, endpoint, function(req, res) { - res.send(true); - }); - }); - - it('should only attempt to send the response once', function() { - expect(this.mockRes.send.calls.count()).toEqual(1); - expect(this.mockRes.send.calls.argsFor(0)[0]).toEqual(true); - }); - -}; - -global.hookExecTest = function(prefix, postfix, endpoint) { - - beforeEach(function() { - setupHook.call( - this, prefix, postfix, endpoint, function() {}); - }); - - it('should pass request and response arguments to the hook', - function() { - expect(this.hookFn).toHaveBeenCalled(); - expect(this.hookFn.calls.argsFor(0)[0]).toBe(this.mockReq); - expect(this.hookFn.calls.argsFor(0)[1]).toBe(this.mockRes); - }); - - it('should not throw and error', function() { - expect(this.error).toBeUndefined(); - }); - -}; - -global.hookErrorTest = function(prefix, postfix, endpoint, fromPromise) { - - beforeEach(function() { - setupHook.call( - this, prefix, postfix, endpoint, - function() { - throw new Error('mock hook error'); - } - ); - }); - - if(fromPromise) { - - it('should call next with the error', function() { - expect(this.mockNextFn).toHaveBeenCalled(); - }); - - } else { - - it('should throw an error', function() { - expect(this.error).toBeDefined(); - expect(this.error.message).toEqual('mock hook error'); - }); - - } - -} - -global.hookPromiseTest = function(prefix, postfix, endpoint) { - - beforeEach(function() { - var mockPromise = this.mockPromise = new MockPromise(); - spyOn(this.mockPromise, 'then'); - setupHook.call( - this, prefix, postfix, endpoint, - function(req, res) { - return mockPromise; - } - ); - }); - - it('should execute the promise callback', function() { - expect(this.mockPromise.then).toHaveBeenCalled() - }); - -} - -global.setupHook = function(prefix, postfix, endpoint, fn) { - this.hookFn = fn; - this.hookFnName = prefix + postfix; - var mockFetchAllResult = - this.mockFetchAllResult = { - toJSON: function() { - return [new (MockModel.get('items'))(), - new (MockModel.get('items'))(), - new (MockModel.get('items'))()]; - } - }; - var mockFetchResult = this.mockFetchResult = new (MockModel.get('items'))(); - var mockSaveResult = this.mockSaveResult = new (MockModel.get('items'))(); - this.mockFetchAll = function() { - return new MockPromise([mockFetchAllResult]); - }; - this.mockFetch = function() { - return new MockPromise([mockFetchResult]); - }; - this.mockSave = function() { - return new MockPromise([mockSaveResult]); - }; - spyOn(this, 'hookFn').and.callThrough(); - spyOn(this, 'mockFetchAll').and.callThrough(); - spyOn(this, 'mockFetch').and.callThrough(); - spyOn(this, 'mockSave').and.callThrough(); - spyOn(this.mockFetchResult, 'save').and.returnValue(this.mockFetchResult); - spyOn(this.mockFetchResult, 'destroy').and.returnValue(this.mockFetchResult); - this.mockModel = MockModel.get('items', { - fetchAll: this.mockFetchAll, - fetch: this.mockFetch, - save: this.mockSave - }); - this.mockRelatedModel = MockModel.get('things'); - this.k.expose(this.mockModel); - this.k.expose(this.mockRelatedModel); - this.k[this.hookFnName](this.hookFn); - this.mockReq = new MockRequest({ - params: { relation: 'things' } - }); - this.mockRes = new MockResponse(); - this.mockNextFn = function() {}; - spyOn(this, 'mockNextFn'); - spyOn(this.mockRes, 'send').and.callThrough(); - try { - this.mockApp[mockHandlerIndex[postfix]+'Handlers'][endpoint]( - this.mockReq, - this.mockRes, - this.mockNextFn - ); - } catch(err) { - this.error = err; - } -}; - -var mockHandlerIndex = { - 'GetItems': 'get', - 'GetItem': 'get', - 'CreateItem': 'post', - 'UpdateItem': 'put', - 'DeleteItem': 'delete', - 'GetThings': 'get' -}; \ No newline at end of file diff --git a/test/helpers/mock-app-helpers.js b/test/helpers/mock-app-helpers.js deleted file mode 100644 index 3bdbc14..0000000 --- a/test/helpers/mock-app-helpers.js +++ /dev/null @@ -1,29 +0,0 @@ - -global.MockApp = function() { - - function appMock() { - this.getHandlers = {}; - this.postHandlers = {}; - this.putHandlers = {}; - this.deleteHandlers = {}; - } - - appMock.prototype.use = function() {}; - - appMock.prototype.get = getMockListener('get'); - - appMock.prototype.post = getMockListener('post'); - - appMock.prototype.put = getMockListener('put'); - - appMock.prototype.delete = getMockListener('delete'); - - function getMockListener(type) { - return function(path, fn) { - this[type + 'Handlers'][path] = fn; - }; - } - - return new appMock(); - -}; diff --git a/test/helpers/mock-collection-helpers.js b/test/helpers/mock-collection-helpers.js deleted file mode 100644 index 9f04fe9..0000000 --- a/test/helpers/mock-collection-helpers.js +++ /dev/null @@ -1,6 +0,0 @@ - -global.MockCollection = { - - create: function() {} - -}; \ No newline at end of file diff --git a/test/helpers/mock-model-helpers.js b/test/helpers/mock-model-helpers.js deleted file mode 100644 index 8ce825c..0000000 --- a/test/helpers/mock-model-helpers.js +++ /dev/null @@ -1,63 +0,0 @@ - -global.MockModel = { - - get: function(tableName, modelMocks) { - - if(!modelMocks) modelMocks = {}; - - var m = function(attributes) { - m.modelInstances.push(this); - this.attributes = attributes; - }; - - m.modelInstances = []; - - m.forge = function() { - return { tableName: tableName }; - }; - - m.prototype.fetchAll = modelMocks.fetchAll || function(params) { - runWithRelatedFuncs(params); - return new MockPromise([MockCollection]); - }; - m.prototype.fetch = modelMocks.fetch || function(params) { - runWithRelatedFuncs(params); - return new MockPromise([this]); - }; - m.prototype.where = modelMocks.where || function() { - return this; - }; - m.prototype.load = modelMocks.load || function() { - return new MockPromise([this]); - }; - m.prototype.related = modelMocks.related || function() { - return MockCollection; - }; - m.prototype.save = modelMocks.save || function() { - return new MockPromise([this]); - }; - m.prototype.destroy = modelMocks.destroy || function() { - return new MockPromise([this]); - }; - m.prototype.set = modelMocks.set || function() { - return this; - } - m.prototype.toJSON = modelMocks.toJSON || function() { - return { name: 'mock toJSON() result' }; - } - - return m; - } - -}; - -function runWithRelatedFuncs(params) { - if(params && params.withRelated) { - for(var i in params.withRelated) { - var r = params.withRelated[i]; - for(var j in r) { - r[j](); - } - } - } -} diff --git a/test/helpers/mock-promise-helpers.js b/test/helpers/mock-promise-helpers.js deleted file mode 100644 index 4e5f422..0000000 --- a/test/helpers/mock-promise-helpers.js +++ /dev/null @@ -1,43 +0,0 @@ - -global.MockPromise = function(args, nextPromise) { - this.args = args; - this.nextPromise = nextPromise; -}; - -MockPromise.prototype.then = function(fn) { - if(!this.nextPromise) this.nextPromise = new MockPromise(this.args); - if(!this.thrownError && fn) { - try { - var returnVal = fn.apply(null, this.args); - if(returnVal) { - if(returnVal instanceof MockPromise) { - this.nextPromise = returnVal; - } else { - this.nextPromise = new MockPromise([returnVal]); - } - } - } catch(thrownError) { - this.thrownError = thrownError; - this.nextPromise.thrownError = thrownError; - } - } else if(this.thrownError) { - this.nextPromise.thrownError = this.thrownError; - } - return this.nextPromise; -}; - -MockPromise.prototype.catch = function(nextFn) { - if(nextFn && this.thrownError) nextFn(this.thrownError); -} - -global.MockFailPromise = function(error) { - this.error = error || new Error('promise failed'); -}; - -MockFailPromise.prototype.then = function() { - return this; -} - -MockFailPromise.prototype.catch = function(nextFn) { - if(nextFn) nextFn(this.error); -} diff --git a/test/helpers/mock-request-helpers.js b/test/helpers/mock-request-helpers.js deleted file mode 100644 index de37041..0000000 --- a/test/helpers/mock-request-helpers.js +++ /dev/null @@ -1,9 +0,0 @@ - -global.MockRequest = function(reqMocks) { - if(!reqMocks) reqMocks = {}; - this.query = reqMocks.query || {}; - this.params = reqMocks.params || {}; - this.body = reqMocks.body || undefined; - this.method = reqMocks.method || undefined; - this.url = reqMocks.url || undefined; -}; diff --git a/test/helpers/mock-response-helpers.js b/test/helpers/mock-response-helpers.js deleted file mode 100644 index 27767f1..0000000 --- a/test/helpers/mock-response-helpers.js +++ /dev/null @@ -1,9 +0,0 @@ - -global.MockResponse = MockResponse = function() { - this.headersSent = false; -}; - -MockResponse.prototype.send = function() { - this.headersSent = true; -}; - diff --git a/test/initialization-spec.js b/test/initialization-spec.js deleted file mode 100644 index 48c275d..0000000 --- a/test/initialization-spec.js +++ /dev/null @@ -1,186 +0,0 @@ -describe('initializing', function() { - - beforeEach(function() { - this.mockApp = new MockApp(); - spyOn(this.mockApp, 'get'); - spyOn(this.mockApp, 'post'); - spyOn(this.mockApp, 'put'); - spyOn(this.mockApp, 'delete'); - }) - - describe('with no options', function() { - - beforeEach(function() { - this.k = requireKalamata()(this.mockApp); - this.k.expose(MockModel.get('items')); - }); - - runEndpointConfigTests('/items'); - - }); - - describe('with an apiRoot option', function() { - - beforeEach(function() { - this.k = requireKalamata()(this.mockApp, { apiRoot: 'api' }); - this.k.expose(MockModel.get('items')); - }); - - runEndpointConfigTests('/api/items'); - - }); - - describe('with an apiRoot option that has more than one path segment', - function() { - - beforeEach(function() { - this.k = requireKalamata()(this.mockApp, { apiRoot: 'api/v1' }); - this.k.expose(MockModel.get('items')); - }); - - runEndpointConfigTests('/api/v1/items'); - - }); - - describe('with invalid options', function() { - - beforeEach(function() { - this.k = requireKalamata()(this.mockApp); - try { - this.k.expose(MockModel.get('items'), { mocked: 'invalid' }); - } catch(err) { - this.error = err; - } - }); - - it('should throw an error', function() { - expect(this.error.message).toEqual('Invalid option: mocked'); - }); - - }); - - describe('with a plural table name', function() { - - beforeEach(function() { - this.k = requireKalamata()(this.mockApp); - this.k.expose(MockModel.get('items')); - }); - - it('should set hook functions based on table name', function() { - expect(this.k.beforeGetItems).toBeDefined(); - expect(this.k.beforeGetRelatedItems).toBeDefined(); - expect(this.k.beforeGetItem).toBeDefined(); - expect(this.k.beforeCreateItem).toBeDefined(); - expect(this.k.beforeUpdateItem).toBeDefined(); - expect(this.k.beforeDeleteItem).toBeDefined(); - expect(this.k.afterGetItems).toBeDefined(); - expect(this.k.afterGetRelatedItems).toBeDefined(); - expect(this.k.afterGetItem).toBeDefined(); - expect(this.k.afterCreateItem).toBeDefined(); - expect(this.k.afterUpdateItem).toBeDefined(); - expect(this.k.afterDeleteItem).toBeDefined(); - }); - - }); - - describe('with a non-plural table name', function() { - - beforeEach(function() { - this.k = requireKalamata()(this.mockApp); - this.k.expose(MockModel.get('people')); - }); - - it('should set hook functions based on table name', function() { - expect(this.k.beforeGetPeopleCollection).toBeDefined(); - expect(this.k.beforeGetRelatedPeopleCollection).toBeDefined(); - expect(this.k.beforeGetPeople).toBeDefined(); - expect(this.k.beforeCreatePeople).toBeDefined(); - expect(this.k.beforeUpdatePeople).toBeDefined(); - expect(this.k.beforeDeletePeople).toBeDefined(); - expect(this.k.afterGetPeopleCollection).toBeDefined(); - expect(this.k.afterGetRelatedPeopleCollection).toBeDefined(); - expect(this.k.afterGetPeople).toBeDefined(); - expect(this.k.afterCreatePeople).toBeDefined(); - expect(this.k.afterUpdatePeople).toBeDefined(); - expect(this.k.afterDeletePeople).toBeDefined(); - }); - - }); - - describe('with custom model and collection names', function() { - - beforeEach(function() { - this.k = requireKalamata()(this.mockApp); - this.k.expose(MockModel.get('people'), { - modelName: 'person', - collectionName: 'people' - }); - }); - - it('should set hook functions based on table name', function() { - expect(this.k.beforeGetPeople).toBeDefined(); - expect(this.k.beforeGetRelatedPeople).toBeDefined(); - expect(this.k.beforeGetPerson).toBeDefined(); - expect(this.k.beforeCreatePerson).toBeDefined(); - expect(this.k.beforeUpdatePerson).toBeDefined(); - expect(this.k.beforeDeletePerson).toBeDefined(); - expect(this.k.afterGetPeople).toBeDefined(); - expect(this.k.afterGetRelatedPeople).toBeDefined(); - expect(this.k.afterGetPerson).toBeDefined(); - expect(this.k.afterCreatePerson).toBeDefined(); - expect(this.k.afterUpdatePerson).toBeDefined(); - expect(this.k.afterDeletePerson).toBeDefined(); - }); - - }); - - describe('with an apiRoot option that has a leading and trailing slash', - function() { - - beforeEach(function() { - this.k = requireKalamata()(this.mockApp, { apiRoot: '/api/' }); - this.k.expose(MockModel.get('items')); - }); - - runEndpointConfigTests('/api/items'); - - }); - - function runEndpointConfigTests(endpointPath) { - - it('should configure get endpoint for collection', function() { - expect(this.mockApp.get.calls.argsFor(0)[0]).toEqual(endpointPath); - }); - - it('should configure get endpoint for single item', function() { - expect(this.mockApp.get.calls.argsFor(1)[0]) - .toEqual(endpointPath + '/:identifier'); - }); - - it('should configure get endpoint for a related collection', function() { - expect(this.mockApp.get.calls.argsFor(2)[0]) - .toEqual(endpointPath + '/:identifier/:relation'); - }); - - it('should configure post endpoint for collection', function() { - expect(this.mockApp.post.calls.argsFor(0)[0]).toEqual(endpointPath); - }); - - it('should configure post endpoint for related collection', function() { - expect(this.mockApp.post.calls.argsFor(1)[0]) - .toEqual(endpointPath + '/:identifier/:relation'); - }); - - it('should configure put endpoint for single item', function() { - expect(this.mockApp.put.calls.argsFor(0)[0]) - .toEqual(endpointPath + '/:identifier'); - }); - - it('should configure delete endpoint for single item', function() { - expect(this.mockApp.delete.calls.argsFor(0)[0]) - .toEqual(endpointPath + '/:identifier'); - }); - - } - -}); \ No newline at end of file diff --git a/test/main.js b/test/main.js new file mode 100644 index 0000000..3ec58be --- /dev/null +++ b/test/main.js @@ -0,0 +1,66 @@ +const bodyParser = require('body-parser') +const express = require('express') +const chai = require('chai') +const chaiHttp = require('chai-http') +chai.use(chaiHttp) +const should = chai.should() + +const db = require('./db') +const Kalamata = require('../src/index') + +process.env.SERVER_SECRET = 'fhdsakjhfkjal' +port = process.env.PORT || 3333 +const g = {} + +// entry ... +describe('app', (suite) => { + + g.app = app = express() + app.use(bodyParser.json()) + g.db = db + + const k = Kalamata(db.models.User) + k.init_app(app) // create the REST routes + app.use((err, req, res, next) => { + const statusCode = parseInt(err.message) + res.status(isNaN(statusCode) ? 400 : statusCode).send(err) + console.log(err) + }) + + before((done) => { + g.db.migrate.latest() + .then(() => { + // init server + g.server = app.listen(port, (err) => { + if (err) return done(err) + done() + }) + }) + .catch(done) + }) + + after((done) => { + g.server.close() + done() + }) + + it('should exist', (done) => { + should.exist(g.app) + done() + }) + + // run the rest of tests + g.baseurl = `http://localhost:${port}` + + const submodules = [ + './post', + './put', + './get', + './del' + ] + submodules.forEach((i) => { + const SubMod = require(i) + SubMod(g) + }) + +}) diff --git a/test/migrations/create_tables.coffee b/test/migrations/create_tables.coffee new file mode 100644 index 0000000..f0f5084 --- /dev/null +++ b/test/migrations/create_tables.coffee @@ -0,0 +1,22 @@ +exports.up = (knex, Promise) -> + Promise.all [ + knex.schema.createTable 'users', (table) -> + table.increments() + table.text 'name' + table.dateTime('created_at').notNullable().defaultTo knex.fn.now() + table.dateTime('updated_at').notNullable().defaultTo knex.fn.now() + , + knex.schema.createTable 'things', (table) -> + table.increments() + table.integer('user_id').references('id').inTable 'users' + table.text 'type' + table.boolean 'deleted' + table.dateTime('created_at').notNullable().defaultTo knex.fn.now() + table.dateTime('updated_at').notNullable().defaultTo knex.fn.now() + ] + +exports.down = (knex, Promise) -> + Promise.all [ + knex.schema.dropTableIfExists('users') + knex.schema.dropTableIfExists('things') + ] diff --git a/test/post-relation-spec.js b/test/post-relation-spec.js deleted file mode 100644 index 6c4afa5..0000000 --- a/test/post-relation-spec.js +++ /dev/null @@ -1,271 +0,0 @@ -describe('POST request to create a relation', function() { - - beforeEach(function() { - var $this = this; - - this.mockApp = new MockApp(); - this.k = requireKalamata()(this.mockApp); - - this.mockCollection = { - create: function() {} - }; - this.mockModel = MockModel.get('items', { - related: function() { - return $this.mockCollection - } - }); - this.mockRelModel = MockModel.get('things'); - this.mockResponse = new MockResponse(); - - this.mockRequestOptions = { - body: { id: 1 }, - params: { relation: 'things' } - } - }); - - describe('to an existing model', function() { - - beforeEach(function() { - this.k.expose(this.mockModel); - this.k.expose(this.mockRelModel); - spyOn(this.mockCollection, 'create'); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.postHandlers['/items/:identifier/:relation']; - fn(new MockRequest(this.mockRequestOptions), this.mockResponse); - }); - - it('should call create on the related collection ', function() { - expect(this.mockCollection.create).toHaveBeenCalled(); - }); - - it('should pass the existing related model to the create call', - function() { - expect(this.mockCollection.create.calls.argsFor(0)[0]) - .toBe(this.mockRelModel.modelInstances[0]); - }); - - it('should send a response', function() { - expect(this.mockResponse.send).toHaveBeenCalled(); - }); - - }); - - describe('to an new model', function() { - - beforeEach(function() { - var $this = this; - this.mockFetchPromise = new MockPromise([{ - related: function() {} - }]); - this.mockModel = MockModel.get('items', { - fetch: function() { - return $this.mockFetchPromise; - } - }); - this.k.expose(this.mockModel); - this.k.expose(this.mockRelModel); - this.mockBody = {}; - spyOn(this.mockCollection, 'create'); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.postHandlers['/items/:identifier/:relation']; - this.mockRequestOptions.body = this.mockBody; - fn(new MockRequest(this.mockRequestOptions), this.mockResponse); - }); - - it('should not call create on the related collection ', function() { - expect(this.mockCollection.create).not.toHaveBeenCalled(); - }); - - it('should not send a response', function() { - expect(this.mockResponse.send).not.toHaveBeenCalled(); - }); - - it('should throw an error', function() { - expect(this.mockFetchPromise.thrownError.message) - .toBe('Create relationship failed: ' + - 'id property not provided'); - }); - - }); - - describe('to an existing model that is not found', function() { - - beforeEach(function() { - var $this = this; - this.mockFetchPromise = new MockPromise([null]); - this.mockRelModel = MockModel.get('things', { - fetch: function() { - return $this.mockFetchPromise; - } - }); - this.k.expose(this.mockModel); - this.k.expose(this.mockRelModel); - spyOn(this.mockCollection, 'create'); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.postHandlers['/items/:identifier/:relation']; - fn(new MockRequest(this.mockRequestOptions), this.mockResponse); - }); - - it('should not call create on the related collection', function() { - expect(this.mockCollection.create).not.toHaveBeenCalled(); - }); - - it('should throw an error', function() { - expect(this.mockFetchPromise.thrownError.message) - .toBe('Create relationship failed: ' + - 'Could not find things model {"id":1}'); - }); - - it('should not send a response', function() { - expect(this.mockResponse.send).not.toHaveBeenCalled(); - }); - - }); - - - describe('with a before hook', function() { - - describe('that returns nothing', function() { - - beforeEach(function() { - var $this = this; - this.k.expose(this.mockModel); - this.k.expose(this.mockRelModel); - this.relHookCalled; - this.k.beforeRelateThing(function() { - $this.relHookCalled = true; - }); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.postHandlers['/items/:identifier/:relation']; - fn(new MockRequest(this.mockRequestOptions), this.mockResponse); - }); - - it('should call the hook function', function() { - expect(this.relHookCalled).toBeTruthy(); - }); - - it('should send a response', function() { - expect(this.mockResponse.send).toHaveBeenCalled(); - }); - - }); - - describe('that throws an error', function() { - - beforeEach(function() { - var $this = this; - this.k.expose(this.mockModel); - this.k.expose(this.mockRelModel); - this.mockErr; - this.k.beforeRelateThing(function() { - throw new Error('mock error'); - }); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.postHandlers['/items/:identifier/:relation']; - try { - fn(new MockRequest(this.mockRequestOptions), this.mockResponse); - } catch(err) { - this.mockErr = err; - } - }); - - it('should call the hook function', function() { - expect(this.mockErr.message).toBe('mock error'); - }); - - it('should not send a response', function() { - expect(this.mockResponse.send).not.toHaveBeenCalled(); - }); - - }); - - }); - - describe('with a before hook on the base model', function() { - - describe('that returns nothing', function() { - - beforeEach(function() { - var $this = this; - this.k.expose(this.mockModel); - this.k.expose(this.mockRelModel); - this.relHookCalled; - this.k.beforeRelateItem(function() { - $this.relHookCalled = true; - }); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.postHandlers['/items/:identifier/:relation']; - fn(new MockRequest(this.mockRequestOptions), this.mockResponse); - }); - - it('should call the hook function', function() { - expect(this.relHookCalled).toBeTruthy(); - }); - - it('should send a response', function() { - expect(this.mockResponse.send).toHaveBeenCalled(); - }); - - }); - - }); - - describe('with an after hook', function() { - - describe('that returns nothing', function() { - - beforeEach(function() { - var $this = this; - this.k.expose(this.mockModel); - this.k.expose(this.mockRelModel); - this.relHookCalled; - this.k.afterRelateThing(function() { - $this.relHookCalled = true; - }); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.postHandlers['/items/:identifier/:relation']; - fn(new MockRequest(this.mockRequestOptions), this.mockResponse); - }); - - it('should call the hook function', function() { - expect(this.relHookCalled).toBeTruthy(); - }); - - it('should send a response', function() { - expect(this.mockResponse.send).toHaveBeenCalled(); - }); - - }); - - }); - - describe('with an after hook on the base model', function() { - - describe('that returns nothing', function() { - - beforeEach(function() { - var $this = this; - this.k.expose(this.mockModel); - this.k.expose(this.mockRelModel); - this.relHookCalled; - this.k.afterRelateItem(function() { - $this.relHookCalled = true; - }); - spyOn(this.mockResponse, 'send'); - var fn = this.mockApp.postHandlers['/items/:identifier/:relation']; - fn(new MockRequest(this.mockRequestOptions), this.mockResponse); - }); - - it('should call the hook function', function() { - expect(this.relHookCalled).toBeTruthy(); - }); - - it('should send a response', function() { - expect(this.mockResponse.send).toHaveBeenCalled(); - }); - - }); - - }); - -}); \ No newline at end of file diff --git a/test/post-spec.js b/test/post-spec.js deleted file mode 100644 index ef61585..0000000 --- a/test/post-spec.js +++ /dev/null @@ -1,156 +0,0 @@ -describe('POST request to create a new item', function() { - - beforeEach(function() { - this.mockApp = new MockApp(); - this.k = requireKalamata()(this.mockApp); - }); - - describe('and save succeeded', function() { - - beforeEach(function() { - var $this = this; - this.mockDataResponse = { mocked: true }; - this.k.expose(MockModel.get('items', { - save: function() { - return new MockPromise([{ - toJSON: function() { - return $this.mockDataResponse; - } - }]); - } - })); - this.mockResponse = new MockResponse(); - spyOn(this.mockResponse, 'send'); - this.mockApp.postHandlers['/items']( - new MockRequest(), - this.mockResponse - ); - }); - - it('should response with the identifier of the new item', function() { - expect(this.mockResponse.send.calls.argsFor(0)[0]) - .toEqual(this.mockDataResponse); - }); - - }); - - describe('and save failed', function() { - - beforeEach(function() { - this.mockBody = { name: 'mock' }; - this.mockNextFn = function() {}; - spyOn(this, 'mockNextFn'); - this.mockRequest = new MockRequest({ - body: this.mockBody - }); - this.k.expose(MockModel.get('items', { - save: function() { - return new MockFailPromise(); - } - })); - this.mockApp.postHandlers['/items']( - new MockRequest(), - new MockResponse(), - this.mockNextFn - ); - }); - - it('should call next with a Create Item failed error', function() { - expect(this.mockNextFn).toHaveBeenCalled(); - expect(this.mockNextFn.calls.argsFor(0)[0]).toEqual(new Error('Create Item ' + - JSON.stringify(this.mockBody) + ' failed')); - }); - - }); - - describe('with a before hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('before', 'CreateItem', '/items'); - - it('should pass model argument to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockModel.modelInstances[0]); - }); - - it('should call save', function() { - expect(this.mockSave).toHaveBeenCalled(); - }); - - }); - - describe('that throws an error', function() { - - hookErrorTest('before', 'CreateItem', '/items'); - - it('should not call save', function() { - expect(this.mockSave).not.toHaveBeenCalled(); - }); - - }); - - describe('that sends a response', function() { - - beforeEach(function() { - setupHook.call( - this, 'before', 'CreateItem', '/items', - function(req, res) { - res.send(true); - } - ); - }); - - it('should not call save', function() { - expect(this.mockSave).not.toHaveBeenCalled(); - }); - - }); - - describe('that returns a promise', function() { - - hookPromiseTest('before', 'CreateItem', '/items'); - - it('should not call save', function() { - expect(this.mockSave).not.toHaveBeenCalled(); - }); - - }); - - }); - - describe('with an after hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('after', 'CreateItem', '/items'); - - beforeEach(function() { - setupHook.call( - this, 'after', 'CreateItem', '/items', - function() {} - ); - }); - - it('should pass the result of save to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockSaveResult); - }); - - }); - - describe('that throws an error', function() { - hookErrorTest('after', 'CreateItem', '/items', true); - }); - - describe('that sends a response', function() { - singleResponseHookTest('after', 'CreateItem', '/items'); - }); - - describe('that returns a promise', function() { - hookPromiseTest('after', 'CreateItem', '/items'); - }); - - }); - -}); \ No newline at end of file diff --git a/test/post.coffee b/test/post.coffee new file mode 100644 index 0000000..58b30ce --- /dev/null +++ b/test/post.coffee @@ -0,0 +1,33 @@ + +chai = require('chai') +should = chai.should() + +module.exports = (g)-> + + addr = g.baseurl + + describe 'post routes', -> + + it 'must create user', (done) -> + chai.request(g.baseurl) + .post('/') + .send({ name: 'gandalf' }) + .end (err, res) -> + return done(err) if err + res.should.have.status(201) + res.should.be.json + res.body.name.should.eql 'gandalf' + g.gandalfID = res.body.id + done() + return + + # it 'must add a tool to gandalf', (done) -> + # chai.request(g.baseurl) + # .post("#{g.gandalfID}/things") + # .send({ type: 'magicwand' }) + # .end (err, res) -> + # return done(err) if err + # res.should.have.status(200) + # res.should.be.json + # res.body.type.should.eql 'magicwand' + # done() diff --git a/test/put-spec.js b/test/put-spec.js deleted file mode 100644 index bff63f3..0000000 --- a/test/put-spec.js +++ /dev/null @@ -1,171 +0,0 @@ -describe('PUT request to update an item', function() { - - beforeEach(function() { - this.mockApp = new MockApp(); - this.k = requireKalamata()(this.mockApp); - }); - - describe('for an item that exists', function() { - - beforeEach(function() { - var $this = this; - this.mockRequest = new MockRequest({ - params: { identifier: '1' }, - body: { name: 'mock' } - }); - this.mockResponse = new MockResponse(); - this.mockFetchedModel = new (MockModel.get('items'))(); - this.mockModel = MockModel.get('items', { - fetch: function() { - return new MockPromise([$this.mockFetchedModel]); - } - }); - spyOn(this.mockFetchedModel, 'set'); - spyOn(this.mockFetchedModel, 'save'); - spyOn(this.mockResponse, 'send'); - this.k.expose(this.mockModel); - this.mockApp.putHandlers['/items/:identifier']( - this.mockRequest, - this.mockResponse - ); - }); - - it('should set attributes on the model that will be fetched', function() { - expect(this.mockModel.modelInstances[0].attributes) - .toEqual({ id : '1' }); - }); - - it('should set req.body properties on the model', function() { - expect(this.mockFetchedModel.set.calls.argsFor(0)[0].name) - .toEqual('mock'); - }); - - it('should call save() on the model', function() { - expect(this.mockFetchedModel.save).toHaveBeenCalled(); - }); - - it('should respond with the model converted to JSON', function() { - expect(this.mockResponse.send.calls.argsFor(0)[0].name) - .toEqual('mock toJSON() result'); - }); - - }); - - describe('for an item that does not exist', function() { - - beforeEach(function() { - var $this = this; - this.mockNextFn = function() {}; - spyOn(this, 'mockNextFn'); - this.p = new MockPromise(); - this.mockParams = { identifier: '1' }; - this.k.expose(MockModel.get('items', { - fetch: function() { - return $this.p; - } - })); - this.mockApp.putHandlers['/items/:identifier']( - new MockRequest({ - params: this.mockParams, - method: 'PUT', - url: 'mock.com/items/1' - }), - new MockResponse(), - this.mockNextFn - ); - }); - - it('should call next with a not found error', function() { - expect(this.mockNextFn).toHaveBeenCalled(); - expect(this.mockNextFn.calls.argsFor(0)[0].message) - .toEqual('PUT mock.com/items/1 failed: id = 1 not found'); - }); - - }); - - describe('with a before hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('before', 'UpdateItem', '/items/:identifier'); - - it('should pass the fetch result argument to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockFetchResult); - }); - - it('should call save on the fetch result', function() { - expect(this.mockFetchResult.save).toHaveBeenCalled(); - }); - - }); - - describe('that throws an error', function() { - - hookErrorTest('before', 'UpdateItem', '/items/:identifier', true); - - it('should not call save on the fetch result', function() { - expect(this.mockFetchResult.save).not.toHaveBeenCalled(); - }); - - }); - - describe('that sends a response', function() { - - beforeEach(function() { - setupHook.call( - this, 'before', 'UpdateItem', '/items/:identifier', - function(req, res) { res.send(true); } - ); - }); - - it('should not call save on the fetch result', function() { - expect(this.mockFetchResult.save).not.toHaveBeenCalled(); - }); - - it('should not throw an error', function() { - expect(this.mockNextFn).not.toHaveBeenCalled(); - }); - - }); - - describe('that returns a promise', function() { - - hookPromiseTest('before', 'UpdateItem', '/items/:identifier'); - - it('should not call save on the fetch result', function() { - expect(this.mockFetchResult.save).not.toHaveBeenCalled(); - }); - - }); - - }); - - describe('with an after hook', function() { - - describe('that runs without executing any code', function() { - - hookExecTest('after', 'UpdateItem', '/items/:identifier'); - - it('should pass the fetch result to the hook', function() { - expect(this.hookFn.calls.argsFor(0)[2]) - .toBe(this.mockFetchResult); - }); - - }); - - describe('that throws an error', function() { - hookErrorTest('after', 'UpdateItem', '/items/:identifier', true); - }); - - describe('that sends a response', function() { - singleResponseHookTest('after', 'UpdateItem', '/items/:identifier'); - }); - - describe('that returns a promise', function() { - hookPromiseTest('after', 'UpdateItem', '/items/:identifier'); - }); - - }); - -}); \ No newline at end of file diff --git a/test/put.coffee b/test/put.coffee new file mode 100644 index 0000000..1f338e7 --- /dev/null +++ b/test/put.coffee @@ -0,0 +1,33 @@ + +chai = require('chai') +should = chai.should() + +module.exports = (g)-> + + addr = g.baseurl + + describe 'put routes', -> + + it 'must update user', (done) -> + chai.request(g.baseurl) + .put("/#{g.gandalfID}") + .send({ name: 'gandalfek' }) + .end (err, res) -> + return done(err) if err + res.should.have.status(200) + res.should.be.json + res.body.name.should.eql 'gandalfek' + res.body.id.should.eql g.gandalfID + done() + return + + # it 'must add a tool to gandalf', (done) -> + # chai.request(g.baseurl) + # .post("#{g.gandalfID}/things") + # .send({ type: 'magicwand' }) + # .end (err, res) -> + # return done(err) if err + # res.should.have.status(200) + # res.should.be.json + # res.body.type.should.eql 'magicwand' + # done() From b751b9c84a99dc1c860edc17e02b0eb6c6d3f16f Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Wed, 12 Apr 2017 14:08:14 +0200 Subject: [PATCH 02/27] fetch require option used --- src/index.js | 5 +---- test/main.js | 3 +++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 709b9c0..b2f4583 100644 --- a/src/index.js +++ b/src/index.js @@ -45,11 +45,8 @@ module.exports = function(model, opts) { function _fetch_middleware(req, res, next) { var mod = new model({id: req.params.id}) - mod.fetch() + mod.fetch({require: true}) .then(function(fetched) { - if(!fetched) { - return next(new Error(404)) - } req.fetched = fetched next() }) diff --git a/test/main.js b/test/main.js index 3ec58be..8bdf766 100644 --- a/test/main.js +++ b/test/main.js @@ -22,6 +22,9 @@ describe('app', (suite) => { const k = Kalamata(db.models.User) k.init_app(app) // create the REST routes app.use((err, req, res, next) => { + if (err.message === 'EmptyResponse') { + return res.status(404).send(err) + } const statusCode = parseInt(err.message) res.status(isNaN(statusCode) ? 400 : statusCode).send(err) console.log(err) From 067167c766370377f6c7d48ad3e9404f42bb828a Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Thu, 13 Apr 2017 07:21:19 +0200 Subject: [PATCH 03/27] relations get, post --- src/index.js | 29 ++++++++++++++++++++++++++++- test/db.coffee | 10 ++++++---- test/main.js | 1 + test/relations.coffee | 30 ++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 test/relations.coffee diff --git a/src/index.js b/src/index.js index b2f4583..985cd70 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,11 @@ module.exports = function(model, opts) { next() } + function _get_relation_middleware(req, res, next) { + res.json(req.fetched.related(req.params.relation)) + next() + } + function _list_middleware(req, res, next) { let mod = new model() if(req.listquery) { @@ -43,9 +48,28 @@ module.exports = function(model, opts) { .catch(next) } + function _create_relation_middleware(req, res, next) { + const relation = req.fetched.related(req.params.relation) + // for hasMany relations + const newitem = new relation.model(req.body) + relation.create(newitem) + .then(function(savedModel) { + req.savedModel = savedModel + res.status(201).json(savedModel) + next() + }) + .catch(next) + } + function _fetch_middleware(req, res, next) { var mod = new model({id: req.params.id}) - mod.fetch({require: true}) + const fetchopts = { + require: true + } + if (req.params.relation) { + fetchopts.withRelated = [req.params.relation] + } + mod.fetch(fetchopts) .then(function(fetched) { req.fetched = fetched next() @@ -76,6 +100,9 @@ module.exports = function(model, opts) { app.post('/', _create_middleware) app.put('/:id', _fetch_middleware, _update_middleware) app.delete('/:id', _fetch_middleware, _delete_middleware) + // relations + app.get('/:id/:relation', _fetch_middleware, _get_relation_middleware) + app.post('/:id/:relation', _fetch_middleware, _create_relation_middleware) } return { diff --git a/test/db.coffee b/test/db.coffee index bb09331..6986b67 100644 --- a/test/db.coffee +++ b/test/db.coffee @@ -11,14 +11,16 @@ debugopts = knex = Knex(debugopts) bookshelf = require('bookshelf')(knex) +Thing = bookshelf.Model.extend + tableName: 'things' + User = bookshelf.Model.extend tableName: 'users' - -Tool = bookshelf.Model.extend - tableName: 'tools' + tools: () -> + return this.hasMany(Thing) knex.models = User: User - Tool: Tool + Thing: Thing module.exports = knex diff --git a/test/main.js b/test/main.js index 8bdf766..4ff86ed 100644 --- a/test/main.js +++ b/test/main.js @@ -59,6 +59,7 @@ describe('app', (suite) => { './post', './put', './get', + './relations', './del' ] submodules.forEach((i) => { diff --git a/test/relations.coffee b/test/relations.coffee new file mode 100644 index 0000000..e0e756b --- /dev/null +++ b/test/relations.coffee @@ -0,0 +1,30 @@ + +chai = require('chai') +should = chai.should() + +module.exports = (g)-> + + addr = g.baseurl + + describe 'post routes', -> + + it 'must get gandalfs tools - []', (done) -> + chai.request(g.baseurl).get("/#{g.gandalfID}/tools").end (err, res) -> + return done(err) if err + res.should.have.status(200) + res.should.be.json + res.body.should.eql [] + done() + return + + it 'must add an axe to gandalf', (done) -> + chai.request(g.baseurl) + .post("/#{g.gandalfID}/tools") + .send({ type: 'axe' }) + .end (err, res) -> + return done(err) if err + res.should.have.status(201) + res.should.be.json + res.body.type.should.eql 'axe' + done() + return From af5144a5ad7f018b4ebb49387acec909d5fcc771 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Thu, 13 Apr 2017 07:21:33 +0200 Subject: [PATCH 04/27] fix --- test/del.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/test/del.coffee b/test/del.coffee index 98d042e..e5b4e06 100644 --- a/test/del.coffee +++ b/test/del.coffee @@ -27,3 +27,4 @@ module.exports = (g)-> return done('shall 404 but have not') if not err res.should.have.status(404) done() + return From a584957261d0a94a4dcc3e633411aad7836fa710 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Thu, 13 Apr 2017 08:18:50 +0200 Subject: [PATCH 05/27] load query for get routes --- src/index.js | 29 ++++++++++++++++++++++++----- test/get.coffee | 24 ++++++++++++++++++++++++ test/main.js | 1 - test/post.coffee | 30 ++++++++++++++++++++---------- test/relations.coffee | 30 ------------------------------ 5 files changed, 68 insertions(+), 46 deletions(-) delete mode 100644 test/relations.coffee diff --git a/src/index.js b/src/index.js index 985cd70..bdddd92 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,17 @@ module.exports = function(model, opts) { next() } + function _load_query(req, res, next) { + if(req.query.load) { + try { + req.loadquery = req.query.load.split(',') + } catch(err) { + return next(new Error('could not parse query.load: ' + req.query.load)) + } + } + next() + } + function _get_relation_middleware(req, res, next) { res.json(req.fetched.related(req.params.relation)) next() @@ -24,7 +35,11 @@ module.exports = function(model, opts) { if(req.listquery) { mod = mod.where(req.listquery) } - mod.fetchAll() + const fetchopts = {} + if (req.loadquery) { + fetchopts.withRelated = req.loadquery + } + mod.fetchAll(fetchopts) .then(function(collection) { res.json(collection) next() @@ -64,10 +79,14 @@ module.exports = function(model, opts) { function _fetch_middleware(req, res, next) { var mod = new model({id: req.params.id}) const fetchopts = { - require: true + require: true, + withRelated: [] } if (req.params.relation) { - fetchopts.withRelated = [req.params.relation] + fetchopts.withRelated.push(req.params.relation) + } + if (req.loadquery) { + fetchopts.withRelated = fetchopts.withRelated.concat(req.loadquery) } mod.fetch(fetchopts) .then(function(fetched) { @@ -95,8 +114,8 @@ module.exports = function(model, opts) { } function _init_app(app) { - app.get('/', _list_query, _list_middleware) - app.get('/:id', _fetch_middleware, _detail_middleware) + app.get('/', _list_query, _load_query, _list_middleware) + app.get('/:id', _load_query, _fetch_middleware, _detail_middleware) app.post('/', _create_middleware) app.put('/:id', _fetch_middleware, _update_middleware) app.delete('/:id', _fetch_middleware, _delete_middleware) diff --git a/test/get.coffee b/test/get.coffee index 8fc3486..4fa3e0f 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -25,3 +25,27 @@ module.exports = (g)-> res.body.name.should.eql 'gandalfek' done() return + + it 'must return gandalf with all tools (magicwand)', (done) -> + chai.request(g.baseurl) + .get("/#{g.gandalfID}?load=tools") + .end (err, res) -> + return done(err) if err + res.should.have.status(200) + res.should.be.json + res.body.tools.length.should.eql 1 + done() + return + + it 'must return all users with all tools', (done) -> + chai.request(g.baseurl) + .get("/?load=tools") + .end (err, res) -> + return done(err) if err + res.should.have.status(200) + res.should.be.json + res.body.length.should.eql 1 + res.body[0].tools.length.should.eql 1 + res.body[0].tools[0].type.should.eql 'magicwand' + done() + return diff --git a/test/main.js b/test/main.js index 4ff86ed..8bdf766 100644 --- a/test/main.js +++ b/test/main.js @@ -59,7 +59,6 @@ describe('app', (suite) => { './post', './put', './get', - './relations', './del' ] submodules.forEach((i) => { diff --git a/test/post.coffee b/test/post.coffee index 58b30ce..3cac905 100644 --- a/test/post.coffee +++ b/test/post.coffee @@ -21,13 +21,23 @@ module.exports = (g)-> done() return - # it 'must add a tool to gandalf', (done) -> - # chai.request(g.baseurl) - # .post("#{g.gandalfID}/things") - # .send({ type: 'magicwand' }) - # .end (err, res) -> - # return done(err) if err - # res.should.have.status(200) - # res.should.be.json - # res.body.type.should.eql 'magicwand' - # done() + it 'must get gandalfs tools - []', (done) -> + chai.request(g.baseurl).get("/#{g.gandalfID}/tools").end (err, res) -> + return done(err) if err + res.should.have.status(200) + res.should.be.json + res.body.should.eql [] + done() + return + + it 'must add an magicwand to gandalf', (done) -> + chai.request(g.baseurl) + .post("/#{g.gandalfID}/tools") + .send({ type: 'magicwand' }) + .end (err, res) -> + return done(err) if err + res.should.have.status(201) + res.should.be.json + res.body.type.should.eql 'magicwand' + done() + return diff --git a/test/relations.coffee b/test/relations.coffee deleted file mode 100644 index e0e756b..0000000 --- a/test/relations.coffee +++ /dev/null @@ -1,30 +0,0 @@ - -chai = require('chai') -should = chai.should() - -module.exports = (g)-> - - addr = g.baseurl - - describe 'post routes', -> - - it 'must get gandalfs tools - []', (done) -> - chai.request(g.baseurl).get("/#{g.gandalfID}/tools").end (err, res) -> - return done(err) if err - res.should.have.status(200) - res.should.be.json - res.body.should.eql [] - done() - return - - it 'must add an axe to gandalf', (done) -> - chai.request(g.baseurl) - .post("/#{g.gandalfID}/tools") - .send({ type: 'axe' }) - .end (err, res) -> - return done(err) if err - res.should.have.status(201) - res.should.be.json - res.body.type.should.eql 'axe' - done() - return From 9a595c2b01414353c372d1c2a5de43bfdeed06c6 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Thu, 13 Apr 2017 09:22:45 +0200 Subject: [PATCH 06/27] README update --- README.md | 341 ++++++++++++++---------------------------------------- 1 file changed, 84 insertions(+), 257 deletions(-) diff --git a/README.md b/README.md index 76b1fd2..dc1ab9b 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,116 @@ -Kalamata -======== +## Kalamata + [![Build Status](https://travis-ci.org/mikec/kalamata.svg?branch=master)](https://travis-ci.org/mikec/kalamata) Fully extensible Node.js REST API framework for [Bookshelf.js](http://bookshelfjs.org/) and [Express](http://expressjs.com/) Try the sample app [kalamata-sample](https://github.com/mikec/kalamata-sample) -Install ------------ -`cd` into your project and `npm install kalamata` +## Install + +`npm install kalamata` -What it is ------------ +## What it is -Kalamata helps you build REST APIs that run on Express. It creates some standard CRUD endpoints for you, and allows you to extend these with your application specific logic. +Kalamata helps you build REST APIs that run on Express. +It provide set of middlewares helping you to creat standard CRUD endpoints. +Combining them with your own middlewares allows you to customise these with your application specific logic. -### How it works +## How it works Lets say you have a Bookshelf model called `User` ```js -var User = bookshelf.Model.extend({ +const User = bookshelf.Model.extend({ tableName: 'users' -}); +}) ``` -You can use Kalamata to expose this model to your API +You can use Kalamata to expose this model to express app: ```js -// set up express and kalamata -var app = require('express')(); -var kalamata = require('kalamata'); -var api = kalamata(app); +// import express and kalamata +const express = require('express') +const kalamata = require('kalamata') + +const app = express() // create express app +// create middlewares for user model +const user_middlewarez = kalamata(User) -// expose the User model -api.expose(User); +// create standard CRUD endpoints the User model on app +user_middlewarez.init_app(app) // tell express to listen for incoming requests app.listen(8080, function() { - console.log('Server listening on port 8080'); -}); + console.log('Server listening on port 8080') +}) ``` -which will create these endpoints +which will create these endpoints (see [src/index.js](index.js:init_app)) -| Method | URL | Action | -| :----- | :------------| :---------------------------- | -| GET | `/users` | Get all users | -| GET | `/users/:id` | Get a user by id | -| POST | `/users` | Create a new user | -| PUT | `/users/:id` | Update an existing user | -| DELETE | `/users/:id` | Delete an existing user | +| Method | URL | Action | +| :----- | :------------ | :---------------------------- | +| GET | `/` | Get all users | +| GET | `/:id` | Get a user by id | +| POST | `/` | Create a new user | +| PUT | `/:id` | Update an existing user | +| DELETE | `/:id` | Delete an existing user | +| POST | `/:id/:relation` | Create a new relation | +| GET | `/:id/:relation` | Get a user's relation | -### Extending the default endpoints +### Customising the default endpoints -You can extend the default endpoints by modifying data before or after it is saved, using `before` and `after` hooks. These give you access to the Express request and response objects, and the Bookshelf model instance. +You can customise the default endpoints by swapping default middlewares with your own. Some examples: ```js -/* - * function executes on PUT `/users/:id` - * before updated user data is saved - */ -api.beforeUpdateUser(function(req, res, user) { - // set a propety before user is saved - user.set('updated_on', Date.now()); -}); - +// middleware that sets updated_on attribute before fetched item is saved +function _set_updated_on(req, res, next) { + // set a propety user is saved + req.fetched.set('updated_on', Date.now()) + next() // DON'T forget call next +} +// and use it after fetch and before update +app.put('/:id', user_middlewarez.fetch_middleware, _set_updated_on, user_middlewarez.update_middleware) ``` ```js -/* - * function executes on GET `/users` - * before the collection of users is fetched - */ -api.beforeGetUsers(function(req, res, user) { - // add a where clause to execute when fetching users - user.where({ deleted:false }); -}); - +// middleware that adds another contraint to fetching query +function _omit_deleted(req, res, user) { + req.listquery.deleted = false + next() // DON'T forget call next +} +app.get('/', user_middlewarez.list_query, _omit_deleted. user_middlewarez.list_middleware) ``` -```js -/* - * function executes on GET `/users/:id` - * after a user is fetched - */ -api.afterGetUser(function(req, res, user) { - if(!isAuthenticatedUser(user)) { - // override the default user data response - res.send({ error: 'access denied' }); - } -}); +If the server receives a `GET /5` request, but you don't want to respond with the user's data: +```js +function _deny_foruser5(req, res, next) { + if(req.fetched.get('id') == 5) { + // break the middleware chain by calling error middleware + return next({ error: "access denied" }) + } + next() // else call next middleware in chain +} +app.get('/:id', user_middlewarez.fetch_middleware, _deny_foruser5, user_middlewarez.detail_middleware) ``` -Configuring the API --------------------- - -##### Initialize `kalamata(expressApp, [options])` - -`apiRoot` option sets a prefix for all API endpoints - -> ```js -> /* -> * prefixes all endpoints with `/api/v1`, -> * for example `/api/v1/users` -> */ -> var api = kalamata(app, { apiRoot: '/api/v1' }); -> ``` - - -##### expose `expose(bookshelfModel, [options])` -`endpointName` option sets the name of the endpoint. +## Default Endpoints -> Defaults to the bookshelf model's tableName property. -> -> ```js -> // sets endpoints up on `/allusers` -> api.expose(User, { endpointName: 'allusers' }); -> ``` +Calling `init_app` on user_middlewares object will create a set of default CRUD endpoints. +Here are the default endpoints, assuming that `user_middlewares.init_app(app)` was called. -`identifier` option sets the name of the identifier param - -> Defaults to `id` -> -> ```js -> /* -> * when identifier is set to `user_id`, -> * a request to `/users/32` will fetch -> * the user with `user_id = 32` -> */ -> api.expose(User, { identifier: 'user_id' }); -> ``` - -`modelName` option sets the name of the model - -> Defaults to the endpoint name capitalized with the `s` removed (`users` -> `User`) - -`collectionName` options sets the name for a collection of model instances - -> Defaults to the endpoint name capitalized (`users` -> `Users`) - - -Default Endpoints -------------------- - -Calling `expose` on a model will create a set of default CRUD endpoints. Here are the default endpoints, assuming that `api.expose(User)` was called. - -#### GET `/users` +#### GET `/` Gets an array of users ```js /* - * GET `/users` + * GET `/` */ // response: @@ -169,25 +122,26 @@ Gets an array of users ``` ##### `where` parameter includes a where clause in the query -`/users?where={name:"user2"}` +`/?where={name:"user2"}` Expects the same parameters as the [bookshelf.js where method](http://bookshelfjs.org/#Model-where) ##### `load` parameter will load related models and include them in the response -`/users?load=orders,favorites` +`/?load=orders,favorites` -Expects a comma delimited string of relations. Calls the [bookshelf.js load method](http://bookshelfjs.org/#Model-load) method with an array of relations. +Expects a comma delimited string of relations. +Calls the [bookshelf.js load method](http://bookshelfjs.org/#Model-load) method with an array of relations. -#### GET `/users/:identifier` +#### GET `/:identifier` Gets a user ```js /* - * GET `/users/2` + * GET `/2` */ // response: @@ -197,17 +151,18 @@ Gets a user ##### `load` parameter will load related models and include them in the response -`/user/2?load=orders,favorites` +`/2?load=orders,favorites` -Expects a comma delimited string of relations. Calls the [bookshelf.js load method](http://bookshelfjs.org/#Model-load) method with an array of relations. +Expects a comma delimited string of relations. +Calls the [bookshelf.js load method](http://bookshelfjs.org/#Model-load) method with an array of relations. -#### POST `/users` +#### POST `/` Creates a user ```js /* - * POST `/users` { "name": "user4" } + * POST `/` { "name": "user4" } */ // response: @@ -216,13 +171,13 @@ Creates a user ``` -#### PUT `/users/:identifier` +#### PUT `/:identifier` Modifies a user ```js /* - * PUT `/users/2` { "name": "user2 MODIFIED" } + * PUT `/2` { "name": "user2 MODIFIED" } */ // response: @@ -231,13 +186,13 @@ Modifies a user ``` -#### DELETE `/users/:identifier` +#### DELETE `/:identifier` Deletes a user ```js /* - * DELETE `/users/3` + * DELETE `/3` */ // response: @@ -246,13 +201,13 @@ true ``` -#### GET `/users/:identifier/things` +#### GET `/:identifier/things` Gets an array of things related to a user ```js /* - * GET `/users/2/things` + * GET `/2/things` */ // response: @@ -261,144 +216,16 @@ Gets an array of things related to a user ``` -#### POST `/users/:identifier/things` +#### POST `/:identifier/things` Relates a thing to a user ```js /* - * POST `/users/2/things` { "id": "3" } + * POST `/2/things` { "id": "3" } */ // response: {} ``` - -Hooks -------- - -Hooks let you extend and override default endpoint behaviors. - -`before` hooks are executed before the default database action, such as fetch, save, or delete. `after` hooks are executed after all database actions are complete. - -Hook names are generated based on endpoint configurations. This list is based on a `/users` endpoint where `modelName = User` and `collectionName = Users` - -| Hook Name | Request | Arguments | -| :-------------------------| :------------------------ | :------------------------------------ | -| `beforeGetUsers` | GET `/users` | [req, res, userModel] | -| `afterGetUsers` | GET `/users` | [req, res, userCollection] | -| `beforeGetUser` | GET `/users/:id` | [req, res, userModel] | -| `afterGetUser` | GET `/users/:id` | [req, res, userModel] | -| `beforeCreateUser` | POST `/users` | [req, res, userModel] | -| `afterCreateUser` | POST `/users` | [req, res, userModel] | -| `beforeUpdateUser` | PUT `/users/:id` | [req, res, userModel] | -| `afterUpdateUser` | PUT `/users/:id` | [req, res, userModel] | -| `beforeDeleteUser` | DELETE `/users/:id` | [req, res, userModel] | -| `afterDeleteUser` | DELETE `/users/:id` | [req, res, userModel] | -| `beforeGetRelatedThings` | GET `/users/:id/things` | [req, res, thingModel] | -| `afterGetRelatedThings` | GET `/users/:id/things` | [req, res, thingsCollection] | -| `beforeRelatedThing` | POST `/users/:id/things` | [req, res, userModel] | -| `afterRelateThing` | POST `/users/:id/things` | [req, res, userModel, thingModel] | - -`req` and `res` are an Express [request](http://expressjs.com/4x/api.html#request) and [response](http://expressjs.com/4x/api.html#response) - -`userModel` is an instance of a [bookshelf model](http://bookshelfjs.org/#Model) - -`userCollection` is an instance of a [bookshelf collection](http://bookshelfjs.org/#Collection) - -### Adding hooks - -```js -api.beforeCreateUser(function(req, res, user) { - // do stuff before the user is created -}); - -api.afterCreateUser(function(req, res, user) { - // do stuff after the user is created -}); -``` - -### What hooks can do - -Because you have the full power of Express and Bookshelf within your hooks, you have total control over how the Kalamata endpoints behave. Here are some examples: - -#### Manipulating data - -If the server receives a `POST /users { "name":"Joey" }` request: - -```js -/* - * The user model can be manipulated before it is saved. - * - * When this hook is finished executing, - * `{ "name":"Joey McGee" }` will be saved - * - */ -api.beforeCreateUser(function(req, res, user) { - var userName = user.get('name'); - user.set({name:userName + ' McGee'}); -}); -``` - -```js -/* - * After the user is created, the response can be manipulated. - * - * When this hook is finished executing, the server will - * respond with `{ "name":"Joey", "lastName":"McGee" }` - * - * The changes to the user will not be saved, because this hook - * is executed after the user is saved - * - */ -api.afterCreateUser(function(req, res, user) { - var nameSplit = user.get('name').split(' '); - user.set({ - name: nameSplit[0], - lastName: nameSplit[1] - }); -}); -``` - -#### Cancelling default actions - -If the server receives a `GET /user/5` request, but you don't want to respond with the user's data: - -```js -/* - * Send a response from the before hook - * - * Once a response is sent, Kalamata will not execute - * any of the default actions, including after hooks. - * - */ -api.beforeGetUser(function(req, res, user) { - if(user.get('id') == 5) { - res.send({ error: "access denied" }); - } -}); -api.afterGetUser(function(req, res, user) { - // will not be executed on requests for `user/5` -}); - -``` - -#### Overriding default actions - -If the server receives a `DELETE /user/5` request, Kalamata will call `user.destroy()` by default. You can override this default behavior by returning a promise from the before hook: - -```js -/* - * Call a function that returns a promise, and have the - * hook function return the result of that promise - * - * Kalamata will not execute the default action, - * which in this case would have been `user.destroy()` - * - * Flag the user as deleted with a `deleted=true` property - */ -api.beforeDeleteUser(function(req, res, user) { - return user.save({ deleted: true }); -}); - From e48f632ce749b4b68b0a6e2b0ad45d914ed8463f Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 15 Apr 2017 08:23:42 +0200 Subject: [PATCH 07/27] rest of MW exported --- src/index.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/index.js b/src/index.js index bdddd92..bcb00be 100644 --- a/src/index.js +++ b/src/index.js @@ -126,6 +126,16 @@ module.exports = function(model, opts) { return { init_app: _init_app + list_query: _list_query, + load_query: _load_query, + get_relation_middleware: _get_relation_middleware, + list_middleware: _list_middleware, + detail_middleware: _detail_middleware, + create_middleware: _create_middleware, + create_relation_middleware: _create_relation_middleware, + fetch_middleware: _fetch_middleware, + update_middleware: _update_middleware, + delete_middleware: _delete_middleware } } From c2af9bbe2b582f30d7919ed7c99cdc52ec6d9e41 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 15 Apr 2017 08:25:51 +0200 Subject: [PATCH 08/27] fix --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index bcb00be..0d1430f 100644 --- a/src/index.js +++ b/src/index.js @@ -125,7 +125,7 @@ module.exports = function(model, opts) { } return { - init_app: _init_app + init_app: _init_app, list_query: _list_query, load_query: _load_query, get_relation_middleware: _get_relation_middleware, From 810c9be62ad0a33a799426d93ecafab7cf66c74b Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 15 Apr 2017 10:26:06 +0200 Subject: [PATCH 09/27] delete relation --- src/index.js | 15 +++++++++++++++ test/del.coffee | 25 ++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 0d1430f..f6cb57b 100644 --- a/src/index.js +++ b/src/index.js @@ -76,6 +76,19 @@ module.exports = function(model, opts) { .catch(next) } + function _delete_relation_middleware(req, res, next) { + const relation = req.fetched.related(req.params.relation) + relation.query({where: req.query}).fetch() + .then((found) => { + return found.invokeThen('destroy') + }) + .then((deleted) => { + res.status(200).send('deleted') + next() + }) + .catch(next) + } + function _fetch_middleware(req, res, next) { var mod = new model({id: req.params.id}) const fetchopts = { @@ -122,6 +135,7 @@ module.exports = function(model, opts) { // relations app.get('/:id/:relation', _fetch_middleware, _get_relation_middleware) app.post('/:id/:relation', _fetch_middleware, _create_relation_middleware) + app.delete('/:id/:relation', _fetch_middleware, _delete_relation_middleware) } return { @@ -133,6 +147,7 @@ module.exports = function(model, opts) { detail_middleware: _detail_middleware, create_middleware: _create_middleware, create_relation_middleware: _create_relation_middleware, + delete_relation_middleware: _delete_relation_middleware, fetch_middleware: _fetch_middleware, update_middleware: _update_middleware, delete_middleware: _delete_middleware diff --git a/test/del.coffee b/test/del.coffee index e5b4e06..6a4e407 100644 --- a/test/del.coffee +++ b/test/del.coffee @@ -4,10 +4,33 @@ should = chai.should() module.exports = (g)-> - addr = g.baseurl + r = chai.request(g.baseurl) describe 'delete routes', -> + it 'must delete relation (remove magicwand from gandalf)', (done) -> + # add another toold to gandalf + r.post("/#{g.gandalfID}/tools").send({ type: 'horse' }) + .then (res) -> + # vrify he has 2 tools + return r.get("/#{g.gandalfID}?load=tools") + .then (res) -> + res.should.have.status(200) + res.should.be.json + res.body.tools.length.should.eql 2 + # remove magicwand + return r.delete("/#{g.gandalfID}/tools?type=magicwand") + .then (res) -> + res.should.have.status(200) + # verify gandalf is toolless + return r.get("/#{g.gandalfID}?load=tools") + .then (res) -> + res.should.have.status(200) + res.should.be.json + res.body.tools.length.should.eql 1 + done() + .catch(done) + it 'must delete user', (done) -> chai.request(g.baseurl) .delete("/#{g.gandalfID}") From aa6467307bf84248b74262ed487b9ea1cadf4352 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 15 Apr 2017 22:07:01 +0200 Subject: [PATCH 10/27] typo --- test/get.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/get.coffee b/test/get.coffee index 4fa3e0f..f48a7fa 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -6,7 +6,7 @@ module.exports = (g)-> addr = g.baseurl - describe 'post routes', -> + describe 'get routes', -> it 'must get all', (done) -> chai.request(g.baseurl).get('/').end (err, res) -> From 1306ef63c71ce87c00056ecbe9cc09c803d8f26d Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sun, 16 Apr 2017 00:20:23 +0200 Subject: [PATCH 11/27] put relations --- src/index.js | 30 +++++++++++++++++++++++++----- test/del.coffee | 2 +- test/get.coffee | 2 +- test/post.coffee | 1 + test/put.coffee | 26 +++++++++++++------------- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/index.js b/src/index.js index f6cb57b..0bf0a42 100644 --- a/src/index.js +++ b/src/index.js @@ -77,13 +77,31 @@ module.exports = function(model, opts) { } function _delete_relation_middleware(req, res, next) { + req.foundrelated.invokeThen('destroy') + .then((deleted) => { + res.status(200).send('deleted') + next() + }) + .catch(next) + } + + function _update_relation_middleware(req, res, next) { + req.foundrelated.map((i) => { + i.set(req.body) // updated values + }) + req.foundrelated.invokeThen('save') + .then((saved) => { + res.status(200).send('saved') + next() + }) + .catch(next) + } + + function _load_related_middleware(req, res, next) { const relation = req.fetched.related(req.params.relation) relation.query({where: req.query}).fetch() .then((found) => { - return found.invokeThen('destroy') - }) - .then((deleted) => { - res.status(200).send('deleted') + req.foundrelated = found next() }) .catch(next) @@ -135,7 +153,8 @@ module.exports = function(model, opts) { // relations app.get('/:id/:relation', _fetch_middleware, _get_relation_middleware) app.post('/:id/:relation', _fetch_middleware, _create_relation_middleware) - app.delete('/:id/:relation', _fetch_middleware, _delete_relation_middleware) + app.put('/:id/:relation', _fetch_middleware, _load_related_middleware, _update_relation_middleware) + app.delete('/:id/:relation', _fetch_middleware, _load_related_middleware, _delete_relation_middleware) } return { @@ -148,6 +167,7 @@ module.exports = function(model, opts) { create_middleware: _create_middleware, create_relation_middleware: _create_relation_middleware, delete_relation_middleware: _delete_relation_middleware, + load_related_middleware: _load_related_middleware, fetch_middleware: _fetch_middleware, update_middleware: _update_middleware, delete_middleware: _delete_middleware diff --git a/test/del.coffee b/test/del.coffee index 6a4e407..383df76 100644 --- a/test/del.coffee +++ b/test/del.coffee @@ -19,7 +19,7 @@ module.exports = (g)-> res.should.be.json res.body.tools.length.should.eql 2 # remove magicwand - return r.delete("/#{g.gandalfID}/tools?type=magicwand") + return r.delete("/#{g.gandalfID}/tools?type=supermagicwand") .then (res) -> res.should.have.status(200) # verify gandalf is toolless diff --git a/test/get.coffee b/test/get.coffee index f48a7fa..a2f855a 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -46,6 +46,6 @@ module.exports = (g)-> res.should.be.json res.body.length.should.eql 1 res.body[0].tools.length.should.eql 1 - res.body[0].tools[0].type.should.eql 'magicwand' + res.body[0].tools[0].type.should.eql 'supermagicwand' done() return diff --git a/test/post.coffee b/test/post.coffee index 3cac905..bf5e8e3 100644 --- a/test/post.coffee +++ b/test/post.coffee @@ -39,5 +39,6 @@ module.exports = (g)-> res.should.have.status(201) res.should.be.json res.body.type.should.eql 'magicwand' + g.magicwandid = res.body.id done() return diff --git a/test/put.coffee b/test/put.coffee index 1f338e7..f055116 100644 --- a/test/put.coffee +++ b/test/put.coffee @@ -4,13 +4,12 @@ should = chai.should() module.exports = (g)-> - addr = g.baseurl + r = chai.request(g.baseurl) describe 'put routes', -> it 'must update user', (done) -> - chai.request(g.baseurl) - .put("/#{g.gandalfID}") + r.put("/#{g.gandalfID}") .send({ name: 'gandalfek' }) .end (err, res) -> return done(err) if err @@ -21,13 +20,14 @@ module.exports = (g)-> done() return - # it 'must add a tool to gandalf', (done) -> - # chai.request(g.baseurl) - # .post("#{g.gandalfID}/things") - # .send({ type: 'magicwand' }) - # .end (err, res) -> - # return done(err) if err - # res.should.have.status(200) - # res.should.be.json - # res.body.type.should.eql 'magicwand' - # done() + it 'must change magicwand to supermagicwand', () -> + return r.put("/#{g.gandalfID}/tools?id=#{g.magicwandid}") + .send({ type: 'supermagicwand' }) + .then (res) -> + res.should.have.status(200) + # verify that gandalf has now supermagicwand + return chai.request(g.baseurl).get("/#{g.gandalfID}?load=tools") + .then (res) -> + res.should.have.status(200) + res.should.be.json + res.body.tools[0].type.should.eql 'supermagicwand' From c0da111dea7c13ffdc9a05cebd68463fb0c540bc Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sun, 16 Apr 2017 08:00:43 +0200 Subject: [PATCH 12/27] update_relation_middleware forgotten export --- src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.js b/src/index.js index 0bf0a42..fde9a12 100644 --- a/src/index.js +++ b/src/index.js @@ -168,6 +168,7 @@ module.exports = function(model, opts) { create_relation_middleware: _create_relation_middleware, delete_relation_middleware: _delete_relation_middleware, load_related_middleware: _load_related_middleware, + update_relation_middleware: _update_relation_middleware, fetch_middleware: _fetch_middleware, update_middleware: _update_middleware, delete_middleware: _delete_middleware From c44843170da5865f9c815bef08c427fc87769d1f Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sun, 16 Apr 2017 08:12:45 +0200 Subject: [PATCH 13/27] updated relations jsoned back --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index fde9a12..446da31 100644 --- a/src/index.js +++ b/src/index.js @@ -91,7 +91,7 @@ module.exports = function(model, opts) { }) req.foundrelated.invokeThen('save') .then((saved) => { - res.status(200).send('saved') + res.status(200).json(saved) next() }) .catch(next) From 78d7c047f696560c17c3c09dd25e266405ad0774 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sun, 16 Apr 2017 12:39:13 +0200 Subject: [PATCH 14/27] listquery and loadquery always in req --- src/index.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/index.js b/src/index.js index 446da31..a9f23ad 100644 --- a/src/index.js +++ b/src/index.js @@ -4,23 +4,19 @@ module.exports = function(model, opts) { opts = opts || {} function _list_query(req, res, next) { - if(req.query.where) { - try { - req.listquery = parseJSON(req.query.where) - } catch(err) { - return next(new Error('Could not parse JSON: ' + req.query.where)) - } + try { + req.listquery = req.query.where ? parseJSON(req.query.where) : {} + } catch(err) { + return next(new Error('Could not parse JSON: ' + req.query.where)) } next() } function _load_query(req, res, next) { - if(req.query.load) { - try { - req.loadquery = req.query.load.split(',') - } catch(err) { - return next(new Error('could not parse query.load: ' + req.query.load)) - } + try { + req.loadquery = req.query.load ? req.query.load.split(',') : [] + } catch(err) { + return next(new Error('could not parse query.load: ' + req.query.load)) } next() } From fd46a9e244e03c851cc5b161ef8113f7349859b5 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Tue, 18 Apr 2017 09:03:16 +0200 Subject: [PATCH 15/27] paging --- src/index.js | 40 +++++++++++++++++++++++++++++++++++----- test/db.coffee | 1 + test/del.coffee | 17 +++-------------- test/get.coffee | 24 ++++++++++++++++++++++-- test/post.coffee | 12 ++++++++++-- 5 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/index.js b/src/index.js index a9f23ad..713f993 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,31 @@ module.exports = function(model, opts) { next() } + function _extract_paging(req) { // default pageinfo extractor + return { + page: req.query.page, + pagesize: req.query.pagesize + } + } + + function _paging_query(req, res, next) { + const pinfo = opts.pageinfo_extractor ? + opts.pageinfo_extractor(req) : _extract_paging(req) + const page = parseInt(pinfo.page) + if (pinfo.page && (isNaN(page) || page <= 0)) { + return next(new Error('wrong page')) + } + const pagesize = parseInt(pinfo.pagesize) + if (pinfo.pagesize && (isNaN(pagesize) || pagesize <= 0)) { + return next(new Error('wrong pagesize')) + } + if (pinfo.page) { + req.page = page + req.pagesize = pagesize + } + next() + } + function _get_relation_middleware(req, res, next) { res.json(req.fetched.related(req.params.relation)) next() @@ -29,14 +54,18 @@ module.exports = function(model, opts) { function _list_middleware(req, res, next) { let mod = new model() if(req.listquery) { - mod = mod.where(req.listquery) + mod = mod.query({where: req.listquery}) } const fetchopts = {} if (req.loadquery) { fetchopts.withRelated = req.loadquery } - mod.fetchAll(fetchopts) - .then(function(collection) { + if (req.page) { + fetchopts.page = req.page + fetchopts.pageSize = req.pagesize + } + const fetchMethod = req.page === undefined ? mod.fetchAll : mod.fetchPage + fetchMethod.bind(mod)(fetchopts).then(function(collection) { res.json(collection) next() }) @@ -141,7 +170,7 @@ module.exports = function(model, opts) { } function _init_app(app) { - app.get('/', _list_query, _load_query, _list_middleware) + app.get('/', _list_query, _paging_query, _load_query, _list_middleware) app.get('/:id', _load_query, _fetch_middleware, _detail_middleware) app.post('/', _create_middleware) app.put('/:id', _fetch_middleware, _update_middleware) @@ -167,7 +196,8 @@ module.exports = function(model, opts) { update_relation_middleware: _update_relation_middleware, fetch_middleware: _fetch_middleware, update_middleware: _update_middleware, - delete_middleware: _delete_middleware + delete_middleware: _delete_middleware, + paging_query: _paging_query } } diff --git a/test/db.coffee b/test/db.coffee index 6986b67..fced406 100644 --- a/test/db.coffee +++ b/test/db.coffee @@ -10,6 +10,7 @@ debugopts = knex = Knex(debugopts) bookshelf = require('bookshelf')(knex) +bookshelf.plugin('pagination') Thing = bookshelf.Model.extend tableName: 'things' diff --git a/test/del.coffee b/test/del.coffee index 383df76..ba2a550 100644 --- a/test/del.coffee +++ b/test/del.coffee @@ -8,18 +8,9 @@ module.exports = (g)-> describe 'delete routes', -> - it 'must delete relation (remove magicwand from gandalf)', (done) -> - # add another toold to gandalf - r.post("/#{g.gandalfID}/tools").send({ type: 'horse' }) - .then (res) -> - # vrify he has 2 tools - return r.get("/#{g.gandalfID}?load=tools") - .then (res) -> - res.should.have.status(200) - res.should.be.json - res.body.tools.length.should.eql 2 - # remove magicwand - return r.delete("/#{g.gandalfID}/tools?type=supermagicwand") + it 'must delete relation (remove magicwand from gandalf)', () -> + # remove magicwand + return r.delete("/#{g.gandalfID}/tools?type=supermagicwand") .then (res) -> res.should.have.status(200) # verify gandalf is toolless @@ -28,8 +19,6 @@ module.exports = (g)-> res.should.have.status(200) res.should.be.json res.body.tools.length.should.eql 1 - done() - .catch(done) it 'must delete user', (done) -> chai.request(g.baseurl) diff --git a/test/get.coffee b/test/get.coffee index a2f855a..729de04 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -33,7 +33,7 @@ module.exports = (g)-> return done(err) if err res.should.have.status(200) res.should.be.json - res.body.tools.length.should.eql 1 + res.body.tools.length.should.eql 2 done() return @@ -45,7 +45,27 @@ module.exports = (g)-> res.should.have.status(200) res.should.be.json res.body.length.should.eql 1 - res.body[0].tools.length.should.eql 1 + res.body[0].tools.length.should.eql 2 res.body[0].tools[0].type.should.eql 'supermagicwand' done() return + + it 'must list 2nd page of users', (done) -> + # add another user + chai.request(g.baseurl) + .post('/') + .send({ name: 'saruman' }) + .end (err, res) -> + return done(err) if err + res.should.have.status(201) + res.should.be.json + g.sarumanID = res.body.id + # list 2nd page + chai.request(g.baseurl).get('/?page=2&pagesize=1').end (err, res) -> + return done(err) if err + res.should.have.status(200) + res.should.be.json + res.body.length.should.eql 1 + res.body[0].name.should.eql 'saruman' + done() + return diff --git a/test/post.coffee b/test/post.coffee index bf5e8e3..93859df 100644 --- a/test/post.coffee +++ b/test/post.coffee @@ -30,7 +30,7 @@ module.exports = (g)-> done() return - it 'must add an magicwand to gandalf', (done) -> + it 'must add an magicwand and hat to gandalf', (done) -> chai.request(g.baseurl) .post("/#{g.gandalfID}/tools") .send({ type: 'magicwand' }) @@ -40,5 +40,13 @@ module.exports = (g)-> res.should.be.json res.body.type.should.eql 'magicwand' g.magicwandid = res.body.id - done() + chai.request(g.baseurl) + .post("/#{g.gandalfID}/tools") + .send({ type: 'hat' }) + .end (err, res) -> + return done(err) if err + res.should.have.status(201) + res.should.be.json + res.body.type.should.eql 'hat' + done() return From f395cfcf72cb70f52b085f451624f4cd47389372 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Tue, 18 Apr 2017 16:31:22 +0200 Subject: [PATCH 16/27] loading related vol1 --- package.json | 4 ++-- src/index.js | 32 +++++++++++++++----------------- test/db.coffee | 4 +++- test/get.coffee | 9 +++++++++ 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 760e585..8256c8e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ ], "peerDependencies": { "express": "^4.10.0", - "bookshelf": "^0.10" + "body-parser": "^1.9.0", + "bookshelf": "^0.10.3" }, "devDependencies": { "chai": "^3.5.0", @@ -30,7 +31,6 @@ "sqlite3": "^3.1.8" }, "dependencies": { - "body-parser": "^1.9.0", "bluebird": "^3" }, "scripts": { diff --git a/src/index.js b/src/index.js index 713f993..67e2c66 100644 --- a/src/index.js +++ b/src/index.js @@ -46,8 +46,8 @@ module.exports = function(model, opts) { next() } - function _get_relation_middleware(req, res, next) { - res.json(req.fetched.related(req.params.relation)) + function _get_related_middleware(req, res, next) { + res.json(req.fetchedrelated) // just JSON back req.fetchedrelated next() } @@ -122,11 +122,13 @@ module.exports = function(model, opts) { .catch(next) } - function _load_related_middleware(req, res, next) { + function _fetch_related_middleware(req, res, next) { const relation = req.fetched.related(req.params.relation) - relation.query({where: req.query}).fetch() - .then((found) => { - req.foundrelated = found + const q = relation.query({where: req.query.where || {}}) + const fetchopts = (req.page) ? {page: req.page, pageSize: req.pagesize} : {} + const p = (req.page !== undefined) ? q.fetchPage(fetchopts) : q.fetch(fetchopts) + p.then((found) => { + req.fetchedrelated = found next() }) .catch(next) @@ -135,14 +137,10 @@ module.exports = function(model, opts) { function _fetch_middleware(req, res, next) { var mod = new model({id: req.params.id}) const fetchopts = { - require: true, - withRelated: [] - } - if (req.params.relation) { - fetchopts.withRelated.push(req.params.relation) + require: true } if (req.loadquery) { - fetchopts.withRelated = fetchopts.withRelated.concat(req.loadquery) + fetchopts.withRelated = req.loadquery } mod.fetch(fetchopts) .then(function(fetched) { @@ -176,23 +174,23 @@ module.exports = function(model, opts) { app.put('/:id', _fetch_middleware, _update_middleware) app.delete('/:id', _fetch_middleware, _delete_middleware) // relations - app.get('/:id/:relation', _fetch_middleware, _get_relation_middleware) + app.get('/:id/:relation', _fetch_middleware, _paging_query, _fetch_related_middleware, _get_related_middleware) app.post('/:id/:relation', _fetch_middleware, _create_relation_middleware) - app.put('/:id/:relation', _fetch_middleware, _load_related_middleware, _update_relation_middleware) - app.delete('/:id/:relation', _fetch_middleware, _load_related_middleware, _delete_relation_middleware) + app.put('/:id/:relation', _fetch_middleware, _fetch_related_middleware, _update_relation_middleware) + app.delete('/:id/:relation', _fetch_middleware, _fetch_related_middleware, _delete_relation_middleware) } return { init_app: _init_app, list_query: _list_query, load_query: _load_query, - get_relation_middleware: _get_relation_middleware, + get_related_middleware: _get_related_middleware, list_middleware: _list_middleware, detail_middleware: _detail_middleware, create_middleware: _create_middleware, create_relation_middleware: _create_relation_middleware, delete_relation_middleware: _delete_relation_middleware, - load_related_middleware: _load_related_middleware, + fetch_related_middleware: _fetch_related_middleware, update_relation_middleware: _update_relation_middleware, fetch_middleware: _fetch_middleware, update_middleware: _update_middleware, diff --git a/test/db.coffee b/test/db.coffee index fced406..14aa819 100644 --- a/test/db.coffee +++ b/test/db.coffee @@ -14,11 +14,13 @@ bookshelf.plugin('pagination') Thing = bookshelf.Model.extend tableName: 'things' + user: () -> + return this.belongsTo(User) User = bookshelf.Model.extend tableName: 'users' tools: () -> - return this.hasMany(Thing) + return this.hasMany(Thing, 'user_id') knex.models = User: User diff --git a/test/get.coffee b/test/get.coffee index 729de04..5f04008 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -69,3 +69,12 @@ module.exports = (g)-> res.body[0].name.should.eql 'saruman' done() return + + it 'must list 2nd page of gandalf thigs', () -> + return chai.request(g.baseurl) + .get("/#{g.gandalfID}/tools/?page=2&pagesize=1") + .then (res) -> + res.should.have.status(200) + res.should.be.json + res.body.length.should.eql 1 + res.body[0].name.should.eql 'saruman' From d8bbbef354b97f9bb62f5fd3c18e17a3105b1224 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Fri, 21 Apr 2017 21:35:21 +0200 Subject: [PATCH 17/27] paging vol2 --- src/index.js | 13 +++++++------ test/get.coffee | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/index.js b/src/index.js index 67e2c66..a7c3e65 100644 --- a/src/index.js +++ b/src/index.js @@ -102,7 +102,7 @@ module.exports = function(model, opts) { } function _delete_relation_middleware(req, res, next) { - req.foundrelated.invokeThen('destroy') + req.fetchedrelated.invokeThen('destroy') .then((deleted) => { res.status(200).send('deleted') next() @@ -111,10 +111,10 @@ module.exports = function(model, opts) { } function _update_relation_middleware(req, res, next) { - req.foundrelated.map((i) => { + req.fetchedrelated.map((i) => { i.set(req.body) // updated values }) - req.foundrelated.invokeThen('save') + req.fetchedrelated.invokeThen('save') .then((saved) => { res.status(200).json(saved) next() @@ -124,10 +124,11 @@ module.exports = function(model, opts) { function _fetch_related_middleware(req, res, next) { const relation = req.fetched.related(req.params.relation) - const q = relation.query({where: req.query.where || {}}) + const mod = relation.model.collection() + const q = mod.query({where: req.query || {}}) const fetchopts = (req.page) ? {page: req.page, pageSize: req.pagesize} : {} - const p = (req.page !== undefined) ? q.fetchPage(fetchopts) : q.fetch(fetchopts) - p.then((found) => { + const fetch = (req.page !== undefined) ? q.fetchPage : q.fetch + fetch.bind(q)(fetchopts).then((found) => { req.fetchedrelated = found next() }) diff --git a/test/get.coffee b/test/get.coffee index 5f04008..5556fef 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -77,4 +77,4 @@ module.exports = (g)-> res.should.have.status(200) res.should.be.json res.body.length.should.eql 1 - res.body[0].name.should.eql 'saruman' + res.body[0].type.should.eql 'hat' From f938181dbce4bc8303ac3eb016c1f92657e3e4d5 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 08:11:12 +0200 Subject: [PATCH 18/27] refactoring tests --- test/del.coffee | 9 +++---- test/get.coffee | 61 +++++++++++++++--------------------------------- test/post.coffee | 43 +++++++++++----------------------- test/put.coffee | 11 +++------ 4 files changed, 38 insertions(+), 86 deletions(-) diff --git a/test/del.coffee b/test/del.coffee index ba2a550..435cae9 100644 --- a/test/del.coffee +++ b/test/del.coffee @@ -21,21 +21,18 @@ module.exports = (g)-> res.body.tools.length.should.eql 1 it 'must delete user', (done) -> - chai.request(g.baseurl) - .delete("/#{g.gandalfID}") + r.delete("/#{g.gandalfID}") .end (err, res) -> return done(err) if err res.should.have.status(200) - chai.request(g.baseurl).get("/#{g.gandalfID}").end (err, res) -> + r.get("/#{g.gandalfID}").end (err, res) -> return done('still exist') if not err res.should.have.status(404) done() return it 'must 404 on notexisting user', (done) -> - chai.request(g.baseurl) - .delete("/hovno") - .end (err, res) -> + r.delete("/hovno").end (err, res) -> return done('shall 404 but have not') if not err res.should.have.status(404) done() diff --git a/test/get.coffee b/test/get.coffee index 5556fef..5eb8da5 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -4,76 +4,53 @@ should = chai.should() module.exports = (g)-> - addr = g.baseurl + r = chai.request(g.baseurl) describe 'get routes', -> - it 'must get all', (done) -> - chai.request(g.baseurl).get('/').end (err, res) -> - return done(err) if err + it 'must get all', () -> + r.get('/').then (res) -> res.should.have.status(200) res.should.be.json res.body[0].name.should.eql 'gandalfek' - done() - return - it 'must get gandalf', (done) -> - chai.request(g.baseurl).get("/#{g.gandalfID}").end (err, res) -> - return done(err) if err + it 'must get gandalf', () -> + r.get("/#{g.gandalfID}").then (res) -> res.should.have.status(200) res.should.be.json res.body.name.should.eql 'gandalfek' - done() - return - it 'must return gandalf with all tools (magicwand)', (done) -> - chai.request(g.baseurl) - .get("/#{g.gandalfID}?load=tools") - .end (err, res) -> - return done(err) if err + it 'must return gandalf with all tools (magicwand)', () -> + r.get("/#{g.gandalfID}?load=tools").then (res) -> res.should.have.status(200) res.should.be.json res.body.tools.length.should.eql 2 - done() - return - it 'must return all users with all tools', (done) -> - chai.request(g.baseurl) - .get("/?load=tools") - .end (err, res) -> - return done(err) if err + it 'must return all users with all tools', () -> + r.get("/?load=tools").then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 1 res.body[0].tools.length.should.eql 2 res.body[0].tools[0].type.should.eql 'supermagicwand' - done() - return - it 'must list 2nd page of users', (done) -> + it 'must list 2nd page of users', () -> # add another user - chai.request(g.baseurl) - .post('/') - .send({ name: 'saruman' }) - .end (err, res) -> - return done(err) if err + r.post('/').send({ name: 'saruman' }) + .then (res) -> res.should.have.status(201) res.should.be.json g.sarumanID = res.body.id # list 2nd page - chai.request(g.baseurl).get('/?page=2&pagesize=1').end (err, res) -> - return done(err) if err - res.should.have.status(200) - res.should.be.json - res.body.length.should.eql 1 - res.body[0].name.should.eql 'saruman' - done() - return + r.get('/?page=2&pagesize=1') + .then (res) -> + res.should.have.status(200) + res.should.be.json + res.body.length.should.eql 1 + res.body[0].name.should.eql 'saruman' it 'must list 2nd page of gandalf thigs', () -> - return chai.request(g.baseurl) - .get("/#{g.gandalfID}/tools/?page=2&pagesize=1") - .then (res) -> + r.get("/#{g.gandalfID}/tools/?page=2&pagesize=1").then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 1 diff --git a/test/post.coffee b/test/post.coffee index 93859df..8af4da5 100644 --- a/test/post.coffee +++ b/test/post.coffee @@ -4,49 +4,32 @@ should = chai.should() module.exports = (g)-> - addr = g.baseurl + r = chai.request(g.baseurl) describe 'post routes', -> - it 'must create user', (done) -> - chai.request(g.baseurl) - .post('/') - .send({ name: 'gandalf' }) - .end (err, res) -> - return done(err) if err + it 'must create user', () -> + r.post('/').send({ name: 'gandalf' }).then (res) -> res.should.have.status(201) res.should.be.json res.body.name.should.eql 'gandalf' g.gandalfID = res.body.id - done() - return - it 'must get gandalfs tools - []', (done) -> - chai.request(g.baseurl).get("/#{g.gandalfID}/tools").end (err, res) -> - return done(err) if err + it 'must get gandalfs tools - []', () -> + r.get("/#{g.gandalfID}/tools").then (res) -> res.should.have.status(200) res.should.be.json res.body.should.eql [] - done() - return - it 'must add an magicwand and hat to gandalf', (done) -> - chai.request(g.baseurl) - .post("/#{g.gandalfID}/tools") - .send({ type: 'magicwand' }) - .end (err, res) -> - return done(err) if err + it 'must add an magicwand and hat to gandalf', () -> + r.post("/#{g.gandalfID}/tools").send({ type: 'magicwand' }) + .then (res) -> res.should.have.status(201) res.should.be.json res.body.type.should.eql 'magicwand' g.magicwandid = res.body.id - chai.request(g.baseurl) - .post("/#{g.gandalfID}/tools") - .send({ type: 'hat' }) - .end (err, res) -> - return done(err) if err - res.should.have.status(201) - res.should.be.json - res.body.type.should.eql 'hat' - done() - return + return r.post("/#{g.gandalfID}/tools").send({ type: 'hat' }) + .then (res) -> + res.should.have.status(201) + res.should.be.json + res.body.type.should.eql 'hat' diff --git a/test/put.coffee b/test/put.coffee index f055116..81a13ce 100644 --- a/test/put.coffee +++ b/test/put.coffee @@ -8,17 +8,12 @@ module.exports = (g)-> describe 'put routes', -> - it 'must update user', (done) -> - r.put("/#{g.gandalfID}") - .send({ name: 'gandalfek' }) - .end (err, res) -> - return done(err) if err + it 'must update user', () -> + r.put("/#{g.gandalfID}").send({ name: 'gandalfek' }).then (res) -> res.should.have.status(200) res.should.be.json res.body.name.should.eql 'gandalfek' res.body.id.should.eql g.gandalfID - done() - return it 'must change magicwand to supermagicwand', () -> return r.put("/#{g.gandalfID}/tools?id=#{g.magicwandid}") @@ -26,7 +21,7 @@ module.exports = (g)-> .then (res) -> res.should.have.status(200) # verify that gandalf has now supermagicwand - return chai.request(g.baseurl).get("/#{g.gandalfID}?load=tools") + return r.get("/#{g.gandalfID}?load=tools") .then (res) -> res.should.have.status(200) res.should.be.json From 2453d267a84083a22a3453ff330ec60a9caa36b9 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 09:01:56 +0200 Subject: [PATCH 19/27] sorting --- src/index.js | 41 +++++++++++++++++++++++++++++++++++++---- test/get.coffee | 16 ++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index a7c3e65..8510c7f 100644 --- a/src/index.js +++ b/src/index.js @@ -46,6 +46,34 @@ module.exports = function(model, opts) { next() } + function _extract_sorting(req) { // default sortinginfo extractor + if (req.query.sortCol && req.query.sortOrder) { + const i = { + sortCol: req.query.sortCol, + sortOrder: req.query.sortOrder + } + delete req.query.sortCol + delete req.query.sortOrder + return i + } + } + + function _sorting_query(req, res, next) { + const info = opts.sortinfo_extractor ? + opts.sortinfo_extractor(req) : _extract_sorting(req) + if (info) { + if (! info.sortCol || info.sortCol.length === 0) { + return next(new Error('wrong sorting column')) + } + if (! info.sortOrder.match(/^ASC$|^DESC$/)) { + return next(new Error('wrong sort order')) + } + req.sortCol = info.sortCol + req.sortOrder = info.sortOrder + } + next() + } + function _get_related_middleware(req, res, next) { res.json(req.fetchedrelated) // just JSON back req.fetchedrelated next() @@ -56,6 +84,9 @@ module.exports = function(model, opts) { if(req.listquery) { mod = mod.query({where: req.listquery}) } + if(req.sortCol) { + mod = mod.orderBy(req.sortCol, req.sortOrder) + } const fetchopts = {} if (req.loadquery) { fetchopts.withRelated = req.loadquery @@ -124,8 +155,10 @@ module.exports = function(model, opts) { function _fetch_related_middleware(req, res, next) { const relation = req.fetched.related(req.params.relation) - const mod = relation.model.collection() - const q = mod.query({where: req.query || {}}) + let q = relation.model.collection().query({where: req.query || {}}) + if(req.sortCol) { + q = q.orderBy(req.sortCol, req.sortOrder) + } const fetchopts = (req.page) ? {page: req.page, pageSize: req.pagesize} : {} const fetch = (req.page !== undefined) ? q.fetchPage : q.fetch fetch.bind(q)(fetchopts).then((found) => { @@ -169,13 +202,13 @@ module.exports = function(model, opts) { } function _init_app(app) { - app.get('/', _list_query, _paging_query, _load_query, _list_middleware) + app.get('/', _list_query, _paging_query, _sorting_query, _load_query, _list_middleware) app.get('/:id', _load_query, _fetch_middleware, _detail_middleware) app.post('/', _create_middleware) app.put('/:id', _fetch_middleware, _update_middleware) app.delete('/:id', _fetch_middleware, _delete_middleware) // relations - app.get('/:id/:relation', _fetch_middleware, _paging_query, _fetch_related_middleware, _get_related_middleware) + app.get('/:id/:relation', _fetch_middleware, _paging_query, _sorting_query, _fetch_related_middleware, _get_related_middleware) app.post('/:id/:relation', _fetch_middleware, _create_relation_middleware) app.put('/:id/:relation', _fetch_middleware, _fetch_related_middleware, _update_relation_middleware) app.delete('/:id/:relation', _fetch_middleware, _fetch_related_middleware, _delete_relation_middleware) diff --git a/test/get.coffee b/test/get.coffee index 5eb8da5..4b944d7 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -55,3 +55,19 @@ module.exports = (g)-> res.should.be.json res.body.length.should.eql 1 res.body[0].type.should.eql 'hat' + + it 'must list users sorted according name', () -> + r.get("/?sortCol=name&sortOrder=DESC").then (res) -> + res.should.have.status(200) + res.should.be.json + res.body.length.should.eql 2 + res.body[0].name.should.eql 'saruman' + res.body[1].name.should.eql 'gandalfek' + + it 'must list gandalf thigs', () -> + r.get("/#{g.gandalfID}/tools/?sortCol=type&sortOrder=ASC").then (res) -> + res.should.have.status(200) + res.should.be.json + res.body.length.should.eql 2 + res.body[0].type.should.eql 'hat' + res.body[1].type.should.eql 'supermagicwand' From ae45ceb8099629da67a5acbb0c7fb6f72d4c5e51 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 09:09:24 +0200 Subject: [PATCH 20/27] x-total-count header set if paginated result --- src/index.js | 6 ++++++ test/get.coffee | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/index.js b/src/index.js index 8510c7f..1013bd2 100644 --- a/src/index.js +++ b/src/index.js @@ -75,6 +75,9 @@ module.exports = function(model, opts) { } function _get_related_middleware(req, res, next) { + if (req.fetchedrelated.pagination) { + res.set('x-total-count', req.fetchedrelated.pagination.rowCount) + } res.json(req.fetchedrelated) // just JSON back req.fetchedrelated next() } @@ -97,6 +100,9 @@ module.exports = function(model, opts) { } const fetchMethod = req.page === undefined ? mod.fetchAll : mod.fetchPage fetchMethod.bind(mod)(fetchopts).then(function(collection) { + if (collection.pagination) { + res.set('x-total-count', collection.pagination.rowCount) + } res.json(collection) next() }) diff --git a/test/get.coffee b/test/get.coffee index 4b944d7..cfaec79 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -48,6 +48,7 @@ module.exports = (g)-> res.should.be.json res.body.length.should.eql 1 res.body[0].name.should.eql 'saruman' + res.headers['x-total-count'].should.eql '2' it 'must list 2nd page of gandalf thigs', () -> r.get("/#{g.gandalfID}/tools/?page=2&pagesize=1").then (res) -> @@ -55,6 +56,7 @@ module.exports = (g)-> res.should.be.json res.body.length.should.eql 1 res.body[0].type.should.eql 'hat' + res.headers['x-total-count'].should.eql '2' it 'must list users sorted according name', () -> r.get("/?sortCol=name&sortOrder=DESC").then (res) -> From cc73be35db3516786e8b3a9a5976094ec2fa76d1 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 09:33:20 +0200 Subject: [PATCH 21/27] attrs query --- src/index.js | 18 ++++++++++++++++-- test/get.coffee | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 1013bd2..5e14f2f 100644 --- a/src/index.js +++ b/src/index.js @@ -74,6 +74,14 @@ module.exports = function(model, opts) { next() } + function _attrs_query(req, res, next) { + if (req.query.attrs) { + req.columns4fetch = req.query.attrs.split(',') + delete req.query.attrs + } + next() + } + function _get_related_middleware(req, res, next) { if (req.fetchedrelated.pagination) { res.set('x-total-count', req.fetchedrelated.pagination.rowCount) @@ -98,6 +106,9 @@ module.exports = function(model, opts) { fetchopts.page = req.page fetchopts.pageSize = req.pagesize } + if (req.columns4fetch) { + fetchopts.columns = req.columns4fetch + } const fetchMethod = req.page === undefined ? mod.fetchAll : mod.fetchPage fetchMethod.bind(mod)(fetchopts).then(function(collection) { if (collection.pagination) { @@ -166,6 +177,9 @@ module.exports = function(model, opts) { q = q.orderBy(req.sortCol, req.sortOrder) } const fetchopts = (req.page) ? {page: req.page, pageSize: req.pagesize} : {} + if (req.columns4fetch) { + fetchopts.columns = req.columns4fetch + } const fetch = (req.page !== undefined) ? q.fetchPage : q.fetch fetch.bind(q)(fetchopts).then((found) => { req.fetchedrelated = found @@ -208,13 +222,13 @@ module.exports = function(model, opts) { } function _init_app(app) { - app.get('/', _list_query, _paging_query, _sorting_query, _load_query, _list_middleware) + app.get('/', _list_query, _paging_query, _sorting_query, _load_query, _attrs_query, _list_middleware) app.get('/:id', _load_query, _fetch_middleware, _detail_middleware) app.post('/', _create_middleware) app.put('/:id', _fetch_middleware, _update_middleware) app.delete('/:id', _fetch_middleware, _delete_middleware) // relations - app.get('/:id/:relation', _fetch_middleware, _paging_query, _sorting_query, _fetch_related_middleware, _get_related_middleware) + app.get('/:id/:relation', _fetch_middleware, _paging_query, _sorting_query, _attrs_query, _fetch_related_middleware, _get_related_middleware) app.post('/:id/:relation', _fetch_middleware, _create_relation_middleware) app.put('/:id/:relation', _fetch_middleware, _fetch_related_middleware, _update_relation_middleware) app.delete('/:id/:relation', _fetch_middleware, _fetch_related_middleware, _delete_relation_middleware) diff --git a/test/get.coffee b/test/get.coffee index cfaec79..ae61ac3 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -50,6 +50,15 @@ module.exports = (g)-> res.body[0].name.should.eql 'saruman' res.headers['x-total-count'].should.eql '2' + it 'must list 2nd page of users but only names', () -> + r.get('/?page=2&pagesize=1&attrs=name') + .then (res) -> + res.should.have.status(200) + res.should.be.json + res.body.length.should.eql 1 + Object.keys(res.body[0]).length.should.eql 1 + res.body[0].name.should.eql 'saruman' + it 'must list 2nd page of gandalf thigs', () -> r.get("/#{g.gandalfID}/tools/?page=2&pagesize=1").then (res) -> res.should.have.status(200) @@ -73,3 +82,13 @@ module.exports = (g)-> res.body.length.should.eql 2 res.body[0].type.should.eql 'hat' res.body[1].type.should.eql 'supermagicwand' + + it 'must list gandalf thig types and ids ONLY', () -> + r.get("/#{g.gandalfID}/tools/?sortCol=type&sortOrder=ASC&attrs=type,id") + .then (res) -> + res.should.have.status(200) + res.should.be.json + res.body.length.should.eql 2 + Object.keys(res.body[0]).length.should.eql 2 # type,id + res.body[0].type.should.eql 'hat' + res.body[1].type.should.eql 'supermagicwand' From 75d7ca47e570929bdcffb215424b37b476a6d00e Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 11:43:32 +0200 Subject: [PATCH 22/27] fixing query lost in fetchPage --- package.json | 2 +- src/index.js | 9 +++++++-- test/get.coffee | 15 +++------------ test/post.coffee | 12 ++++++++++++ 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 8256c8e..5a34d48 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "peerDependencies": { "express": "^4.10.0", "body-parser": "^1.9.0", - "bookshelf": "^0.10.3" + "bookshelf": "vencax/bookshelf" }, "devDependencies": { "chai": "^3.5.0", diff --git a/src/index.js b/src/index.js index 5e14f2f..2e38756 100644 --- a/src/index.js +++ b/src/index.js @@ -22,10 +22,13 @@ module.exports = function(model, opts) { } function _extract_paging(req) { // default pageinfo extractor - return { + const i = { page: req.query.page, pagesize: req.query.pagesize } + delete req.query.page + delete req.query.pagesize + return i } function _paging_query(req, res, next) { @@ -172,7 +175,9 @@ module.exports = function(model, opts) { function _fetch_related_middleware(req, res, next) { const relation = req.fetched.related(req.params.relation) - let q = relation.model.collection().query({where: req.query || {}}) + const where = req.query || {} + where[relation.relatedData.foreignKey] = relation.relatedData.parentFk + let q = relation.model.collection().query({where: where}) if(req.sortCol) { q = q.orderBy(req.sortCol, req.sortOrder) } diff --git a/test/get.coffee b/test/get.coffee index ae61ac3..83afb14 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -30,20 +30,12 @@ module.exports = (g)-> r.get("/?load=tools").then (res) -> res.should.have.status(200) res.should.be.json - res.body.length.should.eql 1 + res.body.length.should.eql 2 res.body[0].tools.length.should.eql 2 res.body[0].tools[0].type.should.eql 'supermagicwand' it 'must list 2nd page of users', () -> - # add another user - r.post('/').send({ name: 'saruman' }) - .then (res) -> - res.should.have.status(201) - res.should.be.json - g.sarumanID = res.body.id - # list 2nd page - r.get('/?page=2&pagesize=1') - .then (res) -> + r.get('/?page=2&pagesize=1').then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 1 @@ -51,8 +43,7 @@ module.exports = (g)-> res.headers['x-total-count'].should.eql '2' it 'must list 2nd page of users but only names', () -> - r.get('/?page=2&pagesize=1&attrs=name') - .then (res) -> + r.get('/?page=2&pagesize=1&attrs=name').then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 1 diff --git a/test/post.coffee b/test/post.coffee index 8af4da5..02aaaa9 100644 --- a/test/post.coffee +++ b/test/post.coffee @@ -33,3 +33,15 @@ module.exports = (g)-> res.should.have.status(201) res.should.be.json res.body.type.should.eql 'hat' + + it 'add another user, saruman', () -> + r.post('/').send({ name: 'saruman' }) + .then (res) -> + res.should.have.status(201) + res.should.be.json + g.sarumanID = res.body.id + return r.post("/#{g.sarumanID}/tools").send({ type: 'fury' }) + .then (res) -> + res.should.have.status(201) + res.should.be.json + res.body.type.should.eql 'fury' From 3cda3517f7c7c852b9ee136a6016b0bece532567 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 13:37:31 +0200 Subject: [PATCH 23/27] src refactored into 3 files --- src/basic.js | 92 +++++++++++++++ src/index.js | 278 +++++---------------------------------------- src/preparation.js | 96 ++++++++++++++++ src/related.js | 74 ++++++++++++ test/main.js | 3 + 5 files changed, 292 insertions(+), 251 deletions(-) create mode 100644 src/basic.js create mode 100644 src/preparation.js create mode 100644 src/related.js diff --git a/src/basic.js b/src/basic.js new file mode 100644 index 0000000..0655af7 --- /dev/null +++ b/src/basic.js @@ -0,0 +1,92 @@ + +module.exports = function(model) { + + function _list_middleware(req, res, next) { + let mod = new model() + if(req.listquery) { + mod = mod.query({where: req.listquery}) + } + if(req.sortCol) { + mod = mod.orderBy(req.sortCol, req.sortOrder) + } + const fetchopts = {} + if (req.loadquery) { + fetchopts.withRelated = req.loadquery + } + if (req.page) { + fetchopts.page = req.page + fetchopts.pageSize = req.pagesize + } + if (req.columns4fetch) { + fetchopts.columns = req.columns4fetch + } + const fetchMethod = req.page === undefined ? mod.fetchAll : mod.fetchPage + fetchMethod.bind(mod)(fetchopts).then(function(collection) { + if (collection.pagination) { + res.set('x-total-count', collection.pagination.rowCount) + } + res.json(collection) + next() + }) + .catch(next) + } + + function _detail_middleware(req, res, next) { + res.json(req.fetched) // just send the fetched + next() + } + + function _create_middleware(req, res, next) { + var newitem = new model(req.body) + newitem.save() + .then(function(savedModel) { + req.saveditem = savedModel + res.status(201).json(savedModel) + next() + }) + .catch(next) + } + + function _fetch_middleware(req, res, next) { + var mod = new model({id: req.params.id}) + const fetchopts = { + require: true + } + if (req.loadquery) { + fetchopts.withRelated = req.loadquery + } + mod.fetch(fetchopts) + .then(function(fetched) { + req.fetched = fetched + next() + }) + .catch(next) + } + + function _update_middleware(req, res, next) { + req.fetched.save(req.body) + .then(function(saved) { + res.status(200).json(saved) + }) + .catch(next) + } + + function _delete_middleware(req, res, next) { + req.fetched.destroy() + .then(function(saved) { + res.status(200).send('deleted') + next() + }) + .catch(next) + } + + return { + list_middleware: _list_middleware, + detail_middleware: _detail_middleware, + create_middleware: _create_middleware, + fetch_middleware: _fetch_middleware, + update_middleware: _update_middleware, + delete_middleware: _delete_middleware + } + +} diff --git a/src/index.js b/src/index.js index 2e38756..de7917a 100644 --- a/src/index.js +++ b/src/index.js @@ -3,258 +3,34 @@ module.exports = function(model, opts) { opts = opts || {} - function _list_query(req, res, next) { - try { - req.listquery = req.query.where ? parseJSON(req.query.where) : {} - } catch(err) { - return next(new Error('Could not parse JSON: ' + req.query.where)) - } - next() - } - - function _load_query(req, res, next) { - try { - req.loadquery = req.query.load ? req.query.load.split(',') : [] - } catch(err) { - return next(new Error('could not parse query.load: ' + req.query.load)) - } - next() - } - - function _extract_paging(req) { // default pageinfo extractor - const i = { - page: req.query.page, - pagesize: req.query.pagesize - } - delete req.query.page - delete req.query.pagesize - return i - } - - function _paging_query(req, res, next) { - const pinfo = opts.pageinfo_extractor ? - opts.pageinfo_extractor(req) : _extract_paging(req) - const page = parseInt(pinfo.page) - if (pinfo.page && (isNaN(page) || page <= 0)) { - return next(new Error('wrong page')) - } - const pagesize = parseInt(pinfo.pagesize) - if (pinfo.pagesize && (isNaN(pagesize) || pagesize <= 0)) { - return next(new Error('wrong pagesize')) - } - if (pinfo.page) { - req.page = page - req.pagesize = pagesize - } - next() - } - - function _extract_sorting(req) { // default sortinginfo extractor - if (req.query.sortCol && req.query.sortOrder) { - const i = { - sortCol: req.query.sortCol, - sortOrder: req.query.sortOrder - } - delete req.query.sortCol - delete req.query.sortOrder - return i - } - } - - function _sorting_query(req, res, next) { - const info = opts.sortinfo_extractor ? - opts.sortinfo_extractor(req) : _extract_sorting(req) - if (info) { - if (! info.sortCol || info.sortCol.length === 0) { - return next(new Error('wrong sorting column')) - } - if (! info.sortOrder.match(/^ASC$|^DESC$/)) { - return next(new Error('wrong sort order')) - } - req.sortCol = info.sortCol - req.sortOrder = info.sortOrder - } - next() - } - - function _attrs_query(req, res, next) { - if (req.query.attrs) { - req.columns4fetch = req.query.attrs.split(',') - delete req.query.attrs - } - next() - } - - function _get_related_middleware(req, res, next) { - if (req.fetchedrelated.pagination) { - res.set('x-total-count', req.fetchedrelated.pagination.rowCount) - } - res.json(req.fetchedrelated) // just JSON back req.fetchedrelated - next() - } - - function _list_middleware(req, res, next) { - let mod = new model() - if(req.listquery) { - mod = mod.query({where: req.listquery}) - } - if(req.sortCol) { - mod = mod.orderBy(req.sortCol, req.sortOrder) - } - const fetchopts = {} - if (req.loadquery) { - fetchopts.withRelated = req.loadquery - } - if (req.page) { - fetchopts.page = req.page - fetchopts.pageSize = req.pagesize - } - if (req.columns4fetch) { - fetchopts.columns = req.columns4fetch - } - const fetchMethod = req.page === undefined ? mod.fetchAll : mod.fetchPage - fetchMethod.bind(mod)(fetchopts).then(function(collection) { - if (collection.pagination) { - res.set('x-total-count', collection.pagination.rowCount) - } - res.json(collection) - next() - }) - .catch(next) - } - - function _detail_middleware(req, res, next) { - res.json(req.fetched) // just send the fetched - next() - } - - function _create_middleware(req, res, next) { - var newitem = new model(req.body) - newitem.save() - .then(function(savedModel) { - req.saveditem = savedModel - res.status(201).json(savedModel) - next() - }) - .catch(next) - } - - function _create_relation_middleware(req, res, next) { - const relation = req.fetched.related(req.params.relation) - // for hasMany relations - const newitem = new relation.model(req.body) - relation.create(newitem) - .then(function(savedModel) { - req.savedModel = savedModel - res.status(201).json(savedModel) - next() - }) - .catch(next) - } - - function _delete_relation_middleware(req, res, next) { - req.fetchedrelated.invokeThen('destroy') - .then((deleted) => { - res.status(200).send('deleted') - next() - }) - .catch(next) - } - - function _update_relation_middleware(req, res, next) { - req.fetchedrelated.map((i) => { - i.set(req.body) // updated values - }) - req.fetchedrelated.invokeThen('save') - .then((saved) => { - res.status(200).json(saved) - next() - }) - .catch(next) - } - - function _fetch_related_middleware(req, res, next) { - const relation = req.fetched.related(req.params.relation) - const where = req.query || {} - where[relation.relatedData.foreignKey] = relation.relatedData.parentFk - let q = relation.model.collection().query({where: where}) - if(req.sortCol) { - q = q.orderBy(req.sortCol, req.sortOrder) - } - const fetchopts = (req.page) ? {page: req.page, pageSize: req.pagesize} : {} - if (req.columns4fetch) { - fetchopts.columns = req.columns4fetch - } - const fetch = (req.page !== undefined) ? q.fetchPage : q.fetch - fetch.bind(q)(fetchopts).then((found) => { - req.fetchedrelated = found - next() - }) - .catch(next) - } - - function _fetch_middleware(req, res, next) { - var mod = new model({id: req.params.id}) - const fetchopts = { - require: true - } - if (req.loadquery) { - fetchopts.withRelated = req.loadquery - } - mod.fetch(fetchopts) - .then(function(fetched) { - req.fetched = fetched - next() - }) - .catch(next) - } - - function _update_middleware(req, res, next) { - req.fetched.save(req.body) - .then(function(saved) { - res.status(200).json(saved) - }) - .catch(next) - } - - function _delete_middleware(req, res, next) { - req.fetched.destroy() - .then(function(saved) { - res.status(200).send('deleted') - next() - }) - .catch(next) - } + const q = require('./preparation')(opts) + const basic = require('./basic')(model) + const related = require('./related')(model) function _init_app(app) { - app.get('/', _list_query, _paging_query, _sorting_query, _load_query, _attrs_query, _list_middleware) - app.get('/:id', _load_query, _fetch_middleware, _detail_middleware) - app.post('/', _create_middleware) - app.put('/:id', _fetch_middleware, _update_middleware) - app.delete('/:id', _fetch_middleware, _delete_middleware) + app.get('/', + q.list_query, q.paging_query, q.sorting_query, q.load_query, q.attrs_query, + basic.list_middleware + ) + app.get('/:id', q.load_query, basic.fetch_middleware, basic.detail_middleware) + app.post('/', basic.create_middleware) + app.put('/:id', basic.fetch_middleware, basic.update_middleware) + app.delete('/:id', basic.fetch_middleware, basic.delete_middleware) // relations - app.get('/:id/:relation', _fetch_middleware, _paging_query, _sorting_query, _attrs_query, _fetch_related_middleware, _get_related_middleware) - app.post('/:id/:relation', _fetch_middleware, _create_relation_middleware) - app.put('/:id/:relation', _fetch_middleware, _fetch_related_middleware, _update_relation_middleware) - app.delete('/:id/:relation', _fetch_middleware, _fetch_related_middleware, _delete_relation_middleware) - } - - return { - init_app: _init_app, - list_query: _list_query, - load_query: _load_query, - get_related_middleware: _get_related_middleware, - list_middleware: _list_middleware, - detail_middleware: _detail_middleware, - create_middleware: _create_middleware, - create_relation_middleware: _create_relation_middleware, - delete_relation_middleware: _delete_relation_middleware, - fetch_related_middleware: _fetch_related_middleware, - update_relation_middleware: _update_relation_middleware, - fetch_middleware: _fetch_middleware, - update_middleware: _update_middleware, - delete_middleware: _delete_middleware, - paging_query: _paging_query - } - + app.get('/:id/:relation', + basic.fetch_middleware, q.paging_query, q.sorting_query, q.attrs_query, + related.fetch_related_middleware, related.get_related_middleware + ) + app.post('/:id/:relation', basic.fetch_middleware, + related.create_relation_middleware + ) + app.put('/:id/:relation', basic.fetch_middleware, + related.fetch_related_middleware, related.update_relation_middleware + ) + app.delete('/:id/:relation', basic.fetch_middleware, + related.fetch_related_middleware, related.delete_relation_middleware + ) + } + + return Object.assign({init_app: _init_app}, q, basic, related) } diff --git a/src/preparation.js b/src/preparation.js new file mode 100644 index 0000000..51566ff --- /dev/null +++ b/src/preparation.js @@ -0,0 +1,96 @@ + +module.exports = function(opts) { + + opts = opts || {} + + function _list_query(req, res, next) { + try { + req.listquery = req.query.where ? parseJSON(req.query.where) : {} + } catch(err) { + return next(new Error('Could not parse JSON: ' + req.query.where)) + } + next() + } + + function _load_query(req, res, next) { + try { + req.loadquery = req.query.load ? req.query.load.split(',') : [] + } catch(err) { + return next(new Error('could not parse query.load: ' + req.query.load)) + } + next() + } + + function _extract_paging(req) { // default pageinfo extractor + const i = { + page: req.query.page, + pagesize: req.query.pagesize + } + delete req.query.page + delete req.query.pagesize + return i + } + + function _paging_query(req, res, next) { + const pinfo = opts.pageinfo_extractor ? + opts.pageinfo_extractor(req) : _extract_paging(req) + const page = parseInt(pinfo.page) + if (pinfo.page && (isNaN(page) || page <= 0)) { + return next(new Error('wrong page')) + } + const pagesize = parseInt(pinfo.pagesize) + if (pinfo.pagesize && (isNaN(pagesize) || pagesize <= 0)) { + return next(new Error('wrong pagesize')) + } + if (pinfo.page) { + req.page = page + req.pagesize = pagesize + } + next() + } + + function _extract_sorting(req) { // default sortinginfo extractor + if (req.query.sortCol && req.query.sortOrder) { + const i = { + sortCol: req.query.sortCol, + sortOrder: req.query.sortOrder + } + delete req.query.sortCol + delete req.query.sortOrder + return i + } + } + + function _sorting_query(req, res, next) { + const info = opts.sortinfo_extractor ? + opts.sortinfo_extractor(req) : _extract_sorting(req) + if (info) { + if (! info.sortCol || info.sortCol.length === 0) { + return next(new Error('wrong sorting column')) + } + if (! info.sortOrder.match(/^ASC$|^DESC$/)) { + return next(new Error('wrong sort order')) + } + req.sortCol = info.sortCol + req.sortOrder = info.sortOrder + } + next() + } + + function _attrs_query(req, res, next) { + if (req.query.attrs) { + req.columns4fetch = req.query.attrs.split(',') + delete req.query.attrs + } + next() + } + + return { + attrs_query: _attrs_query, + list_query: _list_query, + load_query: _load_query, + paging_query: _paging_query, + sorting_query: _sorting_query + } + +} diff --git a/src/related.js b/src/related.js new file mode 100644 index 0000000..e5b6010 --- /dev/null +++ b/src/related.js @@ -0,0 +1,74 @@ + +module.exports = function(model) { + + function _get_related_middleware(req, res, next) { + if (req.fetchedrelated.pagination) { + res.set('x-total-count', req.fetchedrelated.pagination.rowCount) + } + res.json(req.fetchedrelated) // just JSON back req.fetchedrelated + next() + } + + function _create_relation_middleware(req, res, next) { + const relation = req.fetched.related(req.params.relation) + // for hasMany relations + const newitem = new relation.model(req.body) + relation.create(newitem) + .then(function(savedModel) { + req.savedModel = savedModel + res.status(201).json(savedModel) + next() + }) + .catch(next) + } + + function _delete_relation_middleware(req, res, next) { + req.fetchedrelated.invokeThen('destroy') + .then((deleted) => { + res.status(200).send('deleted') + next() + }) + .catch(next) + } + + function _update_relation_middleware(req, res, next) { + req.fetchedrelated.map((i) => { + i.set(req.body) // updated values + }) + req.fetchedrelated.invokeThen('save') + .then((saved) => { + res.status(200).json(saved) + next() + }) + .catch(next) + } + + function _fetch_related_middleware(req, res, next) { + const relation = req.fetched.related(req.params.relation) + const where = req.query || {} + where[relation.relatedData.foreignKey] = relation.relatedData.parentFk + let q = relation.model.collection().query({where: where}) + if(req.sortCol) { + q = q.orderBy(req.sortCol, req.sortOrder) + } + const fetchopts = (req.page) ? {page: req.page, pageSize: req.pagesize} : {} + if (req.columns4fetch) { + fetchopts.columns = req.columns4fetch + } + const fetch = (req.page !== undefined) ? q.fetchPage : q.fetch + fetch.bind(q)(fetchopts).then((found) => { + req.fetchedrelated = found + next() + }) + .catch(next) + } + + return { + get_related_middleware: _get_related_middleware, + create_relation_middleware: _create_relation_middleware, + delete_relation_middleware: _delete_relation_middleware, + fetch_related_middleware: _fetch_related_middleware, + update_relation_middleware: _update_relation_middleware + } + +} diff --git a/test/main.js b/test/main.js index 8bdf766..bb4c0e2 100644 --- a/test/main.js +++ b/test/main.js @@ -49,6 +49,9 @@ describe('app', (suite) => { it('should exist', (done) => { should.exist(g.app) + should.exist(k.list_query) + should.exist(k.list_middleware) + should.exist(k.create_relation_middleware) done() }) From 32ef89c650bd1e01ac639126c91663d66514c9bb Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 14:35:21 +0200 Subject: [PATCH 24/27] createError func mandatory --- src/index.js | 4 +++- src/preparation.js | 14 ++++++-------- test/main.js | 6 +++++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index de7917a..1decd98 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,9 @@ module.exports = function(model, opts) { - opts = opts || {} + if (! opts.createError) { + throw new Error('mandatory function createError not provided in opts :/') + } const q = require('./preparation')(opts) const basic = require('./basic')(model) diff --git a/src/preparation.js b/src/preparation.js index 51566ff..c2b2736 100644 --- a/src/preparation.js +++ b/src/preparation.js @@ -1,13 +1,11 @@ module.exports = function(opts) { - opts = opts || {} - function _list_query(req, res, next) { try { req.listquery = req.query.where ? parseJSON(req.query.where) : {} } catch(err) { - return next(new Error('Could not parse JSON: ' + req.query.where)) + return next(opts.createError('Could not parse JSON: ' + req.query.where)) } next() } @@ -16,7 +14,7 @@ module.exports = function(opts) { try { req.loadquery = req.query.load ? req.query.load.split(',') : [] } catch(err) { - return next(new Error('could not parse query.load: ' + req.query.load)) + return next(opts.createError('could not parse query.load: ' + req.query.load)) } next() } @@ -36,11 +34,11 @@ module.exports = function(opts) { opts.pageinfo_extractor(req) : _extract_paging(req) const page = parseInt(pinfo.page) if (pinfo.page && (isNaN(page) || page <= 0)) { - return next(new Error('wrong page')) + return next(opts.createError('wrong page')) } const pagesize = parseInt(pinfo.pagesize) if (pinfo.pagesize && (isNaN(pagesize) || pagesize <= 0)) { - return next(new Error('wrong pagesize')) + return next(opts.createError('wrong pagesize')) } if (pinfo.page) { req.page = page @@ -66,10 +64,10 @@ module.exports = function(opts) { opts.sortinfo_extractor(req) : _extract_sorting(req) if (info) { if (! info.sortCol || info.sortCol.length === 0) { - return next(new Error('wrong sorting column')) + return next(opts.createError('wrong sorting column')) } if (! info.sortOrder.match(/^ASC$|^DESC$/)) { - return next(new Error('wrong sort order')) + return next(opts.createError('wrong sort order')) } req.sortCol = info.sortCol req.sortOrder = info.sortOrder diff --git a/test/main.js b/test/main.js index bb4c0e2..8cf1ff2 100644 --- a/test/main.js +++ b/test/main.js @@ -19,7 +19,11 @@ describe('app', (suite) => { app.use(bodyParser.json()) g.db = db - const k = Kalamata(db.models.User) + const k = Kalamata(db.models.User, { + createError: (message, status = 400) => { + return new Error({status: status, message: message}) + } + }) k.init_app(app) // create the REST routes app.use((err, req, res, next) => { if (err.message === 'EmptyResponse') { From 0400188e136cbab69b85243821785f17c2769169 Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 14:52:00 +0200 Subject: [PATCH 25/27] misc req.query attrs begins with _ --- README.md | 2 +- src/basic.js | 4 +-- src/index.js | 2 +- src/preparation.js | 63 +++++++++++++++++++++------------------------- test/del.coffee | 2 +- test/get.coffee | 16 ++++++------ test/main.js | 2 +- test/put.coffee | 2 +- 8 files changed, 43 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index dc1ab9b..a34d5b1 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ app.put('/:id', user_middlewarez.fetch_middleware, _set_updated_on, user_middlew ```js // middleware that adds another contraint to fetching query function _omit_deleted(req, res, user) { - req.listquery.deleted = false + req.query.deleted = false next() // DON'T forget call next } app.get('/', user_middlewarez.list_query, _omit_deleted. user_middlewarez.list_middleware) diff --git a/src/basic.js b/src/basic.js index 0655af7..e30f8d7 100644 --- a/src/basic.js +++ b/src/basic.js @@ -3,8 +3,8 @@ module.exports = function(model) { function _list_middleware(req, res, next) { let mod = new model() - if(req.listquery) { - mod = mod.query({where: req.listquery}) + if(req.query) { + mod = mod.query({where: req.query}) } if(req.sortCol) { mod = mod.orderBy(req.sortCol, req.sortOrder) diff --git a/src/index.js b/src/index.js index 1decd98..d640e57 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ module.exports = function(model, opts) { function _init_app(app) { app.get('/', - q.list_query, q.paging_query, q.sorting_query, q.load_query, q.attrs_query, + q.paging_query, q.sorting_query, q.load_query, q.attrs_query, basic.list_middleware ) app.get('/:id', q.load_query, basic.fetch_middleware, basic.detail_middleware) diff --git a/src/preparation.js b/src/preparation.js index c2b2736..4295c72 100644 --- a/src/preparation.js +++ b/src/preparation.js @@ -1,46 +1,40 @@ module.exports = function(opts) { - function _list_query(req, res, next) { - try { - req.listquery = req.query.where ? parseJSON(req.query.where) : {} - } catch(err) { - return next(opts.createError('Could not parse JSON: ' + req.query.where)) - } - next() - } - function _load_query(req, res, next) { try { - req.loadquery = req.query.load ? req.query.load.split(',') : [] + req.loadquery = req.query._load ? req.query._load.split(',') : [] + delete req.query._load } catch(err) { - return next(opts.createError('could not parse query.load: ' + req.query.load)) + return next(opts.createError('could not parse query._load')) } next() } function _extract_paging(req) { // default pageinfo extractor - const i = { - page: req.query.page, - pagesize: req.query.pagesize + if (req.query._page && req.query._pagesize) { + const i = { + page: req.query._page, + pagesize: req.query._pagesize + } + delete req.query._page + delete req.query._pagesize + return i } - delete req.query.page - delete req.query.pagesize - return i } function _paging_query(req, res, next) { const pinfo = opts.pageinfo_extractor ? opts.pageinfo_extractor(req) : _extract_paging(req) - const page = parseInt(pinfo.page) - if (pinfo.page && (isNaN(page) || page <= 0)) { - return next(opts.createError('wrong page')) - } - const pagesize = parseInt(pinfo.pagesize) - if (pinfo.pagesize && (isNaN(pagesize) || pagesize <= 0)) { - return next(opts.createError('wrong pagesize')) - } - if (pinfo.page) { + if (pinfo) { + const page = parseInt(pinfo.page) + if (isNaN(page) || page <= 0) { + return next(opts.createError('wrong page')) + } + const pagesize = parseInt(pinfo.pagesize) + if (isNaN(pagesize) || pagesize <= 0) { + return next(opts.createError('wrong pagesize')) + } req.page = page req.pagesize = pagesize } @@ -48,13 +42,13 @@ module.exports = function(opts) { } function _extract_sorting(req) { // default sortinginfo extractor - if (req.query.sortCol && req.query.sortOrder) { + if (req.query._sortCol && req.query._sortOrder) { const i = { - sortCol: req.query.sortCol, - sortOrder: req.query.sortOrder + sortCol: req.query._sortCol, + sortOrder: req.query._sortOrder } - delete req.query.sortCol - delete req.query.sortOrder + delete req.query._sortCol + delete req.query._sortOrder return i } } @@ -76,16 +70,15 @@ module.exports = function(opts) { } function _attrs_query(req, res, next) { - if (req.query.attrs) { - req.columns4fetch = req.query.attrs.split(',') - delete req.query.attrs + if (req.query._attrs) { + req.columns4fetch = req.query._attrs.split(',') + delete req.query._attrs } next() } return { attrs_query: _attrs_query, - list_query: _list_query, load_query: _load_query, paging_query: _paging_query, sorting_query: _sorting_query diff --git a/test/del.coffee b/test/del.coffee index 435cae9..2255ca2 100644 --- a/test/del.coffee +++ b/test/del.coffee @@ -14,7 +14,7 @@ module.exports = (g)-> .then (res) -> res.should.have.status(200) # verify gandalf is toolless - return r.get("/#{g.gandalfID}?load=tools") + return r.get("/#{g.gandalfID}?_load=tools") .then (res) -> res.should.have.status(200) res.should.be.json diff --git a/test/get.coffee b/test/get.coffee index 83afb14..ea40091 100644 --- a/test/get.coffee +++ b/test/get.coffee @@ -21,13 +21,13 @@ module.exports = (g)-> res.body.name.should.eql 'gandalfek' it 'must return gandalf with all tools (magicwand)', () -> - r.get("/#{g.gandalfID}?load=tools").then (res) -> + r.get("/#{g.gandalfID}?_load=tools").then (res) -> res.should.have.status(200) res.should.be.json res.body.tools.length.should.eql 2 it 'must return all users with all tools', () -> - r.get("/?load=tools").then (res) -> + r.get("/?_load=tools").then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 2 @@ -35,7 +35,7 @@ module.exports = (g)-> res.body[0].tools[0].type.should.eql 'supermagicwand' it 'must list 2nd page of users', () -> - r.get('/?page=2&pagesize=1').then (res) -> + r.get('/?_page=2&_pagesize=1').then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 1 @@ -43,7 +43,7 @@ module.exports = (g)-> res.headers['x-total-count'].should.eql '2' it 'must list 2nd page of users but only names', () -> - r.get('/?page=2&pagesize=1&attrs=name').then (res) -> + r.get('/?_page=2&_pagesize=1&_attrs=name').then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 1 @@ -51,7 +51,7 @@ module.exports = (g)-> res.body[0].name.should.eql 'saruman' it 'must list 2nd page of gandalf thigs', () -> - r.get("/#{g.gandalfID}/tools/?page=2&pagesize=1").then (res) -> + r.get("/#{g.gandalfID}/tools/?_page=2&_pagesize=1").then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 1 @@ -59,7 +59,7 @@ module.exports = (g)-> res.headers['x-total-count'].should.eql '2' it 'must list users sorted according name', () -> - r.get("/?sortCol=name&sortOrder=DESC").then (res) -> + r.get("/?_sortCol=name&_sortOrder=DESC").then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 2 @@ -67,7 +67,7 @@ module.exports = (g)-> res.body[1].name.should.eql 'gandalfek' it 'must list gandalf thigs', () -> - r.get("/#{g.gandalfID}/tools/?sortCol=type&sortOrder=ASC").then (res) -> + r.get("/#{g.gandalfID}/tools/?_sortCol=type&_sortOrder=ASC").then (res) -> res.should.have.status(200) res.should.be.json res.body.length.should.eql 2 @@ -75,7 +75,7 @@ module.exports = (g)-> res.body[1].type.should.eql 'supermagicwand' it 'must list gandalf thig types and ids ONLY', () -> - r.get("/#{g.gandalfID}/tools/?sortCol=type&sortOrder=ASC&attrs=type,id") + r.get("/#{g.gandalfID}/tools/?_sortCol=type&_sortOrder=ASC&_attrs=type,id") .then (res) -> res.should.have.status(200) res.should.be.json diff --git a/test/main.js b/test/main.js index 8cf1ff2..b5ec0c6 100644 --- a/test/main.js +++ b/test/main.js @@ -53,7 +53,7 @@ describe('app', (suite) => { it('should exist', (done) => { should.exist(g.app) - should.exist(k.list_query) + should.exist(k.sorting_query) should.exist(k.list_middleware) should.exist(k.create_relation_middleware) done() diff --git a/test/put.coffee b/test/put.coffee index 81a13ce..966f0f7 100644 --- a/test/put.coffee +++ b/test/put.coffee @@ -21,7 +21,7 @@ module.exports = (g)-> .then (res) -> res.should.have.status(200) # verify that gandalf has now supermagicwand - return r.get("/#{g.gandalfID}?load=tools") + return r.get("/#{g.gandalfID}?_load=tools") .then (res) -> res.should.have.status(200) res.should.be.json From 1bbc3bc38988e99e4ac3e489e62e0eee549b735b Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 14:59:07 +0200 Subject: [PATCH 26/27] middlewares renamed (shortend) --- src/basic.js | 12 ++++++------ src/index.js | 29 ++++++++++------------------- src/preparation.js | 8 ++++---- src/related.js | 10 +++++----- test/main.js | 6 +++--- 5 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/basic.js b/src/basic.js index e30f8d7..6b9bcce 100644 --- a/src/basic.js +++ b/src/basic.js @@ -81,12 +81,12 @@ module.exports = function(model) { } return { - list_middleware: _list_middleware, - detail_middleware: _detail_middleware, - create_middleware: _create_middleware, - fetch_middleware: _fetch_middleware, - update_middleware: _update_middleware, - delete_middleware: _delete_middleware + fetch: _fetch_middleware, + list: _list_middleware, + detail: _detail_middleware, + create: _create_middleware, + update: _update_middleware, + delete: _delete_middleware } } diff --git a/src/index.js b/src/index.js index d640e57..5baad35 100644 --- a/src/index.js +++ b/src/index.js @@ -10,28 +10,19 @@ module.exports = function(model, opts) { const related = require('./related')(model) function _init_app(app) { - app.get('/', - q.paging_query, q.sorting_query, q.load_query, q.attrs_query, - basic.list_middleware - ) - app.get('/:id', q.load_query, basic.fetch_middleware, basic.detail_middleware) - app.post('/', basic.create_middleware) - app.put('/:id', basic.fetch_middleware, basic.update_middleware) - app.delete('/:id', basic.fetch_middleware, basic.delete_middleware) + app.get('/', q.paging_q, q.sorting_q, q.load_q, q.attrs_q, basic.list) + app.get('/:id', q.load_q, basic.fetch, basic.detail) + app.post('/', basic.create) + app.put('/:id', basic.fetch, basic.update) + app.delete('/:id', basic.fetch, basic.delete) // relations app.get('/:id/:relation', - basic.fetch_middleware, q.paging_query, q.sorting_query, q.attrs_query, - related.fetch_related_middleware, related.get_related_middleware - ) - app.post('/:id/:relation', basic.fetch_middleware, - related.create_relation_middleware - ) - app.put('/:id/:relation', basic.fetch_middleware, - related.fetch_related_middleware, related.update_relation_middleware - ) - app.delete('/:id/:relation', basic.fetch_middleware, - related.fetch_related_middleware, related.delete_relation_middleware + basic.fetch, q.paging_q, q.sorting_q, q.attrs_q, + related.fetch_rel, related.get_rel ) + app.post('/:id/:relation', basic.fetch, related.create_rel) + app.put('/:id/:relation', basic.fetch, related.fetch_rel, related.update_rel) + app.delete('/:id/:relation', basic.fetch, related.fetch_rel, related.delete_rel) } return Object.assign({init_app: _init_app}, q, basic, related) diff --git a/src/preparation.js b/src/preparation.js index 4295c72..4bd7a2b 100644 --- a/src/preparation.js +++ b/src/preparation.js @@ -78,10 +78,10 @@ module.exports = function(opts) { } return { - attrs_query: _attrs_query, - load_query: _load_query, - paging_query: _paging_query, - sorting_query: _sorting_query + attrs_q: _attrs_query, + load_q: _load_query, + paging_q: _paging_query, + sorting_q: _sorting_query } } diff --git a/src/related.js b/src/related.js index e5b6010..4964360 100644 --- a/src/related.js +++ b/src/related.js @@ -64,11 +64,11 @@ module.exports = function(model) { } return { - get_related_middleware: _get_related_middleware, - create_relation_middleware: _create_relation_middleware, - delete_relation_middleware: _delete_relation_middleware, - fetch_related_middleware: _fetch_related_middleware, - update_relation_middleware: _update_relation_middleware + fetch_rel: _fetch_related_middleware, + get_rel: _get_related_middleware, + create_rel: _create_relation_middleware, + update_rel: _update_relation_middleware, + delete_rel: _delete_relation_middleware } } diff --git a/test/main.js b/test/main.js index b5ec0c6..7df20fa 100644 --- a/test/main.js +++ b/test/main.js @@ -53,9 +53,9 @@ describe('app', (suite) => { it('should exist', (done) => { should.exist(g.app) - should.exist(k.sorting_query) - should.exist(k.list_middleware) - should.exist(k.create_relation_middleware) + should.exist(k.sorting_q) + should.exist(k.list) + should.exist(k.create_rel) done() }) From c60c5dadce5ab25153cf587d90ee48e95b6e0e3f Mon Sep 17 00:00:00 2001 From: Vaclav Klecanda Date: Sat, 22 Apr 2017 22:01:36 +0200 Subject: [PATCH 27/27] relation update json updated items back --- src/related.js | 2 +- test/put.coffee | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/related.js b/src/related.js index 4964360..34ab9c4 100644 --- a/src/related.js +++ b/src/related.js @@ -37,7 +37,7 @@ module.exports = function(model) { }) req.fetchedrelated.invokeThen('save') .then((saved) => { - res.status(200).json(saved) + res.status(200).json(req.fetchedrelated) next() }) .catch(next) diff --git a/test/put.coffee b/test/put.coffee index 966f0f7..8b4718e 100644 --- a/test/put.coffee +++ b/test/put.coffee @@ -20,6 +20,9 @@ module.exports = (g)-> .send({ type: 'supermagicwand' }) .then (res) -> res.should.have.status(200) + res.should.be.json + res.body.length.should.eql 1 + res.body[0].type.should.eql 'supermagicwand' # verify that gandalf has now supermagicwand return r.get("/#{g.gandalfID}?_load=tools") .then (res) ->