Add deep sort to mongoose, fix sort order on both connectors, cleanup

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>
This commit is contained in:
Convly 2020-12-08 11:30:05 +01:00
parent 95784438ce
commit 92451db825
3 changed files with 95 additions and 59 deletions

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
const { each, prop } = require('lodash/fp'); const { each, prop, reject, isEmpty } = require('lodash/fp');
const { singular } = require('pluralize'); const { singular } = require('pluralize');
const { toQueries, runPopulateQueries } = require('./utils/populate-queries'); const { toQueries, runPopulateQueries } = require('./utils/populate-queries');
@ -18,18 +18,12 @@ const buildQuery = ({ model, filters }) => qb => {
qb.distinct(); qb.distinct();
} }
if (_.has(filters, 'sort')) { const joinsTree = buildJoinsAndFilter(qb, model, filters);
qb.orderBy(
filters.sort
.filter(({ field }) => !field.includes('.'))
.map(({ field, order }) => ({
column: field,
order,
}))
);
}
buildJoinsAndFilter(qb, model, filters); if (_.has(filters, 'sort')) {
const clauses = filters.sort.map(buildSortClauseFromTree(joinsTree));
qb.orderBy(reject(isEmpty, clauses));
}
if (_.has(filters, 'start')) { if (_.has(filters, 'start')) {
qb.offset(filters.start); qb.offset(filters.start);
@ -47,6 +41,25 @@ const buildQuery = ({ model, filters }) => qb => {
} }
}; };
/**
* Build a bookshelf sort clause (simple or deep) based on a joins tree
* @param tree - The joins tree that contains the aliased associations
*/
const buildSortClauseFromTree = tree => ({ field, order }) => {
if (!field.includes('.')) {
return { column: field, order };
}
const [relation, attribute] = field.split('.');
for (const { alias, assoc } of Object.values(tree.joins)) {
if (relation === assoc.alias) {
return { column: `${alias}.${attribute}`, order };
}
}
return {};
};
/** /**
* Add joins and where filters * Add joins and where filters
* @param {Object} qb - knex query builder * @param {Object} qb - knex query builder
@ -226,20 +239,7 @@ const buildJoinsAndFilter = (qb, model, filters) => {
*/ */
const addFiltersQueriesToJoinTree = tree => { const addFiltersQueriesToJoinTree = tree => {
_.each(tree.joins, value => { _.each(tree.joins, value => {
const { alias, model, assoc } = value; const { alias, model } = value;
// Sort
sortClauses
// Keep only nested clauses
.filter(({ field }) => field.includes('.'))
// If the sort is linked to the current join, then add an orderBy
.forEach(({ field, order }) => {
const [relation, attributeName] = field.split('.');
if (relation === assoc.alias) {
qb.orderBy(`${alias}.${attributeName}`, order);
}
});
// PublicationState // PublicationState
runPopulateQueries( runPopulateQueries(
@ -256,10 +256,13 @@ const buildJoinsAndFilter = (qb, model, filters) => {
const aliasedWhereClauses = buildWhereClauses(whereClauses, { model }); const aliasedWhereClauses = buildWhereClauses(whereClauses, { model });
aliasedWhereClauses.forEach(w => buildWhereClause({ qb, ...w })); aliasedWhereClauses.forEach(w => buildWhereClause({ qb, ...w }));
// Force needed joins for deep sort clauses
generateNestedJoinsFromFields(sortClauses.map(prop('field'))); generateNestedJoinsFromFields(sortClauses.map(prop('field')));
buildJoinsFromTree(qb, tree); buildJoinsFromTree(qb, tree);
addFiltersQueriesToJoinTree(tree); addFiltersQueriesToJoinTree(tree);
return tree;
}; };
/** /**

View File

@ -1,7 +1,8 @@
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
var semver = require('semver'); const { gt, isEmpty, reject, set } = require('lodash/fp');
const semver = require('semver');
const { const {
hasDeepFilters, hasDeepFilters,
contentTypes: { contentTypes: {
@ -12,6 +13,11 @@ const {
const utils = require('./utils')(); const utils = require('./utils')();
const populateQueries = require('./utils/populate-queries'); const populateQueries = require('./utils/populate-queries');
const sortOrderMapper = {
asc: 1,
desc: -1,
};
const combineSearchAndWhere = (search = [], wheres = []) => { const combineSearchAndWhere = (search = [], wheres = []) => {
const criterias = {}; const criterias = {};
if (search.length > 0 && wheres.length > 0) { if (search.length > 0 && wheres.length > 0) {
@ -88,8 +94,9 @@ const buildQuery = ({
aggregate = false, aggregate = false,
} = {}) => { } = {}) => {
const search = buildSearchOr(model, searchParam); const search = buildSearchOr(model, searchParam);
const { where, sort } = filters;
if (!hasDeepFilters(filters.where) && aggregate === false) { if (!hasDeepFilters({ where, sort }) && aggregate === false) {
return buildSimpleQuery({ model, filters, search, populate }); return buildSimpleQuery({ model, filters, search, populate });
} }
@ -171,7 +178,14 @@ const buildDeepQuery = ({ model, filters, search, populate }) => {
) )
.populate(populate); .populate(populate);
return applyQueryParams({ model, query, filters }); const stringIds = ids.map(id => id.toString());
const getIndexInIds = obj => stringIds.indexOf(obj._id.toString());
return (
applyQueryParams({ model, query, filters })
// Reorder results using `ids`
.then(results => results.sort((a, b) => (gt(...[a, b].map(getIndexInIds)) ? 1 : -1)))
);
}) })
.then(...args); .then(...args);
}, },
@ -210,17 +224,6 @@ const buildDeepQuery = ({ model, filters, search, populate }) => {
* @param {Object} options.filters - Filters object * @param {Object} options.filters - Filters object
*/ */
const applyQueryParams = ({ model, query, filters }) => { const applyQueryParams = ({ model, query, filters }) => {
// Apply sort param
if (_.has(filters, 'sort')) {
const sortFilter = filters.sort.reduce((acc, sort) => {
const { field, order } = sort;
acc[field] = order === 'asc' ? 1 : -1;
return acc;
}, {});
query = query.sort(sortFilter);
}
// Apply start param // Apply start param
if (_.has(filters, 'start')) { if (_.has(filters, 'start')) {
query = query.skip(filters.start); query = query.skip(filters.start);
@ -315,9 +318,25 @@ const pathsToTree = paths => paths.reduce((acc, path) => _.merge(acc, _.set({},
* @param {Object} options.paths - A tree of paths to aggregate e.g { article : { tags : { label: {}}}} * @param {Object} options.paths - A tree of paths to aggregate e.g { article : { tags : { label: {}}}}
*/ */
const buildQueryAggregate = (model, filters, { paths } = {}) => { const buildQueryAggregate = (model, filters, { paths } = {}) => {
return Object.keys(paths).reduce((acc, key) => { const aggregate = [];
return acc.concat(buildLookup({ model, key, paths: paths[key], filters }));
}, []); // Build lookups
Object.keys(paths).map(key =>
aggregate.push(buildLookup({ key, paths: paths[key], model, filters }))
);
// Build the sort operation
if (Array.isArray(filters.sort) && !isEmpty(filters.sort)) {
aggregate.push({
$sort: filters.sort.reduce(
(acc, { field, order }) => set([field], sortOrderMapper[order], acc),
{}
),
});
}
// Remove empty aggregate clauses & return
return reject(isEmpty, aggregate);
}; };
/** /**
@ -657,7 +676,7 @@ const findModelByPath = ({ rootModel, path }) => {
* Returns a model path from an attribute path and a root model * Returns a model path from an attribute path and a root model
* @param {Object} options - Options * @param {Object} options - Options
* @param {Object} options.rootModel - Mongoose model * @param {Object} options.rootModel - Mongoose model
* @param {string} options.path - Attribute path * @param {string|Object} options.path - Attribute path
*/ */
const findModelPath = ({ rootModel, path }) => { const findModelPath = ({ rootModel, path }) => {
const parts = (_.isObject(path) ? path.path : path).split('.'); const parts = (_.isObject(path) ? path.path : path).split('.');

View File

@ -11,7 +11,7 @@ const isAttribute = (model, field) =>
/** /**
* Returns the model, attribute name and association from a path of relation * Returns the model, attribute name and association from a path of relation
* @param {Object} options - Options * @param {Object} options - Options
* @param {string} options.model - Strapi model * @param {Object} options.model - Strapi model
* @param {string} options.field - path of relation / attribute * @param {string} options.field - path of relation / attribute
*/ */
const getAssociationFromFieldKey = ({ model, field }) => { const getAssociationFromFieldKey = ({ model, field }) => {
@ -92,16 +92,21 @@ const normalizeFieldName = ({ model, field }) => {
const BOOLEAN_OPERATORS = ['or']; const BOOLEAN_OPERATORS = ['or'];
const hasDeepFilters = (whereClauses = [], { minDepth = 1 } = {}) => { const hasDeepFilters = ({ where = [], sort = [], minDepth = 1 } = {}) => {
return ( // A query uses deep filtering if some of the clauses contains a sort or a match expression on a field of a relation
whereClauses.filter(({ field, operator, value }) => {
if (BOOLEAN_OPERATORS.includes(operator)) {
return value.filter(hasDeepFilters).length > 0;
}
return field.split('.').length > minDepth; // We don't use minDepth here because deep sorting is limited to depth 1
}).length > 0 const hasDeepSortClauses = sort.some(({ field }) => field.includes('.'));
);
const hasDeepWhereClauses = where.some(({ field, operator, value }) => {
if (BOOLEAN_OPERATORS.includes(operator)) {
return value.some(clauses => hasDeepFilters({ where: clauses }));
}
return field.split('.').length > minDepth;
});
return hasDeepSortClauses || hasDeepWhereClauses;
}; };
const normalizeClauses = (whereClauses, { model }) => { const normalizeClauses = (whereClauses, { model }) => {
@ -142,19 +147,28 @@ const normalizeClauses = (whereClauses, { model }) => {
* @param {Object} options.rest - In case the database layer requires any other params pass them * @param {Object} options.rest - In case the database layer requires any other params pass them
*/ */
const buildQuery = ({ model, filters = {}, ...rest }) => { const buildQuery = ({ model, filters = {}, ...rest }) => {
const { where, sort } = filters;
// Validate query clauses // Validate query clauses
if (filters.where && Array.isArray(filters.where)) { if ([where, sort].some(Array.isArray)) {
if (hasDeepFilters(filters.where, { minDepth: 2 })) { if (hasDeepFilters({ where, sort, minDepth: 2 })) {
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 if (sort) {
filters.where = normalizeClauses(filters.where, { model }); // Check that every field from the sort clauses is valid, throw with error otherwise
sort.forEach(({ field }) => getAssociationFromFieldKey({ model, field }));
}
if (where) {
// Cast where clauses to match the inner types
filters.where = normalizeClauses(where, { model });
}
} }
// call the orm's buildQuery implementation // call the ORM's buildQuery implementation
return strapi.db.connectors.get(model.orm).buildQuery({ model, filters, ...rest }); return strapi.db.connectors.get(model.orm).buildQuery({ model, filters, ...rest });
}; };