mirror of
https://github.com/strapi/strapi.git
synced 2025-11-01 18:33:55 +00:00
Allow or operator and working with bookshelf
Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
parent
987eb9fc7e
commit
82316bbf3a
@ -38,11 +38,11 @@ const buildQuery = ({ model, filters }) => qb => {
|
||||
* @param {Array<Object>} whereClauses - an array of where clause
|
||||
*/
|
||||
const buildJoinsAndFilter = (qb, model, whereClauses) => {
|
||||
const aliasMap = {};
|
||||
/**
|
||||
* Returns an alias for a name (simple incremental alias name)
|
||||
* @param {string} name - name to alias
|
||||
*/
|
||||
const aliasMap = {};
|
||||
const generateAlias = name => {
|
||||
if (!aliasMap[name]) {
|
||||
aliasMap[name] = 1;
|
||||
@ -58,17 +58,14 @@ const buildJoinsAndFilter = (qb, model, whereClauses) => {
|
||||
* @param {Object} qb - Knex query builder
|
||||
* @param {Object} tree - Query tree
|
||||
*/
|
||||
const buildQueryFromTree = (qb, queryTree) => {
|
||||
const buildJoinsFromTree = (qb, queryTree) => {
|
||||
// build joins
|
||||
Object.keys(queryTree.children).forEach(key => {
|
||||
const subQueryTree = queryTree.children[key];
|
||||
Object.keys(queryTree.joins).forEach(key => {
|
||||
const subQueryTree = queryTree.joins[key];
|
||||
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),
|
||||
assoc,
|
||||
model,
|
||||
where: [],
|
||||
children: {},
|
||||
joins: {},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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, {
|
||||
const tree = {
|
||||
alias: model.collectionName,
|
||||
assoc: null,
|
||||
model,
|
||||
where: [],
|
||||
children: {},
|
||||
});
|
||||
return buildQueryFromTree(qb, root);
|
||||
joins: {},
|
||||
};
|
||||
|
||||
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
|
||||
*/
|
||||
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 => {
|
||||
for (let val of value) {
|
||||
subQb.orWhere(q => buildWhereClause({ qb: q, field, operator, value: val }));
|
||||
@ -221,6 +213,20 @@ const buildWhereClause = ({ qb, field, operator, value }) => {
|
||||
}
|
||||
|
||||
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':
|
||||
return qb.where(field, value);
|
||||
case 'ne':
|
||||
|
||||
@ -88,6 +88,50 @@ const normalizeFieldName = ({ model, field }) => {
|
||||
: 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
|
||||
@ -98,33 +142,14 @@ const normalizeFieldName = ({ model, field }) => {
|
||||
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) {
|
||||
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 = filters.where
|
||||
.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,
|
||||
};
|
||||
});
|
||||
filters.where = normalizeClauses(filters.where, { model });
|
||||
}
|
||||
|
||||
// call the orm's buildQuery implementation
|
||||
|
||||
@ -130,6 +130,8 @@ const VALID_OPERATORS = [
|
||||
'null',
|
||||
];
|
||||
|
||||
const BOOLEAN_OPERATORS = ['or'];
|
||||
|
||||
/**
|
||||
* Parse where params
|
||||
*/
|
||||
@ -175,6 +177,10 @@ const convertWhereClause = (whereClause, value) => {
|
||||
const field = whereClause.substring(0, separatorIndex);
|
||||
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
|
||||
if (!VALID_OPERATORS.includes(operator)) {
|
||||
return { field: whereClause, value };
|
||||
|
||||
@ -16,7 +16,7 @@ const addQsParser = app => {
|
||||
get() {
|
||||
const qstr = this.querystring;
|
||||
const cache = (this._querycache = this._querycache || {});
|
||||
return cache[qstr] || (cache[qstr] = qs.parse(qstr));
|
||||
return cache[qstr] || (cache[qstr] = qs.parse(qstr, { depth: 20 }));
|
||||
},
|
||||
|
||||
/*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user