diff --git a/packages/strapi-connector-bookshelf/lib/buildQuery.js b/packages/strapi-connector-bookshelf/lib/buildQuery.js index 24925d37be..1764bdad77 100644 --- a/packages/strapi-connector-bookshelf/lib/buildQuery.js +++ b/packages/strapi-connector-bookshelf/lib/buildQuery.js @@ -38,11 +38,11 @@ const buildQuery = ({ model, filters }) => qb => { * @param {Array} 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} 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': diff --git a/packages/strapi-utils/lib/build-query.js b/packages/strapi-utils/lib/build-query.js index aa04fecaec..8380138c82 100644 --- a/packages/strapi-utils/lib/build-query.js +++ b/packages/strapi-utils/lib/build-query.js @@ -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 diff --git a/packages/strapi-utils/lib/convert-rest-query-params.js b/packages/strapi-utils/lib/convert-rest-query-params.js index 6b2faa5d26..a6263219c9 100644 --- a/packages/strapi-utils/lib/convert-rest-query-params.js +++ b/packages/strapi-utils/lib/convert-rest-query-params.js @@ -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 }; diff --git a/packages/strapi/lib/middlewares/parser/index.js b/packages/strapi/lib/middlewares/parser/index.js index 61e4a29548..7f5c03a4bf 100644 --- a/packages/strapi/lib/middlewares/parser/index.js +++ b/packages/strapi/lib/middlewares/parser/index.js @@ -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 })); }, /*