_comuns/dist-back/filtroDelta.js

246 lines
8.2 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.iniciarFiltroDelta = exports.criarFiltroDelta = exports.zFiltroDelta = exports.zCondicaoFiltroDelta = exports.zColunaFiltroDelta = exports.operadorFiltroDeltaDetalhes = exports.zOperadorFiltroDelta = void 0;
const zod_1 = __importDefault(require("zod"));
/**
* =============================================================================
* FILTRO DELTA (zFiltroDelta)
* =============================================================================
*
* OBJETIVO
* -----------------------------------------------------------------------------
* Novo sistema de filtros projetado para maior flexibilidade e geração segura
* de instruções SQL dinâmicas (WHERE).
*
* REGRAS IMPORTANTES (PARA IA)
* -----------------------------------------------------------------------------
* - O filtro é composto de duas partes obrigatórias: `condicoes` e `formula`.
* - Apenas referências aos `id`s das `condicoes` podem ser usadas na `formula`.
* - Não use sintaxe baseada em objetos aninhados para agrupamento (ex: { E: [], OU: [] }). Use fórmulas em string.
*
* 1) CONDIÇÕES (`condicoes`):
* Array de objetos contendo as definições isoladas de cada comparação.
* Exemplo:
* [
* { id: "{1}", coluna: "idade", operador: ">=", valor: 18 },
* { id: "{2}", coluna: "nome", operador: "like", valor: "%pa%" }
* ]
* - `id`: Deve SEMPRE seguir o formato {identificador}. Ex: `{c1}`, `{idade_min}`.
* - `coluna`: O nome da coluna ou um path JSONB válido (ex: `veiculo.ano`).
* - `operador`: Um dos valores do tipo `tipoOperadorFiltroDelta` (veja a constante).
*
* 2) FÓRMULA (`formula`):
* String que define a expressão lógica relacionando as condições através de seus `id`s.
* Operadores Lógicos Suportados (EM MAIÚSCULO):
* - `E` (AND)
* - `OU` (OR)
* - `NAO` (NOT)
* Suporta o agrupamento lógico usando parênteses `()`
* Exemplo válido: "({1} E NAO({2})) OU {3}"
*
* =============================================================================
*/
/**
* Operadores permitidos para comparação.
*/
exports.zOperadorFiltroDelta = zod_1.default.enum([">", "<", ">=", "<=", "=", "!=", "in", "like", "∩"]);
exports.operadorFiltroDeltaDetalhes = {
">": {
rotulo: "Maior que",
descricao: "O valor de A deve ser maior que o valor de B",
},
"<": {
rotulo: "Menor que",
descricao: "O valor de A deve ser menor que o valor de B",
},
">=": {
rotulo: "Maior ou igual a",
descricao: "O valor de A deve ser maior ou igual ao valor de B",
},
"<=": {
rotulo: "Menor ou igual a",
descricao: "O valor de A deve ser menor ou igual ao valor de B",
},
"=": {
rotulo: "Igual a",
descricao: "O valor de A deve ser igual ao valor de B",
},
"!=": {
rotulo: "Diferente de",
descricao: "O valor de A deve ser diferente do valor de B",
},
in: {
rotulo: "Está em",
descricao: "O valor de A deve estar presente na coleção ou lista definida em B",
},
like: {
rotulo: "Contém",
descricao: "O valor de A deve conter o texto informado em B",
},
"∩": {
rotulo: "Interseção",
descricao: "A e B devem possuir pelo menos um elemento em comum",
},
};
/**
* Valida o identificador da condição.
*
* Formato esperado:
* {identificador} ou {identificador:sufixo}
*/
const regexIdCondicaoFiltroDelta = /^\{[\w:]+\}$/;
/**
* Valida nomes de colunas e caminhos jsonb.
*/
const regexColunaFiltroDelta = /^\w+(\.\w+)*$/;
/**
* Coluna simples ou caminho aninhado de jsonb.
*/
exports.zColunaFiltroDelta = zod_1.default
.string()
.regex(regexColunaFiltroDelta, "Coluna inválida")
.describe("nome da coluna ou caminho aninhado do jsonb");
/**
* Estrutura individual de uma condição do filtro.
*/
exports.zCondicaoFiltroDelta = zod_1.default.object({
/**
* Identificador único da condição.
*
* Exemplo:
* {1}
*/
id: zod_1.default
.string()
.describe("id da condição do filtro")
.regex(regexIdCondicaoFiltroDelta, "O id deve seguir o formato {identificador}"),
/**
* Nome da coluna ou caminho jsonb.
*/
coluna: exports.zColunaFiltroDelta,
/**
* Operador da comparação.
*/
operador: exports.zOperadorFiltroDelta,
/**
* Valor utilizado na comparação.
*/
valor: zod_1.default.any().describe("valor de comparação"),
});
/**
* Verifica se a fórmula possui uma estrutura lógica válida.
*/
const formulaFiltroDeltaValida = (formula) => {
/**
* Extrai apenas os tokens permitidos:
* - referências de condição: {id} ou {id:sufixo}
* - operadores lógicos: E, OU
* - negação: NAO(...)
* - parênteses
*/
const tokensPermitidos = formula.match(/\{[\w:]+\}|\bNAO\b|\bE\b|\bOU\b|\(|\)/g) || [];
/**
* Reconstrói a fórmula usando apenas os tokens válidos.
*/
const formulaReconstruida = tokensPermitidos.join("");
/**
* Remove espaços para comparar o texto original com o texto reconstruído.
*/
const formulaOriginalSemEspacos = formula.replace(/\s+/g, "");
const formulaReconstruidaSemEspacos = formulaReconstruida.replace(/\s+/g, "");
/**
* Se forem diferentes, há conteúdo não permitido na fórmula.
*/
if (formulaOriginalSemEspacos !== formulaReconstruidaSemEspacos) {
return false;
}
/**
* Converte a fórmula para uma expressão JavaScript equivalente.
*
* NAO(x) → !x
* E → &&
* OU → ||
*/
const expressaoLogica = formula
.replace(/\{[\w:]+\}/g, "true")
.replace(/\bNAO\b/g, "!")
.replace(/\bE\b/g, "&&")
.replace(/\bOU\b/g, "||");
try {
/**
* Valida a ordem dos operadores, parênteses e estrutura lógica geral.
*/
new Function(`return (${expressaoLogica})`)();
return true;
}
catch {
return false;
}
};
/**
* Estrutura principal do filtro delta.
*/
exports.zFiltroDelta = zod_1.default
.object({
/**
* Lista de condições disponíveis.
*/
condicoes: zod_1.default.array(exports.zCondicaoFiltroDelta),
/**
* Fórmula lógica que combina as condições.
*/
formula: zod_1.default.string().refine(formulaFiltroDeltaValida, {
message: "Fórmula inválida. Use apenas referências de condições {id} ou {id:sufixo}, operadores E/OU/NAO e parênteses.",
}),
})
.superRefine((value, ctx) => {
/**
* IDs das condições cadastradas.
*/
const idsCondicoes = new Set(value.condicoes.map((condicao) => condicao.id));
/**
* Referências usadas dentro da fórmula.
*/
const referenciasFormula = value.formula.match(/\{[\w:]+\}/g) || [];
/**
* Verifica se toda referência da fórmula existe em condicoes.
*/
for (const referencia of referenciasFormula) {
if (!idsCondicoes.has(referencia)) {
ctx.addIssue({
code: "custom",
path: ["formula"],
message: `A referência ${referencia} não existe em condicoes.`,
});
}
}
});
const criarFiltroDelta = (filtro) => exports.zFiltroDelta.parse(filtro);
exports.criarFiltroDelta = criarFiltroDelta;
/**
* Fábrica para criar filtros com colunas estritamente tipadas.
* Útil para autocompletar os nomes das colunas e garantir a estrutura no TypeScript.
*
* @example
* ```ts
* const { criarFiltroDelta } = iniciarFiltroDelta<"nome" | "idade" | "jsonb.propriedade">()
*
* const filtro = criarFiltroDelta({
* condicoes: [
* { id: "{c1}", coluna: "idade", operador: ">", valor: 18 },
* { id: "{c2}", coluna: "nome", operador: "=", valor: "luiz" },
* { id: "{c3}", coluna: "jsonb.propriedade", operador: "=", valor: "valor" },
* ],
* formula: "({c1} E {c2}) OU {c3}",
* })
* ```
*/
const iniciarFiltroDelta = () => {
return {
criarFiltroDelta: exports.criarFiltroDelta,
};
};
exports.iniciarFiltroDelta = iniciarFiltroDelta;