280 lines
8.1 KiB
TypeScript
280 lines
8.1 KiB
TypeScript
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<typeof zOperadorFiltroDelta>,
|
|
{ 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<typeof zOperadorFiltroDelta>
|
|
export type tipoColunaFiltroDelta = z.infer<typeof zColunaFiltroDelta>
|
|
export type tipoCondicaoFiltroDelta = z.infer<typeof zCondicaoFiltroDelta>
|
|
export type tipoFiltroDelta = z.infer<typeof zFiltroDelta>
|
|
|
|
export type tipoCondicaoFiltroDeltaTipado<T extends string> = tipoCondicaoFiltroDelta & {
|
|
coluna: T
|
|
}
|
|
|
|
export type tipoFiltroDeltaColunaTipada<T extends string> = {
|
|
condicoes: tipoCondicaoFiltroDeltaTipado<T>[]
|
|
formula: string
|
|
}
|
|
|
|
export const criarFiltroDelta = <T extends string>(filtro: tipoFiltroDeltaColunaTipada<T>) =>
|
|
zFiltroDelta.parse(filtro) as tipoFiltroDeltaColunaTipada<T>
|
|
|
|
/**
|
|
* 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 = <TColunas extends string>() => {
|
|
return {
|
|
criarFiltroDelta: criarFiltroDelta<TColunas>,
|
|
}
|
|
}
|