diff --git a/lib/action-util.js b/lib/action-util.js index 1ebacc0..466c8e1 100644 --- a/lib/action-util.js +++ b/lib/action-util.js @@ -1,5 +1,6 @@ 'use strict'; +const Boom = require('@hapi/boom'); const Hoek = require('@hapi/hoek'); const internals = {}; @@ -84,23 +85,59 @@ module.exports = (request, options) => { * @param {Request} request * @param {Object} options */ - parseSort: () => { + parseSort: (model) => { - return request.query.sort || options.sort || undefined; + if (request.query.sort) { + const sortQueryParam = request.query.sort; + const orderedSort = sortQueryParam.split(' '); + const validKeys = model.joiSchema.describe().keys; + + if (orderedSort.length === 1) { + + if (validKeys[sortQueryParam]) { + return sortQueryParam; + } + } + else if (orderedSort.length === 2) {//if param is of style "id" ASC + const key = orderedSort[0].replace(/(^"|"$)/g, '');//strip leading/trailing quotes + if (['asc','desc'].includes(orderedSort[1].toLowerCase()) && validKeys[key]) { + return sortQueryParam; + } + } + + throw Boom.badRequest('Sort query param must be a present in the model\'s Joi schema and sort order must be asc or desc'); + + } + + return options.sort || undefined; }, /** * @param {Request} request * @param {Object} options */ - parseWhere: () => { + parseWhere: (model) => { let queryWhere; - if ( request.query.where ) { - queryWhere = JSON.parse(request.query.where ); + if (request.query.where) { + const validKeys = model.joiSchema.describe().keys; + try { + queryWhere = JSON.parse(request.query.where); + } + catch (ignoreError) { + throw Boom.badRequest('Unable to parse where query parameter'); + } + + const queryKeys = Object.keys(queryWhere); + + if (queryKeys.every((k) => validKeys[k])) { + return queryWhere; + } + + throw Boom.badRequest('Where query param must be a present in the model\'s Joi schema'); } - return queryWhere || options.where || undefined; + return options.where || undefined; }, /** diff --git a/lib/actions/find.js b/lib/actions/find.js index 7d6432a..bc6f713 100644 --- a/lib/actions/find.js +++ b/lib/actions/find.js @@ -34,10 +34,10 @@ module.exports = function findRecords(route, options) { } const limit = actionUtil.parseLimit(); - const sort = actionUtil.parseSort(); + const sort = actionUtil.parseSort(Model); const rangeStart = actionUtil.parseSkip(); const rangeEnd = rangeStart ? rangeStart + limit : limit; - const where = actionUtil.parseWhere(); + const where = actionUtil.parseWhere(Model); const foundModelsQuery = Model.query().range(rangeStart, rangeEnd).limit(limit); diff --git a/lib/actions/populate.js b/lib/actions/populate.js index 7f7388a..8976797 100644 --- a/lib/actions/populate.js +++ b/lib/actions/populate.js @@ -33,7 +33,7 @@ module.exports = function expand(route, options) { const keys = actionUtil.getKeys(); const limit = actionUtil.parseLimit(); - const sort = actionUtil.parseSort(); + const sort = actionUtil.parseSort(Model); const rangeStart = actionUtil.parseSkip(); const rangeEnd = rangeStart + limit; diff --git a/package.json b/package.json index 7d8b4de..cfcb86d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hapipal/tandy", - "version": "3.1.0", + "version": "3.1.1", "description": "Auto-generated, RESTful, CRUDdy route handlers for Schwifty models in hapi", "main": "lib/index.js", "scripts": { diff --git a/test/index.js b/test/index.js index 1995b5d..5a35017 100644 --- a/test/index.js +++ b/test/index.js @@ -1283,7 +1283,7 @@ describe('Tandy', () => { expect(result[0]).to.be.an.object(); expect(result[0].firstName).to.equal('a'); }); - it('Exctracts users by firstName = a using query param' , async () => { + it('Extracts users by firstName = a using query param' , async () => { const server = await getServer(getOptions()); server.registerModel(TestModels.Users); @@ -1319,6 +1319,72 @@ describe('Tandy', () => { expect(result[0]).to.be.an.object(); expect(result[0].firstName).to.equal('a'); }); + it('Attempts to add bad where query with valid json' , async () => { + + const server = await getServer(getOptions()); + server.registerModel(TestModels.Users); + await server.initialize(); + + const knex = server.knex(); + await knex.seed.run({ directory: 'test/seeds' }); + + server.route({ + method: 'GET', + path: '/users', + handler: { tandy: {} }, + config: { + validate: { + query: Joi.object().keys({ + where: Joi.string().required() + }) + } + } + }); + + const options = { + method: 'GET', + url: '/users?where=%7B%20%22firetruck%22%3A%20%22a%22%20%7D' + }; + + const response = await server.inject(options); + const result = response.result; + + expect(response.statusCode).to.equal(400); + expect(result.message).to.equal('Where query param must be a present in the model\'s Joi schema'); + }); + it('Attempts to add bad where query with invalid json' , async () => { + + const server = await getServer(getOptions()); + server.registerModel(TestModels.Users); + await server.initialize(); + + const knex = server.knex(); + await knex.seed.run({ directory: 'test/seeds' }); + + server.route({ + method: 'GET', + path: '/users', + handler: { tandy: {} }, + config: { + validate: { + query: Joi.object().keys({ + where: Joi.string().required() + }) + } + } + }); + + const options = { + method: 'GET', + url: '/users?where=askdfaksfkjaskjkfjsjsk' + }; + + const response = await server.inject(options); + const result = response.result; + + expect(response.statusCode).to.equal(400); + expect(result.message).to.equal('Unable to parse where query parameter'); + }); it('Fetches limited number of users', async () => { const server = await getServer(getOptions()); @@ -1357,6 +1423,235 @@ describe('Tandy', () => { expect(result).to.be.an.array(); expect(result.length).to.equal(1); }); + it('Attempts to sort by invalid key', async () => { + + const server = await getServer(getOptions()); + server.registerModel([ + TestModels.Users, + TestModels.Tokens + ]); + + await server.initialize(); + + const knex = server.knex(); + await knex.seed.run({ directory: 'test/seeds' }); + + server.route({ + method: 'GET', + path: '/users', + handler: { tandy: { limit: 1 } }, + config: { + validate: { + query: Joi.object().keys({ + sort: Joi.string().required() + }) + } + } + }); + + const options = { + method: 'GET', + url: '/users?sort=asdf' + }; + + const response = await server.inject(options); + const result = response.result; + + expect(response.statusCode).to.equal(400); + expect(result.message).to.equal('Sort query param must be a present in the model\'s Joi schema and sort order must be asc or desc'); + }); + it('Fetches users, sorted by id desc', async () => { + + const server = await getServer(getOptions()); + server.registerModel([ + TestModels.Users, + TestModels.Tokens + ]); + + await server.initialize(); + + const knex = server.knex(); + await knex.seed.run({ directory: 'test/seeds' }); + + server.route({ + method: 'GET', + path: '/users', + handler: { tandy: {} }, + config: { + validate: { + query: Joi.object().keys({ + sort: Joi.string().required() + }) + } + } + }); + + const options = { + method: 'GET', + url: '/users?sort="id" desc' + }; + + const response = await server.inject(options); + const result = response.result; + + expect(response.statusCode).to.equal(200); + expect(result).to.be.an.array(); + expect(result.length).to.equal(4); + expect(result[0].id).to.equal(4); + + }); + it('Fetches users with invalid sort order', async () => { + + const server = await getServer(getOptions()); + server.registerModel([ + TestModels.Users, + TestModels.Tokens + ]); + + await server.initialize(); + + const knex = server.knex(); + await knex.seed.run({ directory: 'test/seeds' }); + + server.route({ + method: 'GET', + path: '/users', + handler: { tandy: {} }, + config: { + validate: { + query: Joi.object().keys({ + sort: Joi.string().required() + }) + } + } + }); + + const options = { + method: 'GET', + url: '/users?sort="id" fire' + }; + + const response = await server.inject(options); + const result = response.result; + + expect(response.statusCode).to.equal(400); + expect(result.message).to.equal('Sort query param must be a present in the model\'s Joi schema and sort order must be asc or desc'); + + }); + it('Fetches users with invalid key and sort order', async () => { + + const server = await getServer(getOptions()); + server.registerModel([ + TestModels.Users, + TestModels.Tokens + ]); + + await server.initialize(); + + const knex = server.knex(); + await knex.seed.run({ directory: 'test/seeds' }); + + server.route({ + method: 'GET', + path: '/users', + handler: { tandy: {} }, + config: { + validate: { + query: Joi.object().keys({ + sort: Joi.string().required() + }) + } + } + }); + + const options = { + method: 'GET', + url: '/users?sort="blast" fire' + }; + + const response = await server.inject(options); + const result = response.result; + + expect(response.statusCode).to.equal(400); + expect(result.message).to.equal('Sort query param must be a present in the model\'s Joi schema and sort order must be asc or desc'); + + }); + it('Fetches users with invalid key and valid sort order', async () => { + + const server = await getServer(getOptions()); + server.registerModel([ + TestModels.Users, + TestModels.Tokens + ]); + + await server.initialize(); + + const knex = server.knex(); + await knex.seed.run({ directory: 'test/seeds' }); + + server.route({ + method: 'GET', + path: '/users', + handler: { tandy: {} }, + config: { + validate: { + query: Joi.object().keys({ + sort: Joi.string().required() + }) + } + } + }); + + const options = { + method: 'GET', + url: '/users?sort="blast" asc' + }; + + const response = await server.inject(options); + const result = response.result; + + expect(response.statusCode).to.equal(400); + expect(result.message).to.equal('Sort query param must be a present in the model\'s Joi schema and sort order must be asc or desc'); + + }); + it('Fetches users with too many sort elements', async () => { + + const server = await getServer(getOptions()); + server.registerModel([ + TestModels.Users, + TestModels.Tokens + ]); + + await server.initialize(); + + const knex = server.knex(); + await knex.seed.run({ directory: 'test/seeds' }); + + server.route({ + method: 'GET', + path: '/users', + handler: { tandy: {} }, + config: { + validate: { + query: Joi.object().keys({ + sort: Joi.string().required() + }) + } + } + }); + + const options = { + method: 'GET', + url: '/users?sort="id" ASC firetruck' + }; + + const response = await server.inject(options); + const result = response.result; + + expect(response.statusCode).to.equal(400); + expect(result.message).to.equal('Sort query param must be a present in the model\'s Joi schema and sort order must be asc or desc'); + + }); it('Fetches limited number of tokens for a user', async () => { const server = await getServer(getOptions());