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,71 +132,66 @@ 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
* @param {Array<Object>} whereClauses - Array of Strapi where clause
* @param {Object} model - Strapi model
* @param {Object} queryTree - queryTree
*/
const buildQueryTree = (whereClauses, model, queryTree) => {
for (let whereClause of whereClauses) {
const { field, operator, value } = whereClause;
let [key, ...parts] = field.split('.');
const assoc = findAssoc(model, key);
// if the key is an attribute add as where clause
if (!assoc) {
queryTree.where.push({
field: `${queryTree.alias}.${key}`,
operator,
value,
});
continue;
}
const assocModel = strapi.db.getModelByAssoc(assoc);
// if the last part of the path is an association
// add the primary key of the model to the parts
if (parts.length === 0) {
parts = [assocModel.primaryKey];
}
// init sub query tree
if (!queryTree.children[key]) {
queryTree.children[key] = createTreeNode(assocModel, assoc);
}
buildQueryTree(
[
{
field: parts.join('.'),
operator,
value,
},
],
assocModel,
queryTree.children[key]
);
}
return queryTree;
};
const root = buildQueryTree(whereClauses, model, {
alias: model.collectionName, alias: model.collectionName,
assoc: null, assoc: null,
model, model,
where: [], joins: {},
children: {}, };
});
return buildQueryFromTree(qb, root); const generateNestedJoins = (field, tree) => {
let [key, ...parts] = field.split('.');
const assoc = findAssoc(tree.model, key);
// if the key is an attribute add as where clause
if (!assoc) {
return `${tree.alias}.${key}`;
}
const assocModel = strapi.db.getModelByAssoc(assoc);
// if the last part of the path is an association
// add the primary key of the model to the parts
if (parts.length === 0) {
parts = [assocModel.primaryKey];
}
// init sub query tree
if (!tree.joins[key]) {
tree.joins[key] = createTreeNode(assocModel, assoc);
}
return generateNestedJoins(parts.join('.'), tree.joins[key]);
};
const buildWhereClauses = (whereClauses, { model }) => {
return whereClauses.map(whereClause => {
const { field, operator, value } = whereClause;
if (operator === 'or') {
return { field, operator, value: value.map(v => buildWhereClauses(v, { model })) };
}
const path = generateNestedJoins(field, tree);
return {
field: path,
operator,
value,
};
});
};
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,6 +88,50 @@ const normalizeFieldName = ({ model, field }) => {
: fieldPath.join('.'); : fieldPath.join('.');
}; };
const BOOLEAN_OPERATORS = ['or'];
const hasDeepFilters = whereClauses => {
return (
whereClauses.filter(({ field, operator, value }) => {
if (BOOLEAN_OPERATORS.includes(operator)) {
return value.filter(hasDeepFilters).length > 0;
}
return field.split('.').length > 1;
}).length > 0
);
};
const normalizeClauses = (whereClauses, { model }) => {
return whereClauses
.filter(({ value }) => !_.isNil(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({
model,
field,
});
const { type } = _.get(assocModel, ['allAttributes', attribute], {});
// cast value or array of values
const castedValue = castInput({ type, operator, value });
return {
field: normalizeFieldName({ model, field }),
operator,
value: castedValue,
};
});
};
/** /**
* *
* @param {Object} options - Options * @param {Object} options - Options
@ -98,33 +142,14 @@ const normalizeFieldName = ({ model, field }) => {
const buildQuery = ({ model, filters = {}, ...rest }) => { const buildQuery = ({ model, filters = {}, ...rest }) => {
// Validate query clauses // Validate query clauses
if (filters.where && Array.isArray(filters.where)) { if (filters.where && Array.isArray(filters.where)) {
const deepFilters = filters.where.filter(({ field }) => field.split('.').length > 1); if (hasDeepFilters(filters.where)) {
if (deepFilters.length > 0) {
strapi.log.warn( 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.' '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 // cast where clauses to match the inner types
filters.where = filters.where filters.where = normalizeClauses(filters.where, { model });
.filter(({ value }) => !_.isNil(value))
.map(({ field, operator, value }) => {
const { model: assocModel, attribute } = getAssociationFromFieldKey({
model,
field,
});
const { type } = _.get(assocModel, ['allAttributes', attribute], {});
// cast value or array of values
const castedValue = castInput({ type, operator, value });
return {
field: normalizeFieldName({ model, field }),
operator,
value: castedValue,
};
});
} }
// 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 }));
}, },
/* /*