Feat add support of limit, offset, order
Allows passing these as query params to list and scope methods.
This commit is contained in:
12
src/crud.js
12
src/crud.js
@ -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()));
|
||||
},
|
||||
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 };
|
||||
|
44
src/utils.js
44
src/utils.js
@ -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
113
src/utils.test.js
Normal 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] } } }
|
||||
);
|
||||
});
|
Reference in New Issue
Block a user