Cleanup and add tests

Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
Alexandre Bodin 2020-06-26 12:59:53 +02:00
parent 6776a3ce46
commit c8d743ef60
2 changed files with 143 additions and 60 deletions

View File

@ -1,6 +1,8 @@
const _ = require('lodash');
const { singular } = require('pluralize');
const BOOLEAN_OPERATORS = ['or'];
/**
* Build filters on a bookshelf query
* @param {Object} options - Options
@ -136,6 +138,7 @@ const buildJoinsAndFilter = (qb, model, whereClauses) => {
};
};
// tree made to create the joins strucutre
const tree = {
alias: model.collectionName,
assoc: null,
@ -143,6 +146,12 @@ const buildJoinsAndFilter = (qb, model, whereClauses) => {
joins: {},
};
/**
* Returns the SQL path for a qery field.
* Adds table to the joins tree
* @param {string} field a field used to filter
* @param {Object} tree joins tree
*/
const generateNestedJoins = (field, tree) => {
let [key, ...parts] = field.split('.');
@ -168,11 +177,18 @@ const buildJoinsAndFilter = (qb, model, whereClauses) => {
return generateNestedJoins(parts.join('.'), tree.joins[key]);
};
/**
* Format every where clauses whith the right table name aliases.
* Add table joins to the joins list
* @param {Array<{field, operator, value}>} whereClauses a list of where clauses
* @param {Object} context
* @param {Object} context.model model on which the query is run
*/
const buildWhereClauses = (whereClauses, { model }) => {
return whereClauses.map(whereClause => {
const { field, operator, value } = whereClause;
if (operator === 'or') {
if (BOOLEAN_OPERATORS.includes(operator)) {
return { field, operator, value: value.map(v => buildWhereClauses(v, { model })) };
}
@ -216,13 +232,13 @@ const buildWhereClause = ({ qb, field, operator, value }) => {
case 'or':
return qb.where(orQb => {
value.forEach(orClause => {
orQb.orWhere(q => {
orQb.orWhere(subQb => {
if (Array.isArray(orClause)) {
orClause.forEach(orClause =>
q.where(qq => buildWhereClause({ qb: qq, ...orClause }))
subQb.where(andQb => buildWhereClause({ qb: andQb, ...orClause }))
);
} else {
buildWhereClause({ qb: q, ...orClause });
buildWhereClause({ qb: subQb, ...orClause });
}
});
});

View File

@ -171,9 +171,7 @@ describe('Filtering API', () => {
});
expect(res.body).toEqual(
expect.arrayContaining(
data.products.map(o => expect.objectContaining(o))
)
expect.arrayContaining(data.products.map(o => expect.objectContaining(o)))
);
});
@ -186,9 +184,7 @@ describe('Filtering API', () => {
},
});
expect(res.body).toEqual(
expect.not.arrayContaining([data.products[0]])
);
expect(res.body).toEqual(expect.not.arrayContaining([data.products[0]]));
});
});
@ -235,9 +231,7 @@ describe('Filtering API', () => {
});
expect(res1.body).toEqual(
expect.arrayContaining(
data.products.map(o => expect.objectContaining(o))
)
expect.arrayContaining(data.products.map(o => expect.objectContaining(o)))
);
const res2 = await rq({
@ -285,9 +279,7 @@ describe('Filtering API', () => {
});
expect(res.body).toEqual(
expect.arrayContaining(
data.products.map(o => expect.objectContaining(o))
)
expect.arrayContaining(data.products.map(o => expect.objectContaining(o)))
);
const res2 = await rq({
@ -426,9 +418,7 @@ describe('Filtering API', () => {
},
});
expect(res.body).toEqual(
expect.not.arrayContaining([data.products[0]])
);
expect(res.body).toEqual(expect.not.arrayContaining([data.products[0]]));
});
test('Should return an array without the values matching when an array of values is provided', async () => {
@ -440,9 +430,7 @@ describe('Filtering API', () => {
},
});
expect(res.body).toEqual(
expect.not.arrayContaining([data.products[0]])
);
expect(res.body).toEqual(expect.not.arrayContaining([data.products[0]]));
});
test('Should return an array with values that do not match the filter', async () => {
@ -468,9 +456,7 @@ describe('Filtering API', () => {
},
});
expect(res.body).toEqual(
expect.not.arrayContaining([data.products[0]])
);
expect(res.body).toEqual(expect.not.arrayContaining([data.products[0]]));
const res2 = await rq({
method: 'GET',
@ -552,9 +538,7 @@ describe('Filtering API', () => {
},
});
expect(res2.body).toEqual(
expect.not.arrayContaining([data.products[0]])
);
expect(res2.body).toEqual(expect.not.arrayContaining([data.products[0]]));
});
test('Should work with integers', async () => {
@ -616,9 +600,7 @@ describe('Filtering API', () => {
},
});
expect(res.body).toEqual(
expect.not.arrayContaining([data.products[0]])
);
expect(res.body).toEqual(expect.not.arrayContaining([data.products[0]]));
const res2 = await rq({
method: 'GET',
@ -700,9 +682,7 @@ describe('Filtering API', () => {
},
});
expect(res2.body).toEqual(
expect.not.arrayContaining([data.products[0]])
);
expect(res2.body).toEqual(expect.not.arrayContaining([data.products[0]]));
});
test('Should work with integers', async () => {
@ -756,6 +736,108 @@ describe('Filtering API', () => {
});
describe('Or filtering', () => {
describe('_or filter', () => {
test('Supports simple or', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
_where: {
_or: [
{
rank: 42,
},
{
rank: 82,
},
],
},
},
});
expect(res.body).toEqual(expect.arrayContaining([data.products[0], data.products[1]]));
});
test('Supports simple or on different fields', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
_where: {
_or: [
{
rank: 42,
},
{
price_gt: 28,
},
],
},
},
});
expect(res.body).toEqual(
expect.arrayContaining([data.products[0], data.products[1], data.products[2]])
);
});
test('Supports or with nested and', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
_where: {
_or: [
{
rank: 42,
},
[
{
price_gt: 28,
},
{
rank: 91,
},
],
],
},
},
});
expect(res.body).toEqual(expect.arrayContaining([data.products[0], data.products[2]]));
});
test('Supports or with nested or', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
_where: {
_or: [
{
rank: 42,
},
[
{
price_gt: 28,
},
{
_or: [
{
rank: 91,
},
],
},
],
],
},
},
});
expect(res.body).toEqual(expect.arrayContaining([data.products[0], data.products[2]]));
});
});
test('Filter equals', async () => {
const res = await rq({
method: 'GET',
@ -934,9 +1016,7 @@ describe('Filtering API', () => {
},
});
expect(res.body).toEqual(
expect.not.arrayContaining([data.products[1], data.products[2]])
);
expect(res.body).toEqual(expect.not.arrayContaining([data.products[1], data.products[2]]));
expect(res.body).toEqual(expect.arrayContaining([data.products[0]]));
res = await rq({
@ -976,9 +1056,7 @@ describe('Filtering API', () => {
});
expect(res.body).toEqual(
expect.arrayContaining(
data.products.slice(0).sort((a, b) => a.rank - b.rank)
)
expect.arrayContaining(data.products.slice(0).sort((a, b) => a.rank - b.rank))
);
});
@ -992,9 +1070,7 @@ describe('Filtering API', () => {
});
expect(res.body).toEqual(
expect.arrayContaining(
data.products.slice(0).sort((a, b) => a.rank - b.rank)
)
expect.arrayContaining(data.products.slice(0).sort((a, b) => a.rank - b.rank))
);
const res2 = await rq({
@ -1006,9 +1082,7 @@ describe('Filtering API', () => {
});
expect(res2.body).toEqual(
expect.arrayContaining(
data.products.slice(0).sort((a, b) => b.rank - a.rank)
)
expect.arrayContaining(data.products.slice(0).sort((a, b) => b.rank - a.rank))
);
});
@ -1021,14 +1095,11 @@ describe('Filtering API', () => {
},
});
[
data.products[3],
data.products[0],
data.products[2],
data.products[1],
].forEach(expectedPost => {
expect(res.body).toEqual(expect.arrayContaining([expectedPost]));
});
[data.products[3], data.products[0], data.products[2], data.products[1]].forEach(
expectedPost => {
expect(res.body).toEqual(expect.arrayContaining([expectedPost]));
}
);
});
});
@ -1055,9 +1126,7 @@ describe('Filtering API', () => {
},
});
expect(res.body).toEqual(
expect.arrayContaining([data.products[data.products.length - 1]])
);
expect(res.body).toEqual(expect.arrayContaining([data.products[data.products.length - 1]]));
});
test('Offset', async () => {
@ -1082,9 +1151,7 @@ describe('Filtering API', () => {
},
});
expect(res.body).toEqual(
expect.arrayContaining(data.products.slice(1, 2))
);
expect(res.body).toEqual(expect.arrayContaining(data.products.slice(1, 2)));
});
});