import z from "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. */ export const zOperadorFiltroDelta = z.enum([">", "<", ">=", "<=", "=", "!=", "in", "like", "∩"]) export const operadorFiltroDeltaDetalhes: Record< z.infer, { rotulo: string; descricao: string } > = { ">": { 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. * Permite letras, números, sublinhado (_), ponto (.) e hífen (-) em qualquer combinação. */ export const regexColunaFiltroDelta = /^[\w.-]+$/ /** * Coluna simples ou caminho aninhado de jsonb. */ export const zColunaFiltroDelta = z .string() .regex(regexColunaFiltroDelta, "Coluna inválida") .describe("nome da coluna ou caminho aninhado do jsonb") /** * Estrutura individual de uma condição do filtro. */ export const zCondicaoFiltroDelta = z.object({ /** * Identificador único da condição. * * Exemplo: * {1} */ id: z .string() .describe("id da condição do filtro") .regex(regexIdCondicaoFiltroDelta, "O id deve seguir o formato {identificador}"), /** * Nome da coluna ou caminho jsonb. */ coluna: zColunaFiltroDelta, /** * Operador da comparação. */ operador: zOperadorFiltroDelta, /** * Valor utilizado na comparação. */ valor: z.any().describe("valor de comparação"), }) /** * Verifica se a fórmula possui uma estrutura lógica válida. */ const formulaFiltroDeltaValida = (formula: string) => { /** * 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. */ export const zFiltroDelta = z .object({ /** * Lista de condições disponíveis. */ condicoes: z.array(zCondicaoFiltroDelta), /** * Fórmula lógica que combina as condições. */ formula: z.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.`, }) } } }) export type tipoOperadorFiltroDelta = z.infer export type tipoColunaFiltroDelta = z.infer export type tipoCondicaoFiltroDelta = z.infer export type tipoFiltroDelta = z.infer export type tipoCondicaoFiltroDeltaTipado = tipoCondicaoFiltroDelta & { coluna: T } export type tipoFiltroDeltaColunaTipada = { condicoes: tipoCondicaoFiltroDeltaTipado[] formula: string } export const criarFiltroDelta = (filtro: tipoFiltroDeltaColunaTipada) => zFiltroDelta.parse(filtro) as tipoFiltroDeltaColunaTipada /** * 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}", * }) * ``` */ export const iniciarFiltroDelta = () => { return { criarFiltroDelta: criarFiltroDelta, } }