152 lines
4.8 KiB
TypeScript
152 lines
4.8 KiB
TypeScript
import { FindOptions, Op, OrderItem, Sequelize, WhereOptions } from "sequelize";
|
|
import { Criteria } from "./critera";
|
|
import { type ConvertParams, type CriteriaMappings, ICriteriaToOrmConverter } from "./types";
|
|
import { appendOrder, prependOrder } from "./utils";
|
|
|
|
export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|
//convert(fieldsToSelect: string[], criteria: Criteria, mappings: Mappings = {}): FindOptions {
|
|
convert(criteria: Criteria, params: ConvertParams = {}): FindOptions {
|
|
const options: FindOptions = {};
|
|
const { mappings = {} } = params;
|
|
|
|
// Selección de campos
|
|
/*if (fieldsToSelect.length > 0) {
|
|
options.attributes = fieldsToSelect;
|
|
}*/
|
|
|
|
this.applyFilters(options, criteria, mappings);
|
|
this.applyQuickSearch(options, criteria, params);
|
|
this.applyOrder(options, criteria, mappings);
|
|
this.applyPagination(options, criteria);
|
|
|
|
return options;
|
|
}
|
|
|
|
public applyFilters(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings) {
|
|
const filterConditions: WhereOptions = {};
|
|
if (criteria.hasFilters()) {
|
|
criteria.filters.value.forEach((filter) => {
|
|
const field = mappings[filter.field.value] || filter.field.value;
|
|
const operator = this.mapOperator(filter.operator.value);
|
|
const value = filter.value.value;
|
|
|
|
if (!filterConditions[field]) {
|
|
filterConditions[field] = {};
|
|
}
|
|
|
|
filterConditions[field][operator] = this.transformValue(operator, value);
|
|
});
|
|
|
|
if (options.where) {
|
|
options.where = { [Op.and]: [options.where, { [Op.or]: filterConditions }] };
|
|
} else {
|
|
options.where = { [Op.or]: filterConditions };
|
|
}
|
|
}
|
|
}
|
|
|
|
public applyQuickSearch(options: FindOptions, criteria: Criteria, params: ConvertParams): void {
|
|
const {
|
|
mappings = {},
|
|
searchableFields = [],
|
|
database,
|
|
} = params as ConvertParams & {
|
|
database: Sequelize;
|
|
};
|
|
|
|
const term = typeof criteria.quickSearch === "string" ? criteria.quickSearch.trim() : "";
|
|
|
|
// Si no hay término de búsqueda o no hay campos configurados, no hacemos nada
|
|
if (term === "" || searchableFields.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Construimos query de boolean mode con prefijo
|
|
const booleanTerm = term
|
|
.split(/\s+/)
|
|
.map((w) => `+${w}*`)
|
|
.join(" ");
|
|
|
|
// Campos reales (con mappings aplicados)
|
|
const mappedFields = searchableFields.map((field) => mappings[field] || field);
|
|
|
|
const matchExpr = `MATCH(${mappedFields.join(", ")}) AGAINST (${database.escape(
|
|
booleanTerm
|
|
)} IN BOOLEAN MODE)`;
|
|
|
|
const matchLiteral = Sequelize.literal(matchExpr);
|
|
|
|
// Añadimos score a los attributes (sin machacar si ya existen)
|
|
if (!options.attributes) {
|
|
options.attributes = { include: [] };
|
|
}
|
|
|
|
if (Array.isArray(options.attributes)) {
|
|
options.attributes.push([matchLiteral, "score"]);
|
|
} else {
|
|
options.attributes.include = options.attributes.include || [];
|
|
options.attributes.include.push([matchLiteral, "score"]);
|
|
}
|
|
|
|
// WHERE score > 0
|
|
const scoreCondition = Sequelize.where(matchLiteral, { [Op.gt]: 0 });
|
|
if (options.where) {
|
|
options.where = { [Op.and]: [options.where, { [Op.or]: scoreCondition }] };
|
|
} else {
|
|
options.where = { [Op.and]: scoreCondition };
|
|
}
|
|
|
|
// Ordenar por relevancia (score)
|
|
prependOrder(options, [Sequelize.literal("score"), "DESC"]);
|
|
}
|
|
|
|
public applyOrder(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings): void {
|
|
if (criteria.hasOrder()) {
|
|
const field = mappings[criteria.order.orderBy.value] || criteria.order.orderBy.value;
|
|
const direction = criteria.order.orderType.value.toUpperCase();
|
|
|
|
appendOrder(options, [[field, direction]] as OrderItem[]);
|
|
}
|
|
}
|
|
|
|
public applyPagination(options: FindOptions, criteria: Criteria): void {
|
|
if (criteria.pageSize !== null) {
|
|
options.limit = criteria.pageSize;
|
|
}
|
|
|
|
if (criteria.pageSize !== null && criteria.pageNumber !== null) {
|
|
options.offset = criteria.pageSize * criteria.pageNumber;
|
|
}
|
|
}
|
|
|
|
private mapOperator(operator: string): symbol {
|
|
switch (operator) {
|
|
case "CONTAINS":
|
|
return Op.like;
|
|
case "NOT_CONTAINS":
|
|
return Op.notLike;
|
|
case "NOT_EQUALS":
|
|
return Op.ne;
|
|
case "GREATER_THAN":
|
|
return Op.gt;
|
|
case "GREATER_THAN_OR_EQUAL":
|
|
return Op.gte;
|
|
case "LOWER_THAN":
|
|
return Op.lt;
|
|
case "LOWER_THAN_OR_EQUAL":
|
|
return Op.lte;
|
|
case "EQUALS":
|
|
return Op.eq;
|
|
default:
|
|
return Op.eq;
|
|
}
|
|
}
|
|
|
|
private transformValue(operator: symbol, value: any): any {
|
|
if (operator === Op.like || operator === Op.notLike) {
|
|
return `%${value}%`;
|
|
}
|
|
return value;
|
|
}
|
|
}
|