Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Test Suite

on:
push:
branches: [main]
pull_request:

jobs:
unit:
name: ${{ matrix.test.name }} (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [18.x, 20.x]
test:
- name: "Parse CSV"
file: tests/parseCSV.test.js
- name: "Token Resolver"
file: tests/utils/tokenResolver.test.js
- name: "Condition Evaluation"
file: tests/utils/evaluateCondition.test.js
- name: "Validators"
file: tests/validators.test.js
- name: "Dataset Validation"
file: tests/validateDataset.test.js
- name: "Configuration Scenarios"
file: tests/configs.test.js
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Run targeted test file
run: node --test ${{ matrix.test.file }}
137 changes: 137 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# csv-rule-validator

Librería ligera para validar datasets en formato CSV o JSON mediante reglas declarativas. Permite describir validaciones campo a campo usando archivos JSON y enlazarse con lógica de negocio externa para escenarios avanzados.

## Instalación

```bash
npm install
```

## Uso básico

```js
import { readFileSync } from 'fs';
import { validateDataset } from 'csv-rule-validator';

const csv = readFileSync('./clientes.csv', 'utf8');
const definition = JSON.parse(readFileSync('./configs/basic-onboarding.json', 'utf8'));

const resultado = await validateDataset(csv, definition);

if (!resultado.valid) {
console.table(resultado.errors);
}
```

## Definiciones en JSON

Cada archivo de configuración describe reglas por columna y validaciones de fila opcionales:

```json
{
"columns": {
"dni": {
"rules": [
{ "type": "required" },
{ "type": "string", "length": 8 },
{ "type": "number", "integer": true }
]
},
"email": {
"rules": [
{ "type": "required" },
{
"type": "string",
"pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
"message": "El email no tiene un formato válido"
}
]
}
}
}
```

### Validaciones disponibles

- `required`: verifica que exista un valor.
- `string`: longitudes mínimas/máximas, coincidencia de patrón, mayúsculas/minúsculas.
- `number`: admite `min`, `max` e `integer`.
- `in`: asegura que el valor pertenezca a un listado.
- `compare`: compara el valor con otro campo o constante (`operator`: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `contains`, `startsWith`, `endsWith`).
- `requiredIf`: vuelve obligatorio un campo cuando se cumple una condición declarativa (`when`).
- `custom`: delega la validación a funciones externas o personalizadas.

Todas las reglas aceptan:

- `message`: texto a devolver en caso de error.
- `code`: identificador opcional de error.
- `severity`: etiqueta libre (`warning`, `error`, etc.).
- `onlyIf`: condición declarativa para ejecutar la regla sólo en determinados casos.

### Condiciones declarativas (`when`, `onlyIf`)

Las condiciones usan la misma sintaxis tanto en reglas como en `rowValidations`:

- `field`: nombre de la columna a evaluar (`$row.campo` implícito).
- `target`: token explícito (`$value`, `$row.otroCampo`, `$context.algo`).
- Operadores: `equals`, `notEquals`, `in`, `notIn`, `present`, `empty`, `minLength`, `maxLength`, `regex`, `gt`, `gte`, `lt`, `lte`, `truthy`, `falsy`.
- Combinadores lógicos: `all`, `any`, `not`.
- `external`: nombre de un resolver externo que devuelve `true`/`false`.

### Validaciones por fila

Además de las reglas por columna, podés describir `rowValidations` para ejecutar múltiples afirmaciones cuando la fila cumple cierta condición:

```json
{
"rowValidations": [
{
"when": { "field": "estado", "equals": "aprobada" },
"assertions": [
{ "column": "monto_desembolsado", "rule": { "type": "required" } },
{ "column": "fecha_aprobacion", "rule": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" } }
]
}
]
}
```

## Integración con lógica de negocio

La clave `custom` permite referenciar funciones declaradas en `context.resolvers`. Cada resolver recibe `(value, row, context, rule, args)` y puede devolver:

- `true` si la validación es exitosa.
- `false` para usar el mensaje configurado en la regla.
- Un `string` con el mensaje de error.
- Un objeto `{ valid: boolean, message?: string, meta?: any }` para enviar metadata adicional.

Ejemplo:

```js
const context = {
resolvers: {
async dniExisteEnCore(value) {
const existe = await clientesService.existe(value);
return existe || 'El DNI no figura en el core';
}
}
};
```

## Configuraciones de ejemplo

Se incluyen varios escenarios en la carpeta [`configs/`](./configs):

- [`basic-onboarding.json`](./configs/basic-onboarding.json): reglas esenciales para altas.
- [`dependent-benefits.json`](./configs/dependent-benefits.json): dependencias entre columnas y validaciones de fila.
- [`external-checks.json`](./configs/external-checks.json): integración con sistemas externos.
- [`advanced-risk.json`](./configs/advanced-risk.json): escenarios complejos con múltiples condiciones combinadas.

## Ejecutar el ejemplo

```bash
node example.js
```

El script carga la configuración `external-checks.json`, simula resolvers externos y muestra los errores detectados.
105 changes: 105 additions & 0 deletions configs/advanced-risk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{
"description": "Escenario complejo para scoring de riesgo y calidad de datos",
"columns": {
"operacion_id": {
"rules": [
{ "type": "required" },
{ "type": "string", "pattern": "^OP-\\d{6}$", "message": "El ID debe seguir el patrón OP-######" }
]
},
"estado": {
"rules": [
{ "type": "required" },
{ "type": "in", "values": ["aprobada", "rechazada", "pendiente"] }
]
},
"motivo_rechazo": {
"rules": [
{
"type": "requiredIf",
"when": {
"field": "estado",
"equals": "rechazada"
},
"message": "Los rechazos requieren motivo"
}
]
},
"score": {
"rules": [
{ "type": "number", "min": 0, "max": 100 },
{
"type": "compare",
"operator": "gte",
"left": "$value",
"right": "$row.score_minimo_perfil",
"onlyIf": { "field": "score_minimo_perfil", "present": true },
"message": "El score está por debajo del mínimo permitido para el perfil"
}
]
},
"score_minimo_perfil": {
"rules": [
{ "type": "number", "min": 0, "max": 100 }
]
},
"flag_sospechoso": {
"rules": [
{ "type": "string", "pattern": "^(SI|NO)$", "message": "Usa SI o NO" }
]
},
"comentarios": {
"rules": [
{
"type": "string",
"onlyIf": {
"any": [
{ "field": "flag_sospechoso", "equals": "SI" },
{ "field": "estado", "equals": "rechazada" }
]
},
"minLength": 10,
"message": "Incluí una explicación cuando la operación es riesgosa"
}
]
}
},
"rowValidations": [
{
"when": { "field": "estado", "equals": "aprobada" },
"assertions": [
{
"column": "monto_desembolsado",
"rule": { "type": "required", "message": "Las operaciones aprobadas deben tener desembolso" }
},
{
"column": "fecha_aprobacion",
"rule": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
"message": "El formato de fecha de aprobación es inválido"
}
}
]
},
{
"when": {
"all": [
{ "field": "estado", "equals": "aprobada" },
{ "field": "flag_sospechoso", "equals": "SI" }
]
},
"assertions": [
{
"column": "reporte_manual",
"rule": {
"type": "custom",
"resolver": "validarReporteManual",
"args": ["$row"],
"message": "Falta adjuntar el reporte manual para operaciones sospechosas aprobadas"
}
}
]
}
]
}
33 changes: 33 additions & 0 deletions configs/basic-onboarding.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"description": "Reglas básicas para validar un CSV de altas de clientes",
"columns": {
"dni": {
"rules": [
{ "type": "required", "message": "El DNI es obligatorio" },
{ "type": "string", "length": 8, "message": "El DNI debe tener 8 caracteres" },
{ "type": "number", "integer": true, "message": "El DNI debe ser numérico" }
]
},
"nombre": {
"rules": [
{ "type": "required" },
{ "type": "string", "minLength": 3, "maxLength": 120 }
]
},
"email": {
"rules": [
{ "type": "required" },
{
"type": "string",
"pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
"message": "El email no tiene un formato válido"
}
]
},
"telefono": {
"rules": [
{ "type": "string", "onlyIf": { "present": true, "field": "telefono" }, "minLength": 9, "maxLength": 12 }
]
}
}
}
53 changes: 53 additions & 0 deletions configs/dependent-benefits.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"description": "Valida dependencias entre columnas para beneficios corporativos",
"columns": {
"tipo_empleado": {
"rules": [
{ "type": "required" },
{ "type": "in", "values": ["planta", "contratista", "practicante"] }
]
},
"fecha_ingreso": {
"rules": [
{ "type": "requiredIf", "when": { "field": "tipo_empleado", "equals": "planta" } },
{ "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "message": "Usa formato YYYY-MM-DD" }
]
},
"salario": {
"rules": [
{ "type": "number", "min": 0 },
{
"type": "custom",
"resolver": "validateMinimumSalary",
"args": ["$value", "$row.tipo_empleado"],
"message": "El salario no cumple el mínimo requerido para el tipo de empleado"
}
]
},
"bono": {
"rules": [
{
"type": "compare",
"operator": "lte",
"left": "$value",
"right": "$row.salario",
"message": "El bono no puede ser mayor que el salario"
}
]
}
},
"rowValidations": [
{
"when": { "field": "tipo_empleado", "equals": "planta" },
"assertions": [
{
"column": "beneficios_medicos",
"rule": {
"type": "required",
"message": "Los empleados de planta requieren información de beneficios médicos"
}
}
]
}
]
}
Loading