Clientes y Facturas de cliente
This commit is contained in:
parent
e5cb2b318d
commit
f2ef5d2267
@ -14,8 +14,15 @@ export class ListCustomerInvoicesController extends ExpressController {
|
|||||||
return this.criteria;
|
return this.criteria;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { filters, pageSize, pageNumber } = this.criteria.toPrimitives();
|
const { q: quicksearch, filters, pageSize, pageNumber } = this.criteria.toPrimitives();
|
||||||
return Criteria.fromPrimitives(filters, "invoice_date", "DESC", pageSize, pageNumber);
|
return Criteria.fromPrimitives(
|
||||||
|
filters,
|
||||||
|
"invoice_date",
|
||||||
|
"DESC",
|
||||||
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
quicksearch
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async executeImpl() {
|
protected async executeImpl() {
|
||||||
|
|||||||
@ -116,6 +116,7 @@ export class CustomerInvoiceListMapper
|
|||||||
operationDate: attributes.operationDate!,
|
operationDate: attributes.operationDate!,
|
||||||
|
|
||||||
description: attributes.description!,
|
description: attributes.description!,
|
||||||
|
reference: attributes.description!,
|
||||||
|
|
||||||
customerId: attributes.customerId!,
|
customerId: attributes.customerId!,
|
||||||
recipient: recipientResult.data,
|
recipient: recipientResult.data,
|
||||||
@ -172,6 +173,12 @@ export class CustomerInvoiceListMapper
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const reference = extractOrPushError(
|
||||||
|
maybeFromNullableVO(raw.reference, (value) => Result.ok(String(value))),
|
||||||
|
"description",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
const description = extractOrPushError(
|
const description = extractOrPushError(
|
||||||
maybeFromNullableVO(raw.description, (value) => Result.ok(String(value))),
|
maybeFromNullableVO(raw.description, (value) => Result.ok(String(value))),
|
||||||
"description",
|
"description",
|
||||||
@ -254,6 +261,7 @@ export class CustomerInvoiceListMapper
|
|||||||
invoiceNumber,
|
invoiceNumber,
|
||||||
invoiceDate,
|
invoiceDate,
|
||||||
operationDate,
|
operationDate,
|
||||||
|
reference,
|
||||||
description,
|
description,
|
||||||
languageCode,
|
languageCode,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
|
|||||||
@ -48,8 +48,8 @@ export class InvoiceRecipientListMapper
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const _name = isProforma ? raw.current_customer.name : raw.customer_name;
|
const _name = isProforma ? raw.current_customer.name! : raw.customer_name!;
|
||||||
const _tin = isProforma ? raw.current_customer.tin : raw.customer_tin;
|
const _tin = isProforma ? raw.current_customer.tin! : raw.customer_tin!;
|
||||||
const _street = isProforma ? raw.current_customer.street : raw.customer_street;
|
const _street = isProforma ? raw.current_customer.street : raw.customer_street;
|
||||||
const _street2 = isProforma ? raw.current_customer.street2 : raw.customer_street2;
|
const _street2 = isProforma ? raw.current_customer.street2 : raw.customer_street2;
|
||||||
const _city = isProforma ? raw.current_customer.city : raw.customer_city;
|
const _city = isProforma ? raw.current_customer.city : raw.customer_city;
|
||||||
|
|||||||
@ -235,11 +235,21 @@ export class CustomerInvoiceRepository
|
|||||||
});
|
});
|
||||||
|
|
||||||
const converter = new CriteriaToSequelizeConverter();
|
const converter = new CriteriaToSequelizeConverter();
|
||||||
const query = converter.convert(criteria);
|
const query = converter.convert(criteria, {
|
||||||
|
searchableFields: ["invoice_number", "reference", "description"],
|
||||||
|
mappings: {
|
||||||
|
reference: "CustomerInvoiceModel.reference",
|
||||||
|
},
|
||||||
|
allowedFields: ["invoice_date", "id", "created_at"],
|
||||||
|
enableFullText: true,
|
||||||
|
database: this._database,
|
||||||
|
strictMode: true, // fuerza error si ORDER BY no permitido
|
||||||
|
});
|
||||||
|
|
||||||
query.where = {
|
query.where = {
|
||||||
...query.where,
|
...query.where,
|
||||||
company_id: companyId.toString(),
|
company_id: companyId.toString(),
|
||||||
|
deleted_at: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
query.include = [
|
query.include = [
|
||||||
@ -247,19 +257,45 @@ export class CustomerInvoiceRepository
|
|||||||
model: CustomerModel,
|
model: CustomerModel,
|
||||||
as: "current_customer",
|
as: "current_customer",
|
||||||
required: false, // false => LEFT JOIN
|
required: false, // false => LEFT JOIN
|
||||||
|
attributes: [
|
||||||
|
"name",
|
||||||
|
"trade_name",
|
||||||
|
"tin",
|
||||||
|
"street",
|
||||||
|
"street2",
|
||||||
|
"city",
|
||||||
|
"postal_code",
|
||||||
|
"province",
|
||||||
|
"country",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
model: CustomerInvoiceTaxModel,
|
model: CustomerInvoiceTaxModel,
|
||||||
as: "taxes",
|
as: "taxes",
|
||||||
required: false,
|
required: false,
|
||||||
|
separate: true, // => query aparte, devuelve siempre array
|
||||||
|
attributes: ["tax_id", "tax_code"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const { rows, count } = await CustomerInvoiceModel.findAndCountAll({
|
// Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento)
|
||||||
|
/*const { rows, count } = await CustomerInvoiceModel.findAndCountAll({
|
||||||
...query,
|
...query,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});*/
|
||||||
|
|
||||||
|
const [rows, count] = await Promise.all([
|
||||||
|
CustomerInvoiceModel.findAll({
|
||||||
|
...query,
|
||||||
|
transaction,
|
||||||
|
}),
|
||||||
|
CustomerInvoiceModel.count({
|
||||||
|
where: query.where,
|
||||||
|
distinct: true, // evita duplicados por LEFT JOIN
|
||||||
|
transaction,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return mapper.mapToDTOCollection(rows, count);
|
return mapper.mapToDTOCollection(rows, count);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|||||||
@ -362,7 +362,20 @@ export default (database: Sequelize) => {
|
|||||||
updatedAt: "updated_at",
|
updatedAt: "updated_at",
|
||||||
deletedAt: "deleted_at",
|
deletedAt: "deleted_at",
|
||||||
|
|
||||||
indexes: [{ name: "company_idx", fields: ["company_id"], unique: false }],
|
indexes: [
|
||||||
|
{
|
||||||
|
name: "idx_invoices_company_date",
|
||||||
|
fields: ["company_id", "deleted_at", { name: "invoice_date", order: "DESC" }],
|
||||||
|
},
|
||||||
|
{ name: "idx_invoice_date", fields: ["invoice_date"] }, // <- para ordenación
|
||||||
|
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "ft_customer_invoice",
|
||||||
|
type: "FULLTEXT",
|
||||||
|
fields: ["invoice_number", "reference", "description"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { usePinnedPreviewSheet } from '../../hooks';
|
import { usePinnedPreviewSheet } from '../../hooks';
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { InvoiceSummaryFormData, InvoicesPageFormData } from '../../schemas';
|
import { InvoiceSummaryFormData, InvoicesPageFormData } from '../../schemas';
|
||||||
import { InvoicePreviewPanel } from './invoice-preview-panel';
|
|
||||||
import { useInvoicesListColumns } from './use-invoices-list-columns';
|
import { useInvoicesListColumns } from './use-invoices-list-columns';
|
||||||
|
|
||||||
export type InvoiceUpdateCompProps = {
|
export type InvoiceUpdateCompProps = {
|
||||||
@ -163,7 +162,7 @@ export const InvoicesListGrid = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<preview.Preview>
|
{/*<preview.Preview>
|
||||||
{({ item, isPinned, close, togglePin }) => (
|
{({ item, isPinned, close, togglePin }) => (
|
||||||
<InvoicePreviewPanel
|
<InvoicePreviewPanel
|
||||||
invoice={item}
|
invoice={item}
|
||||||
@ -172,7 +171,7 @@ export const InvoicesListGrid = ({
|
|||||||
onTogglePin={togglePin}
|
onTogglePin={togglePin}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</preview.Preview>
|
</preview.Preview>*/}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { PageHeader } from '@erp/core/components';
|
import { PageHeader } from '@erp/core/components';
|
||||||
import { ErrorAlert } from '@erp/customers/components';
|
import { ErrorAlert } from '@erp/customers/components';
|
||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, AppHeader, BackHistoryButton, useDebounce } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
@ -18,14 +18,15 @@ export const InvoiceListPage = () => {
|
|||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const debouncedQ = useDebounce(search, 300);
|
||||||
|
|
||||||
const criteria = useMemo(
|
const criteria = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
q: search || "",
|
q: debouncedQ || "",
|
||||||
pageSize,
|
pageSize,
|
||||||
pageNumber: pageIndex,
|
pageNumber: pageIndex,
|
||||||
}),
|
}),
|
||||||
[pageSize, pageIndex, search]
|
[pageSize, pageIndex, debouncedQ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -47,7 +48,6 @@ export const InvoiceListPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
const handlePageChange = (newPageIndex: number) => {
|
const handlePageChange = (newPageIndex: number) => {
|
||||||
// TanStack usa pageIndex 0-based → API usa 0-based también
|
|
||||||
setPageIndex(newPageIndex);
|
setPageIndex(newPageIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
} from "./controllers";
|
} from "./controllers";
|
||||||
|
|
||||||
export const customersRouter = (params: ModuleParams) => {
|
export const customersRouter = (params: ModuleParams) => {
|
||||||
const { app, database, baseRoutePath, logger } = params as {
|
const { app, baseRoutePath, logger } = params as {
|
||||||
app: Application;
|
app: Application;
|
||||||
database: Sequelize;
|
database: Sequelize;
|
||||||
baseRoutePath: string;
|
baseRoutePath: string;
|
||||||
|
|||||||
@ -231,8 +231,12 @@ export default (database: Sequelize) => {
|
|||||||
deletedAt: "deleted_at",
|
deletedAt: "deleted_at",
|
||||||
|
|
||||||
indexes: [
|
indexes: [
|
||||||
{ name: "company_idx", fields: ["company_id"], unique: false },
|
{
|
||||||
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true },
|
name: "idx_customers_company_name",
|
||||||
|
fields: ["company_id", "deleted_at", "name"],
|
||||||
|
},
|
||||||
|
{ name: "idx_name", fields: ["name"] }, // <- para ordenación
|
||||||
|
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||||
{
|
{
|
||||||
name: "ft_customer",
|
name: "ft_customer",
|
||||||
type: "FULLTEXT",
|
type: "FULLTEXT",
|
||||||
|
|||||||
@ -170,18 +170,42 @@ export class CustomerRepository
|
|||||||
"email_primary",
|
"email_primary",
|
||||||
"mobile_primary",
|
"mobile_primary",
|
||||||
],
|
],
|
||||||
|
allowedFields: [
|
||||||
|
"name",
|
||||||
|
"trade_name",
|
||||||
|
"reference",
|
||||||
|
"tin",
|
||||||
|
"email_primary",
|
||||||
|
"mobile_primary",
|
||||||
|
],
|
||||||
|
enableFullText: true,
|
||||||
database: this._database,
|
database: this._database,
|
||||||
|
strictMode: true, // fuerza error si ORDER BY no permitido
|
||||||
});
|
});
|
||||||
|
|
||||||
query.where = {
|
query.where = {
|
||||||
...query.where,
|
...query.where,
|
||||||
company_id: companyId.toString(),
|
company_id: companyId.toString(),
|
||||||
|
deleted_at: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { rows, count } = await CustomerModel.findAndCountAll({
|
// Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento)
|
||||||
|
/*const { rows, count } = await CustomerModel.findAndCountAll({
|
||||||
...query,
|
...query,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});*/
|
||||||
|
|
||||||
|
const [rows, count] = await Promise.all([
|
||||||
|
CustomerModel.findAll({
|
||||||
|
...CustomerModel,
|
||||||
|
transaction,
|
||||||
|
}),
|
||||||
|
CustomerModel.count({
|
||||||
|
where: query.where,
|
||||||
|
distinct: true, // evita duplicados por LEFT JOIN
|
||||||
|
transaction,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return mapper.mapToDTOCollection(rows, count);
|
return mapper.mapToDTOCollection(rows, count);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|||||||
@ -3,84 +3,73 @@ import { Criteria } from "./critera";
|
|||||||
import { type ConvertParams, type CriteriaMappings, ICriteriaToOrmConverter } from "./types";
|
import { type ConvertParams, type CriteriaMappings, ICriteriaToOrmConverter } from "./types";
|
||||||
import { appendOrder, prependOrder } from "./utils";
|
import { appendOrder, prependOrder } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conversor optimizado Criteria → Sequelize FindOptions
|
||||||
|
*/
|
||||||
export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
||||||
//convert(fieldsToSelect: string[], criteria: Criteria, mappings: Mappings = {}): FindOptions {
|
|
||||||
convert(criteria: Criteria, params: ConvertParams = {}): FindOptions {
|
convert(criteria: Criteria, params: ConvertParams = {}): FindOptions {
|
||||||
const options: FindOptions = {};
|
const options: FindOptions = params.baseOptions ? { ...params.baseOptions } : {};
|
||||||
const { mappings = {} } = params;
|
const { mappings = {} } = params;
|
||||||
|
|
||||||
// Selección de campos
|
|
||||||
/*if (fieldsToSelect.length > 0) {
|
|
||||||
options.attributes = fieldsToSelect;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
this.applyFilters(options, criteria, mappings);
|
this.applyFilters(options, criteria, mappings);
|
||||||
this.applyQuickSearch(options, criteria, params);
|
this.applyQuickSearch(options, criteria, params);
|
||||||
this.applyOrder(options, criteria, mappings);
|
this.applyOrder(options, criteria, mappings, params);
|
||||||
this.applyPagination(options, criteria);
|
this.applyPagination(options, criteria);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Filtros simples (sin anidaciones complejas) */
|
||||||
public applyFilters(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings) {
|
public applyFilters(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings) {
|
||||||
const filterConditions: WhereOptions = {};
|
if (!criteria.hasFilters()) return;
|
||||||
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]) {
|
const filters: WhereOptions[] = [];
|
||||||
filterConditions[field] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
filterConditions[field][operator] = this.transformValue(operator, value);
|
for (const filter of criteria.filters.value) {
|
||||||
});
|
const field = mappings[filter.field.value] || filter.field.value;
|
||||||
|
const operator = this.mapOperator(filter.operator.value);
|
||||||
if (options.where) {
|
const value = this.transformValue(operator, filter.value.value);
|
||||||
options.where = { [Op.and]: [options.where, { [Op.or]: filterConditions }] };
|
filters.push({ [field]: { [operator]: value } });
|
||||||
} else {
|
|
||||||
options.where = { [Op.or]: filterConditions };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.where = options.where
|
||||||
|
? { [Op.and]: [options.where, ...filters] }
|
||||||
|
: { [Op.and]: filters };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** QuickSearch seguro con validación FULLTEXT */
|
||||||
public applyQuickSearch(options: FindOptions, criteria: Criteria, params: ConvertParams): void {
|
public applyQuickSearch(options: FindOptions, criteria: Criteria, params: ConvertParams): void {
|
||||||
const {
|
const {
|
||||||
mappings = {},
|
mappings = {},
|
||||||
searchableFields = [],
|
searchableFields = [],
|
||||||
database,
|
database,
|
||||||
} = params as ConvertParams & {
|
enableFullText = false,
|
||||||
database: Sequelize;
|
} = params as ConvertParams & { database?: Sequelize };
|
||||||
};
|
|
||||||
|
|
||||||
const term = typeof criteria.quickSearch === "string" ? criteria.quickSearch.trim() : "";
|
const term = typeof criteria.quickSearch === "string" ? criteria.quickSearch.trim() : "";
|
||||||
|
if (!term || searchableFields.length === 0 || !enableFullText) return;
|
||||||
|
|
||||||
// Si no hay término de búsqueda o no hay campos configurados, no hacemos nada
|
// Validación defensiva
|
||||||
if (term === "" || searchableFields.length === 0) {
|
if (!database) {
|
||||||
|
const msg = `[CriteriaToSequelizeConverter] enableFullText=true pero falta 'database' en params.`;
|
||||||
|
if (params.strictMode) throw new Error(msg);
|
||||||
|
console.warn(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construimos query de boolean mode con prefijo
|
|
||||||
const booleanTerm = term
|
const booleanTerm = term
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.map((w) => `+${w}*`)
|
.map((w) => `+${w}*`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
// Campos reales (con mappings aplicados)
|
const mappedFields = searchableFields.map((f) => mappings[f] || f);
|
||||||
const mappedFields = searchableFields.map((field) => mappings[field] || field);
|
|
||||||
|
|
||||||
const matchExpr = `MATCH(${mappedFields.join(", ")}) AGAINST (${database.escape(
|
const matchExpr = `MATCH(${mappedFields.join(", ")}) AGAINST (${database.escape(
|
||||||
booleanTerm
|
booleanTerm
|
||||||
)} IN BOOLEAN MODE)`;
|
)} IN BOOLEAN MODE)`;
|
||||||
|
|
||||||
const matchLiteral = Sequelize.literal(matchExpr);
|
const matchLiteral = Sequelize.literal(matchExpr);
|
||||||
|
|
||||||
// Añadimos score a los attributes (sin machacar si ya existen)
|
// Añadir campo virtual "score"
|
||||||
if (!options.attributes) {
|
if (!options.attributes) options.attributes = { include: [] };
|
||||||
options.attributes = { include: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(options.attributes)) {
|
if (Array.isArray(options.attributes)) {
|
||||||
options.attributes.push([matchLiteral, "score"]);
|
options.attributes.push([matchLiteral, "score"]);
|
||||||
} else {
|
} else {
|
||||||
@ -90,35 +79,50 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
|
|
||||||
// WHERE score > 0
|
// WHERE score > 0
|
||||||
const scoreCondition = Sequelize.where(matchLiteral, { [Op.gt]: 0 });
|
const scoreCondition = Sequelize.where(matchLiteral, { [Op.gt]: 0 });
|
||||||
if (options.where) {
|
options.where = options.where
|
||||||
options.where = { [Op.and]: [options.where, { [Op.or]: scoreCondition }] };
|
? { [Op.and]: [options.where, scoreCondition] }
|
||||||
} else {
|
: { [Op.and]: [scoreCondition] };
|
||||||
options.where = { [Op.and]: scoreCondition };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordenar por relevancia (score)
|
// Ordenar por relevancia
|
||||||
prependOrder(options, [Sequelize.literal("score"), "DESC"]);
|
prependOrder(options, [Sequelize.literal("score"), "DESC"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyOrder(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings): void {
|
/** Ordenación validada y parametrizable */
|
||||||
if (criteria.hasOrder()) {
|
public applyOrder(
|
||||||
const field = mappings[criteria.order.orderBy.value] || criteria.order.orderBy.value;
|
options: FindOptions,
|
||||||
const direction = criteria.order.orderType.value.toUpperCase();
|
criteria: Criteria,
|
||||||
|
mappings: CriteriaMappings,
|
||||||
|
params: ConvertParams
|
||||||
|
): void {
|
||||||
|
if (!criteria.hasOrder()) return;
|
||||||
|
|
||||||
appendOrder(options, [[field, direction]] as OrderItem[]);
|
const field = mappings[criteria.order.orderBy.value] || criteria.order.orderBy.value;
|
||||||
|
const direction = criteria.order.orderType.value.toUpperCase();
|
||||||
|
|
||||||
|
const allowedFields = params.allowedFields ?? ["id", "created_at"];
|
||||||
|
const strict = params.strictMode ?? false;
|
||||||
|
|
||||||
|
if (!allowedFields.includes(field)) {
|
||||||
|
const msg = `[CriteriaToSequelizeConverter] Ignored ORDER BY '${field}' (not in allowedFields).`;
|
||||||
|
if (strict) throw new Error(msg);
|
||||||
|
console.warn(msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendOrder(options, [[field, direction]] as OrderItem[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Paginación estándar */
|
||||||
public applyPagination(options: FindOptions, criteria: Criteria): void {
|
public applyPagination(options: FindOptions, criteria: Criteria): void {
|
||||||
if (criteria.pageSize !== null) {
|
if (criteria.pageSize != null) {
|
||||||
options.limit = criteria.pageSize;
|
options.limit = criteria.pageSize;
|
||||||
}
|
if (criteria.pageNumber != null) {
|
||||||
|
options.offset = criteria.pageSize * criteria.pageNumber;
|
||||||
if (criteria.pageSize !== null && criteria.pageNumber !== null) {
|
}
|
||||||
options.offset = criteria.pageSize * criteria.pageNumber;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mapeo de operadores Criteria → Sequelize */
|
||||||
private mapOperator(operator: string): symbol {
|
private mapOperator(operator: string): symbol {
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case "CONTAINS":
|
case "CONTAINS":
|
||||||
@ -143,9 +147,7 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private transformValue(operator: symbol, value: any): any {
|
private transformValue(operator: symbol, value: any): any {
|
||||||
if (operator === Op.like || operator === Op.notLike) {
|
if (operator === Op.like || operator === Op.notLike) return `%${value}%`;
|
||||||
return `%${value}%`;
|
|
||||||
}
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
packages/rdx-criteria/src/types.d.ts
vendored
38
packages/rdx-criteria/src/types.d.ts
vendored
@ -1,16 +1,42 @@
|
|||||||
import { FindOptions } from "sequelize";
|
import { FindOptions } from "sequelize";
|
||||||
import { Criteria } from "./critera";
|
import { Criteria } from "./critera";
|
||||||
|
|
||||||
export type CriteriaMappings = { [key: string]: string };
|
/**
|
||||||
export type ConvertParams = { mappings?: CriteriaMappings; searchableFields?: string[] } & Record<
|
* Mapeo lógico→físico de campos.
|
||||||
string,
|
* - clave: nombre de campo en dominio (Criteria)
|
||||||
unknown
|
* - valor: columna real en BD (p.ej. 'invoice_date' o 'current_customer.name')
|
||||||
>;
|
*/
|
||||||
|
export type CriteriaMappings = Record<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parámetros de conversión → FindOptions (Sequelize).
|
||||||
|
* - allowedFields: lista blanca de campos ordenables (deben estar indexados).
|
||||||
|
* - searchableFields: columnas FULLTEXT (deben tener índice FT en BD).
|
||||||
|
* - enableFullText: activa MATCH ... AGAINST si true.
|
||||||
|
* - mappings: mapeo Criteria→SQL.
|
||||||
|
* - database: instancia Sequelize para escapar literales en FULLTEXT.
|
||||||
|
* - baseOptions: opciones iniciales (se fusionan con el resultado final).
|
||||||
|
* - strictMode?: true = lanza error si orden no permitido
|
||||||
|
*/
|
||||||
|
export interface ConvertParams {
|
||||||
|
allowedFields?: string[]; // p.ej. ['invoice_date','id','created_at']
|
||||||
|
searchableFields?: string[]; // p.ej. ['reference','description','notes']
|
||||||
|
enableFullText?: boolean; // default: false
|
||||||
|
mappings?: CriteriaMappings; // default: {}
|
||||||
|
database?: Sequelize; // requerido si enableFullText=true
|
||||||
|
baseOptions?: FindOptions; // default: {}
|
||||||
|
strictMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICriteriaToOrmConverter {
|
export interface ICriteriaToOrmConverter {
|
||||||
convert(criteria: Criteria, params: ConvertParams): FindOptions;
|
convert(criteria: Criteria, params: ConvertParams): FindOptions;
|
||||||
applyFilters(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings): void;
|
applyFilters(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings): void;
|
||||||
applyQuickSearch(options: FindOptions, criteria: Criteria, params: ConvertParams): void;
|
applyQuickSearch(options: FindOptions, criteria: Criteria, params: ConvertParams): void;
|
||||||
applyOrder(options: FindOptions, criteria: Criteria, mappings: CriteriaMappings): void;
|
applyOrder(
|
||||||
|
options: FindOptions,
|
||||||
|
criteria: Criteria,
|
||||||
|
mappings: CriteriaMappings,
|
||||||
|
params: ConvertParams
|
||||||
|
): void;
|
||||||
applyPagination(options: FindOptions, criteria: Criteria): void;
|
applyPagination(options: FindOptions, criteria: Criteria): void;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user