@cargolift-cdi/business-rules-engine
v0.1.25
Published
Engine de Regras
Readme
Business Rules Engine (Node + TS)
Motor de regras de negócio simples, performático e sem dependências externas. Executa regras em JSON sobre um payload JSON e retorna o payload modificado com auditoria.
Recursos
- Ações: set, remove, rename, foreach
- Valores: fixed, path, expression, format, condition e ganchos para lookup/mapping via providers externos
- Condições: all/any com operadores (=, !=, >, >=, <, <=, in, not_in, contains, starts_with, ends_with, regex, exists, empty, ...)
- Auditoria: regras aplicadas, ignoradas e erros, com duração total
- Políticas de erro: default | fail | skip | warn
Uso básico
import { RuleEngine } from './dist/index.js';
import rules from './samples/rules.json' assert { type: 'json' };
import payload from './samples/payload.json' assert { type: 'json' };
const engine = new RuleEngine({ defaultOnError: 'warn' });
const result = await engine.run(rules, payload);
console.log(result.output); // payload transformado
console.log(result.audit); // auditoriaConsumo em outros projetos
- ESM (Node 18+ / "type": "module")
import { RuleEngine } from 'business-rules-engine';- CommonJS (Jest/NestJS por padrão)
const { RuleEngine } = require('business-rules-engine');Este pacote publica build dual (ESM + CJS) via "exports" do package.json, com tipos TypeScript incluídos.
Providers (Lookup/Mapping)
Implemente e injete os providers para habilitar lookup e mapping:
const engine = new RuleEngine({
providers: {
lookup: { async query({ provider, table, where, select }) { /* ... */ return [] } },
mapping: { async translate({ table, from, to, input, filters }) { /* ... */ return undefined } }
}
});Provider pronto: TypeORM (dynamic)
O pacote expõe um provider pronto com TypeORM, mas carregado dinamicamente para não forçar dependência no core.
Pré-requisito no projeto consumidor: typeorm instalado em runtime.
Configuração por ambiente (mesmo formato do ESB):
LOOKUP_DBS=[{"name":"ods","type":"postgres","host":"localhost","port":5432,"username":"user","password":"pass","database":"db","schema":"public","ssl":false}]Uso:
import { RuleEngine, TypeormLookupProvider } from '@cargolift-cdi/business-rules-engine';
const engine = new RuleEngine({
providers: {
lookup: TypeormLookupProvider.fromEnv(),
}
});Desenvolvimento
Testes
- Executar todos os testes:
npm test - Modo watch:
npm run test:watch
Os testes usam Vitest com TypeScript (ESM). Os arquivos ficam em src/__tests__ e cobrem ações, condições, expressões de data/número, foreach e políticas de erro. Para ver relatório de cobertura, abra a pasta coverage/ gerada após a execução.
Expressões (mode: "expression") — Helpers de Data
O avaliador de expressões suporta chamadas de função (sem eval) e inclui um conjunto de utilitários de data. Você pode usá-los nas regras com mode: "expression". Paths usam JSON Pointer, por exemplo /date_a.
Funções disponíveis:
now(): Date→ data/hora atualdate(value?): Date→ converte ISO string, timestamp (ms) ou Date; vazio = agoraaddDays(date, n): DateaddMonths(date, n): Date(ajusta fim de mês)addYears(date, n): DateaddHours(date, n): DateaddMinutes(date, n): DateaddSeconds(date, n): DateaddMs(date, n): DatetoMillis(date): number|fromMillis(ms): DatediffDays(a, b): number|diffHours(a, b): number|diffMillis(a, b): numberstartOfDay(date): Date|endOfDay(date): DateformatDate(date, mask?): string→maskpadrão:"iso"; tokens suportados:YYYY,MM,DD,HH,mm,ss,SSS
Operadores com datas:
Date + number→ Date (soma milissegundos)Date - number→ Date (subtrai milissegundos)Date - Date→ number (diferença em milissegundos)Date + Date→ não suportado (usetoMillis/fromMillisse realmente precisar combinar instantes)
Exemplos
{
"action": "set",
"target": "/ex/plus_5_days",
"value": { "mode": "expression", "type": "string", "value": "formatDate(addDays(date(/date_a), 5), \"YYYY-MM-DD\")" },
"flags": { "createIfMissing": true }
}{
"action": "set",
"target": "/ex/one_hour_later",
"value": { "mode": "expression", "type": "string", "value": "formatDate(addHours(date(/date_a), 1), \"iso\")" }
}{
"action": "set",
"target": "/ex/diff_days",
"value": { "mode": "expression", "type": "number", "value": "diffDays(date(/date_b), date(/date_a))" }
}{
"action": "set",
"target": "/ex/op_add_ms",
"value": { "mode": "expression", "type": "string", "value": "formatDate(date(/date_a) + 86400000, \"iso\")" } // +1 dia
}{
"action": "set",
"target": "/ex/average",
"value": { "mode": "expression", "type": "string", "value": "formatDate(fromMillis((toMillis(date(/date_a)) + toMillis(date(/date_b))) / 2), \"iso\")" }
}Notas:
- Prefira strings ISO (ex.:
2023-08-15T13:45:30Z) para parsing consistente. - Para produzir Date nativo no payload, use
type: "date"e não formate; para string, useformatDate(...)na expressão outype: "string".
Troubleshooting: Fuso horário (timezone) e DST
Datas em JavaScript/Node são sensíveis a fuso horário local e horário de verão (DST). Alguns pontos importantes e receitas práticas:
Parsing e formatação
- Strings ISO com sufixo
Zsão interpretadas como UTC. Ex.:"2023-08-15T13:45:30Z". formatDate(d, "iso")usatoISOString()(sempre UTC).- Sem
Z, o parser considera o horário local do servidor.
- Strings ISO com sufixo
startOfDay/endOfDay são LOCAIS
startOfDay(date)eendOfDay(date)usamsetHours(...)local. Em ambientes com fuso diferente do esperado, o “início do dia” pode não coincidir com UTC 00:00.- Se você precisa do limite do dia em UTC, monte a string ISO do dia e parseie:
{
"action": "set",
"target": "/ex/utc_start_of_day",
"value": {
"mode": "expression",
"type": "date",
// UTC 00:00:00 do mesmo dia de /date_a
"value": "date(formatDate(date(/date_a), \"YYYY-MM-DD\") + 'T00:00:00.000Z')"
}
}- Somar dias com DST
addDays(date, 1)usa regras de calendário local; atravessar uma mudança de DST pode resultar em +/- 23h ou 25h de diferença aparente.- Se você quer sempre somar blocos fixos de 24h (UTC), some milissegundos em UTC:
{
"action": "set",
"target": "/ex/add_1d_utc_exact",
"value": {
"mode": "expression",
"type": "date",
// 86_400_000 ms = 24h
"value": "fromMillis(toMillis(date(/date_a)) + 86400000)"
}
}- Comparações de data
- Para comparar apenas a data (dia) ignorando hora/fuso, normalize ambos os lados antes:
{
"action": "set",
"target": "/ex/is_same_utc_day",
"value": {
"mode": "expression",
"type": "boolean",
"value": "formatDate(date(/a), 'YYYY-MM-DD') == formatDate(date(/b), 'YYYY-MM-DD')"
}
}Recomendações gerais
- Padronize as datas do payload em ISO UTC (
...Z). - Se o domínio exige horário local específico, aplique as funções
startOfDay/endOfDayconscientemente (sabendo que são locais) ou normalize via strings ISO como mostrado. - Evite “Date + Date”; em vez disso, use
toMillis/fromMillise some durações explícitas.
Timezone: comportamento padrão
Para manter o uso simples e previsível:
- Todas as funções de data do avaliador (
addDays/Months/Years/Hours/...,startOfDay/endOfDay) usam horário LOCAL do ambiente (métodossetHours,setDate, etc.). formatDate(d, "iso")sempre usatoISOString()(UTC). Para strings no horário local, use uma máscara como"YYYY-MM-DD HH:mm:ss".- Se você precisa operar estritamente em UTC, utilize receitas do capítulo de Troubleshooting (por exemplo, construir strings ISO com sufixo
Ze parsear comdate(...), ou somar milissegundos comtoMillis/fromMillis).
- Padronize as datas do payload em ISO UTC (
