Feat add support of limit, offset, order

Allows passing these as query params to list and scope methods.
This commit is contained in:
Joey Baker
2016-09-28 21:17:16 -07:00
parent 5923f0dbcb
commit 8966d7b287
7 changed files with 389 additions and 8 deletions

View File

@ -2,7 +2,7 @@ import joi from 'joi';
import path from 'path';
import error from './error';
import _ from 'lodash';
import { parseInclude, parseWhere } from './utils';
import { parseInclude, parseWhere, parseLimitAndOffset, parseOrder } from './utils';
import { notFound } from 'boom';
import * as associations from './associations/index';
import getConfigForMethod from './get-config-for-method.js';
@ -144,11 +144,13 @@ export const list = ({ server, model, prefix = '/', config }) => {
async handler(request, reply) {
const include = parseInclude(request);
const where = parseWhere(request);
const { limit, offset } = parseLimitAndOffset(request);
const order = parseOrder(request);
if (include instanceof Error) return void reply(include);
const list = await model.findAll({
where, include,
where, include, limit, offset, order,
});
reply(list.map((item) => item.toJSON()));
@ -191,10 +193,14 @@ export const scope = ({ server, model, prefix = '/', config }) => {
async handler(request, reply) {
const include = parseInclude(request);
const where = parseWhere(request);
const { limit, offset } = parseLimitAndOffset(request);
const order = parseOrder(request);
if (include instanceof Error) return void reply(include);
const list = await model.scope(request.params.scope).findAll({ include, where });
const list = await model.scope(request.params.scope).findAll({
include, where, limit, offset, order,
});
reply(list.map((item) => item.toJSON()));
},

View File

@ -37,7 +37,7 @@ test.beforeEach('setup request stub', (t) => {
t.context.request = {
query: {},
payload: {},
models: [t.context.model],
models: t.context.models,
};
});
@ -157,3 +157,75 @@ test('crud#list handler if parseInclude errors', async (t) => {
'responds with a Boom error'
);
});
test('crud#list handler with limit', async (t) => {
const { server, model, request, reply, models } = t.context;
const { findAll } = model;
// set the limit
request.query.limit = 1;
list({ server, model });
const { handler } = server.route.args[0][0];
model.findAll.resolves(models);
try {
await handler(request, reply);
} catch (e) {
t.ifError(e, 'does not error while handling');
} finally {
t.pass('does not error while handling');
}
t.truthy(
reply.calledOnce
, 'calls reply only once'
);
const response = reply.args[0][0];
const findAllArgs = findAll.args[0][0];
t.falsy(response instanceof Error, response);
t.is(
findAllArgs.limit,
request.query.limit,
'queries with the limit'
);
});
test('crud#list handler with order', async (t) => {
const { server, model, request, reply, models } = t.context;
const { findAll } = model;
// set the limit
request.query.order = 'key';
list({ server, model });
const { handler } = server.route.args[0][0];
model.findAll.resolves(models);
try {
await handler(request, reply);
} catch (e) {
t.ifError(e, 'does not error while handling');
} finally {
t.pass('does not error while handling');
}
t.truthy(
reply.calledOnce
, 'calls reply only once'
);
const response = reply.args[0][0];
const findAllArgs = findAll.args[0][0];
t.falsy(response instanceof Error, response);
t.deepEqual(
findAllArgs.order,
[request.query.order],
'queries with the order as an array b/c that\'s what sequelize wants'
);
});

View File

@ -66,6 +66,11 @@ export const idParamsMethods = [
'update',
];
export const restrictMethods = [
'list',
'scope',
];
export default ({
method, attributeValidation, associationValidation, scopes = [], config = {},
}) => {
@ -74,6 +79,7 @@ export default ({
const hasPayload = payloadMethods.includes(method);
const hasScopeParams = scopeParamsMethods.includes(method);
const hasIdParams = idParamsMethods.includes(method);
const hasRestrictMethods = restrictMethods.includes(method);
// clone the config so we don't modify it on multiple passes.
let methodConfig = { ...config, validate: { ...config.validate } };
@ -133,5 +139,18 @@ export default ({
methodConfig = set(methodConfig, 'validate.params', params);
}
if (hasRestrictMethods) {
const query = concatToJoiObject(joi.object()
.keys({
limit: joi.number().min(0).integer(),
offset: joi.number().min(0).integer(),
order: joi.array(),
}),
get(methodConfig, 'validate.query')
);
methodConfig = set(methodConfig, 'validate.query', query);
}
return methodConfig;
};

View File

@ -7,6 +7,7 @@ import
payloadMethods,
scopeParamsMethods,
idParamsMethods,
restrictMethods,
sequelizeOperators,
} from './get-config-for-method.js';
@ -468,6 +469,101 @@ test('validate.payload idParamsMethods', (t) => {
});
});
test('validate.query restrictMethods', (t) => {
restrictMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method });
const { query } = configForMethod.validate;
const restrictKeys = ['limit', 'offset'];
restrictKeys.forEach((key) => {
t.ifError(
query.validate({ [key]: 0 }).error
, `applies restriction (${key}) to validate.query`
);
});
t.ifError(
query.validate({ order: ['thing', 'DESC'] }).error
, 'applies `order` to validate.query'
);
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.query restrictMethods w/ config as plain object', (t) => {
const config = {
validate: {
query: {
aKey: joi.boolean(),
},
},
};
restrictMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
config,
});
const { query } = configForMethod.validate;
const keys = [
...Object.keys(config.validate.query),
];
keys.forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.query restrictMethods w/ config as joi object', (t) => {
const queryKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
query: joi.object().keys(queryKeys),
},
};
whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
config,
});
const { query } = configForMethod.validate;
const keys = [
...Object.keys(queryKeys),
];
keys.forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('does not modify initial config on multiple passes', (t) => {
const { config } = t.context;
const originalConfig = { ...config };

View File

@ -1,9 +1,13 @@
import { omit, identity } from 'lodash';
import { omit, identity, toNumber, isString, isUndefined } from 'lodash';
import { notImplemented } from 'boom';
const sequelizeKeys = ['include', 'order', 'limit', 'offset'];
export const parseInclude = request => {
const include = Array.isArray(request.query.include) ? request.query.include
: [request.query.include];
const include = Array.isArray(request.query.include)
? request.query.include
: [request.query.include]
;
const noGetDb = typeof request.getDb !== 'function';
const noRequestModels = !request.models;
@ -27,7 +31,7 @@ export const parseInclude = request => {
};
export const parseWhere = request => {
const where = omit(request.query, 'include');
const where = omit(request.query, sequelizeKeys);
for (const key of Object.keys(where)) {
try {
@ -40,6 +44,38 @@ export const parseWhere = request => {
return where;
};
export const parseLimitAndOffset = (request) => {
const { limit, offset } = request.query;
const out = {};
if (!isUndefined(limit)) {
out.limit = toNumber(limit);
}
if (!isUndefined(offset)) {
out.offset = toNumber(offset);
}
return out;
};
export const parseOrder = (request) => {
const { order } = request.query;
if (!order) return null;
// transform to an array so sequelize will escape the input for us and
// maintain security. See http://docs.sequelizejs.com/en/latest/docs/querying/#ordering
if (isString(order)) return order.split(' ');
for (const key of Object.keys(order)) {
try {
order[key] = JSON.parse(order[key]);
} catch (e) {
//
}
}
return order;
};
export const getMethod = (model, association, plural = true, method = 'get') => {
const a = plural ? association.original.plural : association.original.singular;
const b = plural ? association.original.singular : association.original.plural; // alternative

113
src/utils.test.js Normal file
View File

@ -0,0 +1,113 @@
import test from 'ava';
import { parseLimitAndOffset, parseOrder, parseWhere } from './utils.js';
test.beforeEach((t) => {
t.context.request = { query: {} };
});
test('parseLimitAndOffset is a function', (t) => {
t.is(typeof parseLimitAndOffset, 'function');
});
test('parseLimitAndOffset returns limit and offset', (t) => {
const { request } = t.context;
request.query.limit = 1;
request.query.offset = 2;
request.query.thing = 'hi';
t.is(
parseLimitAndOffset(request).limit
, request.query.limit
);
t.is(
parseLimitAndOffset(request).offset
, request.query.offset
);
});
test('parseLimitAndOffset returns limit and offset as numbers', (t) => {
const { request } = t.context;
const limit = 1;
const offset = 2;
request.query.limit = `${limit}`;
request.query.offset = `${offset}`;
request.query.thing = 'hi';
t.is(
parseLimitAndOffset(request).limit
, limit
);
t.is(
parseLimitAndOffset(request).offset
, offset
);
});
test('parseOrder is a function', (t) => {
t.is(typeof parseOrder, 'function');
});
test('parseOrder returns order when a string', (t) => {
const { request } = t.context;
const order = 'thing';
request.query.order = order;
request.query.thing = 'hi';
t.deepEqual(
parseOrder(request)
, [order]
);
});
test('parseOrder returns order when json', (t) => {
const { request } = t.context;
const order = [{ model: 'User' }, 'DESC'];
request.query.order = [JSON.stringify({ model: 'User' }), 'DESC'];
request.query.thing = 'hi';
t.deepEqual(
parseOrder(request)
, order
);
});
test('parseOrder returns null when not defined', (t) => {
const { request } = t.context;
request.query.thing = 'hi';
t.is(
parseOrder(request)
, null
);
});
test('parseWhere is a function', (t) => {
t.is(typeof parseWhere, 'function');
});
test('parseWhere returns the non-sequelize keys', (t) => {
const { request } = t.context;
request.query.order = 'thing';
request.query.include = 'User';
request.query.limit = 2;
request.query.thing = 'hi';
t.deepEqual(
parseWhere(request)
, { thing: 'hi' }
);
});
test('parseWhere returns json converted keys', (t) => {
const { request } = t.context;
request.query.order = 'hi';
request.query.thing = '{"id": {"$in": [2, 3]}}';
t.deepEqual(
parseWhere(request)
, { thing: { id: { $in: [2, 3] } } }
);
});