Allow or operator and working with bookshelf

Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
Alexandre Bodin 2020-06-25 20:20:02 +02:00
parent 987eb9fc7e
commit 82316bbf3a
4 changed files with 127 additions and 90 deletions

View File

@ -38,11 +38,11 @@ const buildQuery = ({ model, filters }) => qb => {
* @param {Array<Object>} whereClauses - an array of where clause * @param {Array<Object>} whereClauses - an array of where clause
*/ */
const buildJoinsAndFilter = (qb, model, whereClauses) => { const buildJoinsAndFilter = (qb, model, whereClauses) => {
const aliasMap = {};
/** /**
* Returns an alias for a name (simple incremental alias name) * Returns an alias for a name (simple incremental alias name)
* @param {string} name - name to alias * @param {string} name - name to alias
*/ */
const aliasMap = {};
const generateAlias = name => { const generateAlias = name => {
if (!aliasMap[name]) { if (!aliasMap[name]) {
aliasMap[name] = 1; aliasMap[name] = 1;
@ -58,17 +58,14 @@ const buildJoinsAndFilter = (qb, model, whereClauses) => {
* @param {Object} qb - Knex query builder * @param {Object} qb - Knex query builder
* @param {Object} tree - Query tree * @param {Object} tree - Query tree
*/ */
const buildQueryFromTree = (qb, queryTree) => { const buildJoinsFromTree = (qb, queryTree) => {
// build joins // build joins
Object.keys(queryTree.children).forEach(key => { Object.keys(queryTree.joins).forEach(key => {
const subQueryTree = queryTree.children[key]; const subQueryTree = queryTree.joins[key];
buildJoin(qb, subQueryTree.assoc, queryTree, subQueryTree); buildJoin(qb, subQueryTree.assoc, queryTree, subQueryTree);
buildQueryFromTree(qb, subQueryTree); buildJoinsFromTree(qb, subQueryTree);
}); });
// build where clauses
queryTree.where.forEach(w => buildWhereClause({ qb, ...w }));
}; };
/** /**
@ -135,32 +132,24 @@ const buildJoinsAndFilter = (qb, model, whereClauses) => {
alias: generateAlias(model.collectionName), alias: generateAlias(model.collectionName),
assoc, assoc,
model, model,
where: [], joins: {},
children: {},
}; };
}; };
/** const tree = {
* Builds a Strapi query tree easy alias: model.collectionName,
* @param {Array<Object>} whereClauses - Array of Strapi where clause assoc: null,
* @param {Object} model - Strapi model model,
* @param {Object} queryTree - queryTree joins: {},
*/ };
const buildQueryTree = (whereClauses, model, queryTree) => {
for (let whereClause of whereClauses) { const generateNestedJoins = (field, tree) => {
const { field, operator, value } = whereClause;
let [key, ...parts] = field.split('.'); let [key, ...parts] = field.split('.');
const assoc = findAssoc(model, key); const assoc = findAssoc(tree.model, key);
// if the key is an attribute add as where clause // if the key is an attribute add as where clause
if (!assoc) { if (!assoc) {
queryTree.where.push({ return `${tree.alias}.${key}`;
field: `${queryTree.alias}.${key}`,
operator,
value,
});
continue;
} }
const assocModel = strapi.db.getModelByAssoc(assoc); const assocModel = strapi.db.getModelByAssoc(assoc);
@ -172,34 +161,37 @@ const buildJoinsAndFilter = (qb, model, whereClauses) => {
} }
// init sub query tree // init sub query tree
if (!queryTree.children[key]) { if (!tree.joins[key]) {
queryTree.children[key] = createTreeNode(assocModel, assoc); tree.joins[key] = createTreeNode(assocModel, assoc);
} }
buildQueryTree( return generateNestedJoins(parts.join('.'), tree.joins[key]);
[
{
field: parts.join('.'),
operator,
value,
},
],
assocModel,
queryTree.children[key]
);
}
return queryTree;
}; };
const root = buildQueryTree(whereClauses, model, { const buildWhereClauses = (whereClauses, { model }) => {
alias: model.collectionName, return whereClauses.map(whereClause => {
assoc: null, const { field, operator, value } = whereClause;
model,
where: [], if (operator === 'or') {
children: {}, return { field, operator, value: value.map(v => buildWhereClauses(v, { model })) };
}
const path = generateNestedJoins(field, tree);
return {
field: path,
operator,
value,
};
}); });
return buildQueryFromTree(qb, root); };
const aliasedWhereClauses = buildWhereClauses(whereClauses, { model });
buildJoinsFromTree(qb, tree);
aliasedWhereClauses.forEach(w => buildWhereClause({ qb, ...w }));
return;
}; };
/** /**
@ -212,7 +204,7 @@ const buildJoinsAndFilter = (qb, model, whereClauses) => {
* @param {Object} options.value - Filter value * @param {Object} options.value - Filter value
*/ */
const buildWhereClause = ({ qb, field, operator, value }) => { const buildWhereClause = ({ qb, field, operator, value }) => {
if (Array.isArray(value) && !['in', 'nin'].includes(operator)) { if (Array.isArray(value) && !['or', 'in', 'nin'].includes(operator)) {
return qb.where(subQb => { return qb.where(subQb => {
for (let val of value) { for (let val of value) {
subQb.orWhere(q => buildWhereClause({ qb: q, field, operator, value: val })); subQb.orWhere(q => buildWhereClause({ qb: q, field, operator, value: val }));
@ -221,6 +213,20 @@ const buildWhereClause = ({ qb, field, operator, value }) => {
} }
switch (operator) { switch (operator) {
case 'or':
return qb.where(orQb => {
value.forEach(orClause => {
orQb.orWhere(q => {
if (Array.isArray(orClause)) {
orClause.forEach(orClause =>
q.where(qq => buildWhereClause({ qb: qq, ...orClause }))
);
} else {
buildWhereClause({ qb: q, ...orClause });
}
});
});
});
case 'eq': case 'eq':
return qb.where(field, value); return qb.where(field, value);
case 'ne': case 'ne':

View File

@ -88,27 +88,32 @@ const normalizeFieldName = ({ model, field }) => {
: fieldPath.join('.'); : fieldPath.join('.');
}; };
/** const BOOLEAN_OPERATORS = ['or'];
*
* @param {Object} options - Options const hasDeepFilters = whereClauses => {
* @param {Object} options.model - The model for which the query will be built return (
* @param {Object} options.filters - The filters for the query (start, sort, limit, and where clauses) whereClauses.filter(({ field, operator, value }) => {
* @param {Object} options.rest - In case the database layer requires any other params pass them if (BOOLEAN_OPERATORS.includes(operator)) {
*/ return value.filter(hasDeepFilters).length > 0;
const buildQuery = ({ model, filters = {}, ...rest }) => {
// Validate query clauses
if (filters.where && Array.isArray(filters.where)) {
const deepFilters = filters.where.filter(({ field }) => field.split('.').length > 1);
if (deepFilters.length > 0) {
strapi.log.warn(
'Deep filtering queries should be used carefully (e.g Can cause performance issues).\nWhen possible build custom routes which will in most case be more optimised.'
);
} }
// cast where clauses to match the inner types return field.split('.').length > 1;
filters.where = filters.where }).length > 0
);
};
const normalizeClauses = (whereClauses, { model }) => {
return whereClauses
.filter(({ value }) => !_.isNil(value)) .filter(({ value }) => !_.isNil(value))
.map(({ field, operator, value }) => { .map(({ field, operator, value }) => {
if (BOOLEAN_OPERATORS.includes(operator)) {
return {
field,
operator,
value: value.map(clauses => normalizeClauses(clauses, { model })),
};
}
const { model: assocModel, attribute } = getAssociationFromFieldKey({ const { model: assocModel, attribute } = getAssociationFromFieldKey({
model, model,
field, field,
@ -125,6 +130,26 @@ const buildQuery = ({ model, filters = {}, ...rest }) => {
value: castedValue, value: castedValue,
}; };
}); });
};
/**
*
* @param {Object} options - Options
* @param {Object} options.model - The model for which the query will be built
* @param {Object} options.filters - The filters for the query (start, sort, limit, and where clauses)
* @param {Object} options.rest - In case the database layer requires any other params pass them
*/
const buildQuery = ({ model, filters = {}, ...rest }) => {
// Validate query clauses
if (filters.where && Array.isArray(filters.where)) {
if (hasDeepFilters(filters.where)) {
strapi.log.warn(
'Deep filtering queries should be used carefully (e.g Can cause performance issues).\nWhen possible build custom routes which will in most case be more optimised.'
);
}
// cast where clauses to match the inner types
filters.where = normalizeClauses(filters.where, { model });
} }
// call the orm's buildQuery implementation // call the orm's buildQuery implementation

View File

@ -130,6 +130,8 @@ const VALID_OPERATORS = [
'null', 'null',
]; ];
const BOOLEAN_OPERATORS = ['or'];
/** /**
* Parse where params * Parse where params
*/ */
@ -175,6 +177,10 @@ const convertWhereClause = (whereClause, value) => {
const field = whereClause.substring(0, separatorIndex); const field = whereClause.substring(0, separatorIndex);
const operator = whereClause.slice(separatorIndex + 1); const operator = whereClause.slice(separatorIndex + 1);
if (BOOLEAN_OPERATORS.includes(operator) && field === '') {
return { field: null, operator, value: [].concat(value).map(convertWhereParams) };
}
// the field as underscores // the field as underscores
if (!VALID_OPERATORS.includes(operator)) { if (!VALID_OPERATORS.includes(operator)) {
return { field: whereClause, value }; return { field: whereClause, value };

View File

@ -16,7 +16,7 @@ const addQsParser = app => {
get() { get() {
const qstr = this.querystring; const qstr = this.querystring;
const cache = (this._querycache = this._querycache || {}); const cache = (this._querycache = this._querycache || {});
return cache[qstr] || (cache[qstr] = qs.parse(qstr)); return cache[qstr] || (cache[qstr] = qs.parse(qstr, { depth: 20 }));
}, },
/* /*