235 lines
6.8 KiB
JavaScript
Raw Normal View History

2019-03-13 19:27:18 +01:00
const _ = require('lodash');
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)) {
// 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
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,
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
}
buildWhereClause({
qb,
2019-03-13 19:27:18 +01:00
field: fieldKey,
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
*/
const buildWhereClause = ({ qb, field, operator, value }) => {
if (Array.isArray(value) && !['in', 'nin'].includes(operator)) {
return qb.where(subQb => {
for (let val of value) {
subQb.orWhere(q => buildWhereClause({ qb: q, field, operator, value: val }));
}
});
}
2019-03-13 19:27:18 +01:00
switch (operator) {
case 'eq':
return qb.where(field, value);
2019-03-13 19:27:18 +01:00
case 'ne':
return qb.where(field, '!=', value);
2019-03-13 19:27:18 +01:00
case 'lt':
return qb.where(field, '<', value);
2019-03-13 19:27:18 +01:00
case 'lte':
return qb.where(field, '<=', value);
2019-03-13 19:27:18 +01:00
case 'gt':
return qb.where(field, '>', value);
2019-03-13 19:27:18 +01:00
case 'gte':
return qb.where(field, '>=', value);
2019-03-13 19:27:18 +01:00
case 'in':
return qb.whereIn(field, Array.isArray(value) ? value : [value]);
2019-03-13 19:27:18 +01:00
case 'nin':
return qb.whereNotIn(field, Array.isArray(value) ? value : [value]);
2019-03-13 19:27:18 +01:00
case 'contains': {
return qb.whereRaw('LOWER(??) LIKE LOWER(?)', [field, `%${value}%`]);
2019-03-13 19:27:18 +01:00
}
case 'ncontains':
return qb.whereRaw('LOWER(??) NOT LIKE LOWER(?)', [field, `%${value}%`]);
2019-03-13 19:27:18 +01:00
case 'containss':
return qb.where(field, 'like', `%${value}%`);
2019-03-13 19:27:18 +01:00
case 'ncontainss':
return qb.whereNot(field, 'like', `%${value}%`);
2019-03-13 19:27:18 +01:00
default:
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)
*/
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;
association = tmpModel.associations.find(ast => ast.alias === key);
2019-03-13 19:27:18 +01:00
if (association) {
tmpModel = findModelByAssoc(association);
2019-03-13 19:27:18 +01:00
}
}
return {
association,
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
*/
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
return relationToPopulate.forEach(path => {
const parts = path.split('.');
2019-03-13 19:27:18 +01:00
let tmpModel = model;
for (let part of parts) {
const association = tmpModel.associations.find(assoc => assoc.alias === part);
if (association) {
const assocModel = findModelByAssoc(association);
buildSingleJoin(qb, tmpModel, assocModel, association);
tmpModel = assocModel;
}
2019-03-13 19:27:18 +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;
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}`
);
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-28 16:15:25 +01:00
`${relationTable}.${assocModel.primaryKey}`
);
} else {
const externalKey =
association.type === 'collection'
? `${relationTable}.${association.via}`
2019-03-28 16:15:25 +01:00
: `${relationTable}.${assocModel.primaryKey}`;
const internalKey =
association.type === 'collection'
2019-03-28 16:15:25 +01:00
? `${rootModel.collectionName}.${rootModel.primaryKey}`
: `${rootModel.collectionName}.${association.alias}`;
qb.innerJoin(relationTable, externalKey, internalKey);
}
2019-03-13 19:27:18 +01:00
};
module.exports = buildQuery;