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; } }