2019-03-13 19:27:18 +01:00
|
|
|
const _ = require('lodash');
|
2019-03-28 12:13:32 +01:00
|
|
|
const pluralize = require('pluralize');
|
2019-03-13 19:27:18 +01:00
|
|
|
|
2019-03-28 16:15:25 +01:00
|
|
|
/**
|
|
|
|
* Build filters on a bookshelf query
|
|
|
|
* @param {Object} options - Options
|
|
|
|
* @param {Object} options.model - Bookshelf model
|
|
|
|
* @param {Object} options.filters - Filters params (start, limit, sort, where)
|
|
|
|
*/
|
2019-03-13 19:27:18 +01:00
|
|
|
const buildQuery = ({ model, filters }) => qb => {
|
|
|
|
if (_.has(filters, 'where') && Array.isArray(filters.where)) {
|
2019-03-22 12:16:09 +01:00
|
|
|
// build path with aliases and return a mapping of the paths with there aliases;
|
|
|
|
|
2019-03-13 19:27:18 +01:00
|
|
|
// build joins
|
2019-03-28 16:15:25 +01:00
|
|
|
buildQueryJoins(qb, { model, whereClauses: filters.where });
|
2019-03-13 19:27:18 +01:00
|
|
|
|
|
|
|
// apply filters
|
2019-03-28 17:12:43 +01:00
|
|
|
filters.where.forEach(({ field, operator, value }) => {
|
2019-03-28 16:15:25 +01:00
|
|
|
const { association, model: associationModel, attributeKey } = getAssociationFromFieldKey(
|
2019-03-13 19:27:18 +01:00
|
|
|
model,
|
2019-03-28 17:12:43 +01:00
|
|
|
field
|
2019-03-13 19:27:18 +01:00
|
|
|
);
|
|
|
|
|
2019-03-28 16:15:25 +01:00
|
|
|
let fieldKey = `${associationModel.collectionName}.${attributeKey}`;
|
2019-03-13 19:27:18 +01:00
|
|
|
|
2019-03-28 19:30:21 +01:00
|
|
|
if (association && attributeKey === field) {
|
2019-03-28 16:15:25 +01:00
|
|
|
fieldKey = `${associationModel.collectionName}.${associationModel.primaryKey}`;
|
2019-03-13 19:27:18 +01:00
|
|
|
}
|
|
|
|
|
2019-03-22 12:16:09 +01:00
|
|
|
buildWhereClause({
|
|
|
|
qb,
|
2019-03-13 19:27:18 +01:00
|
|
|
field: fieldKey,
|
2019-03-28 17:12:43 +01:00
|
|
|
operator,
|
|
|
|
value,
|
2019-03-13 19:27:18 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_.has(filters, 'sort')) {
|
|
|
|
qb.orderBy(
|
|
|
|
filters.sort.map(({ field, order }) => ({
|
|
|
|
column: field,
|
|
|
|
order,
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_.has(filters, 'start')) {
|
|
|
|
qb.offset(filters.start);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_.has(filters, 'limit') && filters.limit >= 0) {
|
|
|
|
qb.limit(filters.limit);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-03-28 16:15:25 +01:00
|
|
|
/**
|
|
|
|
* Builds a sql where clause
|
|
|
|
* @param {Object} options - Options
|
|
|
|
* @param {Object} options.qb - Bookshelf (knex) query builder
|
|
|
|
* @param {Object} options.model - Bookshelf model
|
|
|
|
* @param {Object} options.field - Filtered field
|
|
|
|
* @param {Object} options.operator - Filter operator (=,in,not eq etc..)
|
|
|
|
* @param {Object} options.value - Filter value
|
|
|
|
*/
|
2019-03-28 17:12:43 +01:00
|
|
|
const buildWhereClause = ({ qb, field, operator, value }) => {
|
2019-03-22 12:16:09 +01:00
|
|
|
if (Array.isArray(value) && !['in', 'nin'].includes(operator)) {
|
|
|
|
return qb.where(subQb => {
|
|
|
|
for (let val of value) {
|
2019-03-28 17:12:43 +01:00
|
|
|
subQb.orWhere(q => buildWhereClause({ qb: q, field, operator, value: val }));
|
2019-03-22 12:16:09 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-03-13 19:27:18 +01:00
|
|
|
switch (operator) {
|
|
|
|
case 'eq':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.where(field, value);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'ne':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.where(field, '!=', value);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'lt':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.where(field, '<', value);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'lte':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.where(field, '<=', value);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'gt':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.where(field, '>', value);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'gte':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.where(field, '>=', value);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'in':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.whereIn(field, Array.isArray(value) ? value : [value]);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'nin':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.whereNotIn(field, Array.isArray(value) ? value : [value]);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'contains': {
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.whereRaw('LOWER(??) LIKE LOWER(?)', [field, `%${value}%`]);
|
2019-03-13 19:27:18 +01:00
|
|
|
}
|
|
|
|
case 'ncontains':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.whereRaw('LOWER(??) NOT LIKE LOWER(?)', [field, `%${value}%`]);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'containss':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.where(field, 'like', `%${value}%`);
|
2019-03-13 19:27:18 +01:00
|
|
|
case 'ncontainss':
|
2019-03-28 17:12:43 +01:00
|
|
|
return qb.whereNot(field, 'like', `%${value}%`);
|
2019-03-13 19:27:18 +01:00
|
|
|
|
|
|
|
default:
|
2019-03-28 17:12:43 +01:00
|
|
|
throw new Error(`Unhandled whereClause : ${field} ${operator} ${value}`);
|
2019-03-13 19:27:18 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-03-28 16:15:25 +01:00
|
|
|
/**
|
|
|
|
* Returns a list of model path to populate from a list of where clausers
|
|
|
|
* @param {Object} where - where clause
|
|
|
|
*/
|
2019-03-13 19:27:18 +01:00
|
|
|
const extractRelationsFromWhere = where => {
|
|
|
|
return where
|
|
|
|
.map(({ field }) => {
|
|
|
|
const parts = field.split('.');
|
|
|
|
return parts.length === 1 ? field : _.initial(parts).join('.');
|
|
|
|
})
|
|
|
|
.sort()
|
|
|
|
.reverse()
|
|
|
|
.reduce((acc, currentValue) => {
|
|
|
|
const alreadyPopulated = _.some(acc, item => _.startsWith(item, currentValue));
|
|
|
|
if (!alreadyPopulated) {
|
|
|
|
acc.push(currentValue);
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}, []);
|
|
|
|
};
|
|
|
|
|
2019-03-28 16:15:25 +01:00
|
|
|
/**
|
|
|
|
* Returns a model association and the model concerned based on a model and a field to reach
|
|
|
|
* @param {Object} model - Bookshelf model
|
|
|
|
* @param {*} fieldKey - a path to a model field (e.g author.group.title)
|
|
|
|
*/
|
2019-03-22 12:16:09 +01:00
|
|
|
const getAssociationFromFieldKey = (model, fieldKey) => {
|
|
|
|
let tmpModel = model;
|
2019-03-13 19:27:18 +01:00
|
|
|
let association;
|
|
|
|
let attributeKey;
|
|
|
|
|
|
|
|
const parts = fieldKey.split('.');
|
|
|
|
|
|
|
|
for (let key of parts) {
|
|
|
|
attributeKey = key;
|
2019-03-22 12:16:09 +01:00
|
|
|
association = tmpModel.associations.find(ast => ast.alias === key);
|
2019-03-13 19:27:18 +01:00
|
|
|
if (association) {
|
2019-03-22 12:16:09 +01:00
|
|
|
tmpModel = findModelByAssoc(association);
|
2019-03-13 19:27:18 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
association,
|
2019-03-22 12:16:09 +01:00
|
|
|
model: tmpModel,
|
2019-03-13 19:27:18 +01:00
|
|
|
attributeKey,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2019-03-28 16:15:25 +01:00
|
|
|
/**
|
|
|
|
* Returns a Bookshelf model based on a model association
|
|
|
|
* @param {Object} assoc - A strapi association
|
|
|
|
*/
|
2019-03-22 12:16:09 +01:00
|
|
|
const findModelByAssoc = assoc => {
|
|
|
|
const { models } = assoc.plugin ? strapi.plugins[assoc.plugin] : strapi;
|
|
|
|
return models[assoc.collection || assoc.model];
|
|
|
|
};
|
2019-03-13 19:27:18 +01:00
|
|
|
|
2019-03-28 16:15:25 +01:00
|
|
|
/**
|
|
|
|
* Builds database query joins based on a model and a where clause
|
|
|
|
* @param {Object} qb - Bookshelf (knex) query builder
|
|
|
|
* @param {Object} options - Options
|
|
|
|
* @param {Object} options.model - Bookshelf model
|
|
|
|
* @param {Array<Object>} options.whereClauses - a list of where clauses
|
|
|
|
*/
|
|
|
|
const buildQueryJoins = (qb, { model, whereClauses }) => {
|
|
|
|
const relationToPopulate = extractRelationsFromWhere(whereClauses);
|
2019-03-13 19:27:18 +01:00
|
|
|
|
2019-03-22 12:16:09 +01:00
|
|
|
return relationToPopulate.forEach(path => {
|
|
|
|
const parts = path.split('.');
|
2019-03-13 19:27:18 +01:00
|
|
|
|
2019-03-22 12:16:09 +01:00
|
|
|
let tmpModel = model;
|
|
|
|
for (let part of parts) {
|
|
|
|
const association = tmpModel.associations.find(assoc => assoc.alias === part);
|
|
|
|
|
|
|
|
if (association) {
|
2019-03-22 17:58:36 +01:00
|
|
|
const assocModel = findModelByAssoc(association);
|
2019-03-22 12:16:09 +01:00
|
|
|
buildSingleJoin(qb, tmpModel, assocModel, association);
|
|
|
|
tmpModel = assocModel;
|
|
|
|
}
|
2019-03-13 19:27:18 +01:00
|
|
|
}
|
2019-03-22 12:16:09 +01:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2019-03-28 16:15:25 +01:00
|
|
|
/**
|
|
|
|
* Builds an individual join
|
|
|
|
* @param {Object} qb - Bookshelf model
|
|
|
|
* @param {Object} rootModel - The bookshelf model on which we are joining
|
|
|
|
* @param {*} assocModel - The model we are joining to
|
|
|
|
* @param {*} association - The association upo,n which the join is built
|
|
|
|
*/
|
|
|
|
const buildSingleJoin = (qb, rootModel, assocModel, association) => {
|
|
|
|
const relationTable = assocModel.collectionName;
|
2019-03-22 12:16:09 +01:00
|
|
|
|
|
|
|
qb.distinct();
|
|
|
|
|
|
|
|
if (association.nature === 'manyToMany') {
|
|
|
|
// Join on both ends
|
|
|
|
qb.innerJoin(
|
|
|
|
association.tableCollectionName,
|
2019-03-28 16:15:25 +01:00
|
|
|
`${association.tableCollectionName}.${pluralize.singular(rootModel.collectionName)}_${
|
|
|
|
rootModel.primaryKey
|
|
|
|
}`,
|
|
|
|
`${rootModel.collectionName}.${rootModel.primaryKey}`
|
2019-03-22 12:16:09 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
qb.innerJoin(
|
|
|
|
relationTable,
|
2019-03-28 16:15:25 +01:00
|
|
|
`${association.tableCollectionName}.${rootModel.attributes[association.alias].attribute}_${
|
|
|
|
rootModel.attributes[association.alias].column
|
2019-03-22 12:16:09 +01:00
|
|
|
}`,
|
2019-03-28 16:15:25 +01:00
|
|
|
`${relationTable}.${assocModel.primaryKey}`
|
2019-03-22 12:16:09 +01:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
const externalKey =
|
|
|
|
association.type === 'collection'
|
|
|
|
? `${relationTable}.${association.via}`
|
2019-03-28 16:15:25 +01:00
|
|
|
: `${relationTable}.${assocModel.primaryKey}`;
|
2019-03-22 12:16:09 +01:00
|
|
|
|
|
|
|
const internalKey =
|
|
|
|
association.type === 'collection'
|
2019-03-28 16:15:25 +01:00
|
|
|
? `${rootModel.collectionName}.${rootModel.primaryKey}`
|
|
|
|
: `${rootModel.collectionName}.${association.alias}`;
|
2019-03-22 12:16:09 +01:00
|
|
|
|
|
|
|
qb.innerJoin(relationTable, externalKey, internalKey);
|
|
|
|
}
|
2019-03-13 19:27:18 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = buildQuery;
|