npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@felipe-lib/schema-local

v2.3.3

Published

Biblioteca de validação simples, leve e agnóstica com NotificationPattern e ResultPattern para respostas padronizadas

Readme

schemaValidation v2.3.3

Biblioteca de validação simples, leve e agnóstica com NotificationPattern e ResultPattern para respostas padronizadas.


Sumário


Por que usar?

Validar dados é uma das tarefas mais comuns em qualquer aplicação. A maioria das bibliotecas de validação ou é acoplada a um framework, ou força um schema declarativo com DSL própria, ou é pesada em dependências.

O schemaValidation adota uma abordagem diferente:

| Característica | schemaValidation | Outras libs | | ---------------- | ------------------------------- | ------------------------------------------ | | Dependências | Zero | Zod (~10), Joi (~15), class-validator (~5) | | Estilo | Regras como funções puras | DSL declarativa / decorators | | Acoplamento | Agnóstico (qualquer objeto) | Muitas vezes acoplado a framework | | Padrões | Notification + Result + Command | Apenas validação | | Tamanho | ~174 linhas de código | Milhares de linhas |

Ideal para:

  • APIs REST que precisam de respostas estruturadas com lista de erros
  • Validação de formulários, payloads, DTOs
  • Projetos que valorizam zero dependências
  • Quem prefere código imperativo/explícito a DSLs mágicas

Talvez não seja ideal se:

  • Você precisa de type narrowing por schema (type inference da forma dos dados) — use Zod
  • Você precisa de parsing + validação em cadeia — use Zod

Modelo Mental

A biblioteca implementa três padrões de design que trabalham juntos:

┌─────────────────────────────────────────────────────────────┐
│                     SchemaValidator                         │
│                                                             │
│  execute(data, context, options?) ───────────────────────►  │
│                                                             │
│  Schema (Rule[])                                            │
│  ┌─────────┐   ┌─────────┐   ┌─────────┐                   │
│  │ Rule 1  │──►│ Rule 2  │──►│ Rule 3  │──► ...            │
│  │         │   │         │   │         │                    │
│  │ 1. transform (opcional)                                  │
│  │ 2. condition (opcional)                                  │
│  │ 3. runValidate                                           │
│  └────┬────┘   └────┬────┘   └────┬────┘                   │
│       │              │              │                        │
│       ▼              ▼              ▼                        │
│  ┌────────────────────────────────────┐                     │
│  │     NotificationPattern[]          │                     │
│  │  { key, error, description, ... }  │                     │
│  └───────────────┬────────────────────┘                     │
│                  ▼                                          │
│  ┌────────────────────────────────────┐                     │
│  │        ResultPattern<T>            │                     │
│  │  { success, notification[], data } │                     │
│  └────────────────────────────────────┘                     │
└─────────────────────────────────────────────────────────────┘

Os três padrões

Command PatternSchemaValidator implementa Command<T, R, C>. Você instancia com um schema e chama execute(data, context, options?). O terceiro parâmetro aceita { groups?, pick?, omit? } para filtrar quais regras serão executadas. O validator encapsula toda a lógica de execução.

Notification Pattern — Cada falha de validação gera uma Notification com key (campo), error (mensagem) e description (detalhe opcional). Todas as notificações são coletadas em um array.

Result Pattern — O resultado final é um objeto com success (booleano), notification (array de notificações) e data (dados originais/transformados).

Ordem de execução dentro de cada Rule

para cada rule:
  1. transform(data, context)      → modifica os dados (encadeado entre regras)
  2. condition(data, context)      → se false, pula a regra
  3. runValidate(data, context)    → executa a validação
  4. se inválido → notificationMappers → adiciona ao array
  ── se algum passo acima lançar erro ──
  5. retry (opcional)              → loop com backoff (ver seção "Retry")

Quick Start

Instalação

npm install @felipe-lib/schema-local

Sua primeira validação

import { SchemaValidator } from "@felipe-lib/schema-local";

interface User {
    name: string;
    email: string;
}

const validator = new SchemaValidator<User>({
    schema: [
        {
            key: "name",
            error: () => "Nome é obrigatório",
            runValidate: (data) => data.name.trim().length > 0,
        },
        {
            key: "email",
            error: () => "Email inválido",
            runValidate: (data) =>
                /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email),
        },
    ],
});

const result = await validator.execute({
    name: "João",
    email: "[email protected]",
});

console.log(result.success); // true
console.log(result.notification); // []
console.log(result.data); // { name: "João", email: "[email protected]" }

O que aconteceu aqui?

  1. Criamos uma interface User com os campos a validar.
  2. Instanciamos SchemaValidator<User> passando um schema — array de regras.
  3. Cada regra tem key (nome do campo), error (mensagem de erro) e runValidate (função que retorna true se válido).
  4. Chamamos execute(data) e recebemos um ResultPattern com success, notification e data.
  5. Como o nome e o email eram válidos, success é true e notification está vazio.

Nota: Os mappers são opcionais — a biblioteca já fornece defaults que geram o NotificationPattern e ResultPattern padrão. Você só precisa customizá-los se quiser formatos diferentes (ex: resposta de API HTTP).

Adicionando mais regras

interface User {
    name: string;
    email: string;
    age: number;
}

const validator = new SchemaValidator<User>({
    schema: [
        {
            key: "name",
            error: () => "Nome é obrigatório",
            runValidate: (data) => data.name.trim().length > 0,
        },
        {
            key: "email",
            error: () => "Email inválido",
            runValidate: (data) =>
                /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email),
        },
        {
            key: "age",
            error: () => "Idade deve ser maior que 0",
            description: () => "Verifica se a idade é positiva",
            runValidate: (data) => data.age > 0,
        },
    ],
});

Lendo erros

const result = await validator.execute({
    name: "",
    email: "invalido",
    age: -5,
});

if (!result.success) {
    for (const err of result.notification) {
        console.log(`${String(err.key)}: ${err.error}`);
    }
}
// name: Nome é obrigatório
// email: Email inválido
// age: Idade deve ser maior que 0

Por padrão, todas as regras são executadas e todos os erros são coletados — você recebe uma lista completa do que está errado, não apenas o primeiro erro.


Conceitos Fundamentais

Anatomia de uma Rule

interface Rule<T, C> {
    key: keyof T; // campo que está sendo validado
    error: (data: T, context?: C) => string; // mensagem de erro (pode ser dinâmica)
    groups?: string[]; // grupos para filtrar execução (ver seção "Filtrar regras")
    retry?: RetryType; // config de retry com backoff (ver seção "Retry")
    transform?: (data: T, context?: C) => T | Promise<T>; // transforma dados antes da validação
    condition?: (data: T, context?: C) => boolean | Promise<boolean>; // se false, regra é ignorada
    runValidate(data: T, context?: C): boolean | Promise<boolean>; // lógica da validação
    description?: (data: T, context?: C) => string; // descrição opcional (pode ser dinâmica)
}

Todos os callbacks (transform, condition, runValidate, error, description) suportam tanto execução síncrona quanto assíncrona (retornando Promise). Todos recebem (data, context) — permitindo passar dependências externas como conexão de banco, request object, etc.

RetryType

Configura o loop de retry com backoff quando transform, condition ou runValidate lançarem um erro:

interface RetryType {
    active: boolean;      // liga/desliga o retry
    maxAttemps: number;   // delay inicial em ms (ex: 2000)
    multiply?: number;    // multiplicador de backoff (ex: 2 → 2s, 4s, 8s)
    maxRetries: number;   // número máximo de tentativas
}

Comportamento: se active for true e qualquer etapa da pipeline lançar erro, o validador:

  1. Aguarda maxAttemps ms
  2. Restaura os dados ao estado original
  3. Reexecuta transformconditionrunValidate
  4. Se condition retornar false → break (regra ignorada com sucesso)
  5. Se runValidate retornar true → break (sucesso)
  6. Senão → multiplica o timeout (timeout *= multiply) e tenta novamente
  7. Após maxRetries tentativas sem sucesso, o loop termina e uma única notificação de erro é gerada

NotificationPattern

Estrutura padronizada de notificação de erro:

interface NotificationPattern {
    success: boolean; // sempre false para notificações de erro
    key: string | number | symbol; // campo que falhou
    error: string; // mensagem de erro
    description?: string; // detalhamento opcional
}

ResultPattern<T>

Estrutura padronizada do resultado:

interface ResultPattern<T> {
    success: boolean; // true se nenhum erro
    notification: NotificationPattern[]; // array de erros encontrados
    data: T; // dados originais ou transformados
}

Mappers Customizados

Os mappers traduzem a regra e os dados para os formatos de notificação e resultado. Use-os quando precisar de estruturas diferentes da padrão:

// Mapper de notificação — transforma Rule + data + context em NotificationPattern
notificationMappers: (rule, data, context?) => ({
    success: false,
    key: rule.key,
    error: rule.error(data, context),
    description: rule.description?.(data, context),
});

// Mapper de resultado — transforma data + notificações em ResultPattern
resultMappers: (data, notif) => ({
    data,
    notification: notif,
    success: notif.length === 0,
});

Se você não fornecer mappers, a biblioteca usa os defaults — que produzem exatamente as estruturas acima. Forneça mappers customizados apenas quando seu NotificationPattern ou ResultPattern tiverem campos extras.

Command<T, R, C>

Interface que SchemaValidator implementa:

interface Command<
    T extends object,
    R extends object,
    C extends object = {},
> {
    execute(data: T, context?: C, options?: OptionsCommand): Promise<R>;
}

Isso permite tratar qualquer validador como um comando executável, facilitando integração com padrões como Command Bus, Mediator ou Use Case.


Guias Práticos

Validar objetos aninhados

Componha validators: crie um validador para o objeto interno e chame-o dentro da regra do objeto externo.

interface Address {
    street: string;
    zipCode: string;
}

interface User {
    name: string;
    address: Address;
}

const addressValidator = new SchemaValidator<Address>({
    schema: [
        {
            key: "street",
            error: () => "Rua é obrigatória",
            runValidate: (data) => data.street.length > 0,
        },
        {
            key: "zipCode",
            error: () => "CEP inválido",
            runValidate: (data) => /^\d{5}-\d{3}$/.test(data.zipCode),
        },
    ],
});

const userValidator = new SchemaValidator<User>({
    schema: [
        {
            key: "name",
            error: () => "Nome é obrigatório",
            runValidate: (data) => data.name.trim().length > 0,
        },
        {
            key: "address",
            error: (data) => "Endereço inválido",
            runValidate: async (data) => {
                const result = await addressValidator.execute(data.address);
                return result.success;
            },
        },
    ],
});

Validar com chamadas assíncronas

runValidate aceita funções assíncronas — ideal para verificar existência no banco, chamadas de API, etc.

interface LoginData {
    email: string;
    password: string;
}

const loginValidator = new SchemaValidator<LoginData>({
    schema: [
        {
            key: "email",
            error: () => "Email é obrigatório",
            runValidate: (data) => !!data.email,
        },
        {
            key: "password",
            error: () => "Senha deve ter pelo menos 8 caracteres",
            runValidate: (data) => data.password.length >= 8,
        },
        {
            key: "email",
            error: () => "Usuário não encontrado",
            description: () => "Verifica se o email existe na base",
            runValidate: async (data) => {
                const response = await fetch("/api/verify-user", {
                    method: "POST",
                    body: JSON.stringify({ email: data.email }),
                });
                return response.ok;
            },
        },
    ],
});

Validar arrays

Use métodos como every(), some() ou filter() para validar cada item do array.

interface Product {
    name: string;
    price: number;
}

interface Order {
    items: Product[];
}

const orderValidator = new SchemaValidator<Order>({
    schema: [
        {
            key: "items",
            error: () => "Pedido vazio",
            runValidate: (data) => data.items.length > 0,
        },
        {
            key: "items",
            error: () => "Produto sem nome",
            runValidate: (data) =>
                data.items.every((item) => item.name.trim().length > 0),
        },
        {
            key: "items",
            error: () => "Preço inválido",
            runValidate: (data) => data.items.every((item) => item.price > 0),
        },
    ],
});

Criar regras reutilizáveis

Encapsule regras comuns em factory functions para evitar repetição.

interface User {
    username: string;
    email: string;
    password: string;
    confirmPassword: string;
}

const isRequired = (field: keyof User, error: string) => ({
    key: field,
    error: () => error,
    description: () => `Verifica se ${String(field)} foi fornecido`,
    runValidate: (data: User) => {
        const value = data[field];
        return typeof value === "string" ? value.trim().length > 0 : !!value;
    },
});

const isEmail = (error: string) => ({
    key: "email" as const,
    error: () => error,
    description: () => "Valida formato do email",
    runValidate: (data: User) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email),
});

const minLength = (field: keyof User, min: number, error: string) => ({
    key: field,
    error: () => error,
    description: () => `Mínimo de ${min} caracteres`,
    runValidate: (data: User) => {
        const value = data[field];
        return typeof value === "string" ? value.length >= min : false;
    },
});

const passwordsMatch = {
    key: "confirmPassword" as const,
    error: () => "Senhas não conferem",
    description: () => "Confirmação deve ser igual à senha",
    runValidate: (data: User) => data.password === data.confirmPassword,
};

const validator = new SchemaValidator<User>({
    schema: [
        isRequired("username", "Nome de usuário é obrigatório"),
        isEmail("Email inválido"),
        minLength("password", 8, "Senha deve ter pelo menos 8 caracteres"),
        passwordsMatch,
    ],
});

Transformar dados antes de validar

O campo transform modifica os dados antes da validação. O resultado é encadeado entre regras — cada regra recebe os dados já transformados pelas anteriores.

interface User {
    name: string;
    email: string;
}

const validator = new SchemaValidator<User>({
    schema: [
        {
            key: "name",
            error: () => "Nome é obrigatório",
            transform: (data) => ({ ...data, name: data.name.trim() }),
            runValidate: (data) => data.name.length > 0,
        },
        {
            key: "email",
            error: () => "Email inválido",
            transform: (data) => ({
                ...data,
                email: data.email.toLowerCase().trim(),
            }),
            runValidate: (data) =>
                /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email),
        },
    ],
});

const result = await validator.execute({
    name: "  João  ",
    email: "  [email protected]  ",
});

console.log(result.data.name); // "João"
console.log(result.data.email); // "[email protected]"

Transform assíncrono:

const validator = new SchemaValidator<User>({
    schema: [
        {
            key: "email",
            error: () => "Email já cadastrado",
            transform: async (data) => {
                const normalized = data.email.toLowerCase().trim();
                return { ...data, email: normalized };
            },
            runValidate: async (data) => {
                const response = await fetch(
                    `/api/check-email?email=${data.email}`,
                );
                const { exists } = await response.json();
                return !exists;
            },
        },
    ],
});

Validar condicionalmente

O campo condition determina se a regra deve ser executada. Se a condição retornar false, a regra é ignorada.

interface FormData {
    type: "individual" | "company";
    cpf: string;
    cnpj: string;
    companyName: string;
}

const formValidator = new SchemaValidator<FormData>({
    schema: [
        {
            key: "type",
            error: () => "Tipo inválido",
            runValidate: (data) =>
                ["individual", "company"].includes(data.type),
        },
        {
            key: "cpf",
            error: () => "CPF inválido",
            condition: (data) => data.type === "individual",
            runValidate: (data) => /^\d{11}$/.test(data.cpf),
        },
        {
            key: "cnpj",
            error: () => "CNPJ inválido",
            condition: (data) => data.type === "company",
            runValidate: (data) => /^\d{14}$/.test(data.cnpj),
        },
        {
            key: "companyName",
            error: () => "Razão social obrigatória",
            condition: (data) => data.type === "company",
            runValidate: (data) => data.companyName.length > 0,
        },
    ],
});

Condition assíncrona:

interface ProductForm {
    categoryId: string;
    discount: number;
}

const validator = new SchemaValidator<ProductForm>({
    schema: [
        {
            key: "discount",
            error: () => "Desconto inválido",
            condition: async (data) => {
                const response = await fetch(
                    `/api/categories/${data.categoryId}`,
                );
                const category = await response.json();
                return category.allowDiscount;
            },
            runValidate: (data) => data.discount >= 0 && data.discount <= 100,
        },
    ],
});

Parar no primeiro erro

Com abortEarly: true, a validação é interrompida assim que o primeiro erro é encontrado.

interface LoginData {
    email: string;
    password: string;
}

const loginValidator = new SchemaValidator<LoginData>({
    schema: [
        {
            key: "email",
            error: () => "Email é obrigatório",
            runValidate: (data) => data.email.trim().length > 0,
        },
        {
            key: "email",
            error: () => "Email inválido",
            runValidate: (data) =>
                /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email),
        },
        {
            key: "password",
            error: () => "Senha é obrigatória",
            runValidate: (data) => data.password.length > 0,
        },
        {
            key: "password",
            error: () => "Mínimo de 8 caracteres",
            runValidate: (data) => data.password.length >= 8,
        },
    ],
    abortEarly: true,
});

const result = await loginValidator.execute({
    email: "",
    password: "123",
});

console.log(result.notification.length); // 1 — apenas o primeiro erro

Combinar transform + condition + abortEarly

As funcionalidades podem ser combinadas livremente. A ordem de execução dentro de cada regra segue o pipeline: transform → condition → runValidate.

interface OrderData {
    total: number;
    couponCode: string;
    finalTotal: number;
}

const orderValidator = new SchemaValidator<OrderData>({
    schema: [
        {
            key: "total",
            error: () => "Total deve ser positivo",
            runValidate: (data) => data.total > 0,
        },
        {
            key: "couponCode",
            error: () => "Cupom inválido",
            condition: (data) => data.couponCode.length > 0,
            transform: async (data) => {
                const response = await fetch(`/api/coupons/${data.couponCode}`);
                const coupon = await response.json();
                return {
                    ...data,
                    finalTotal: data.total * (1 - coupon.discount / 100),
                };
            },
            runValidate: async (data) => {
                const response = await fetch(`/api/coupons/${data.couponCode}`);
                return response.ok;
            },
        },
    ],
    abortEarly: true,
});

const result = await orderValidator.execute({
    total: 200,
    couponCode: "BLACK20",
    finalTotal: 0,
});

console.log(result.data.finalTotal); // 160 (200 - 20%)

Retry com backoff em validações

Se transform, condition ou runValidate lançarem um erro (ex: falha de rede, timeout), você pode configurar um loop de retry com backoff exponencial usando o campo retry.

interface PaymentData {
    transactionId: string;
    amount: number;
}

const validator = new SchemaValidator<PaymentData>({
    schema: [
        {
            key: "transactionId",
            error: () => "Falha na confirmação do pagamento",
            runValidate: async (data) => {
                const response = await fetch(
                    `/api/transactions/${data.transactionId}`,
                );
                return response.ok;
            },
            retry: {
                active: true,
                maxRetries: 3,
                maxAttemps: 1000,
                multiply: 2,
            },
        },
    ],
});

Como funciona:

  1. Se transform, condition ou runValidate lançarem erro, o validador entra no loop de retry
  2. Cada tentativa espera maxAttemps ms (com backoff multiplicativo se multiply estiver definido)
  3. A cada retry os dados são restaurados ao estado original e toda pipeline é reexecutada
  4. Se condition retornar false → a regra é ignorada (break silencioso)
  5. Se runValidate retornar true → sucesso, sai do loop
  6. Se todas as tentativas falharem → uma única notificação de erro é gerada

Exemplo com backoff: maxAttemps: 1000, multiply: 2, maxRetries: 3

1ª tentativa: falhou → espera 1s
2ª tentativa: falhou → espera 2s
3ª tentativa: falhou → espera 4s → notificação final

Criar padrões customizados

Quando os mappers padrão não bastam, você pode estender NotificationPattern e ResultPattern com campos específicos do seu domínio.

Notification customizado com tipo de erro:

interface CustomNotification {
    success: boolean;
    key: string | number | symbol;
    error: string;
    field: string;
    message: string;
    type: "error" | "warning";
}

interface CustomResult<T> {
    success: boolean;
    notification: CustomNotification[];
    isValid: boolean;
    errors: CustomNotification[];
    data: T;
}

interface User {
    name: string;
    email: string;
}

const validator = new SchemaValidator<
    User,
    CustomNotification,
    CustomResult<User>
>({
    schema: [
        {
            key: "email",
            error: () => "Email inválido",
            runValidate: (data) =>
                /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email),
        },
    ],
    notificationMappers: (rule, data) => ({
        success: false,
        key: rule.key,
        error: rule.error(data),
        field: String(rule.key),
        message: rule.error(data),
        type: "error" as const,
    }),
    resultMappers: (data, notif) => ({
        success: notif.length === 0,
        notification: notif,
        isValid: notif.length === 0,
        errors: notif,
        data,
    }),
});

const result = await validator.execute({
    name: "João",
    email: "invalido",
});

console.log(result.isValid); // false
console.log(result.errors[0].field); // "email"
console.log(result.errors[0].type); // "error"

Integrar com APIs REST

Use mappers customizados para gerar respostas no formato que sua API espera.

interface ApiResponse<T> {
    status: number;
    message: string;
    payload?: T;
    errors: { field: string; reason: string }[];
}

interface ApiNotification {
    success: boolean;
    key: string | number | symbol;
    error: string;
    field: string;
    message: string;
}

interface User {
    name: string;
    email: string;
}

const apiValidator = new SchemaValidator<
    User,
    ApiNotification,
    ApiResponse<User>
>({
    schema: [
        {
            key: "name",
            error: () => "Nome é obrigatório",
            runValidate: (data) => data.name.trim().length > 0,
        },
        {
            key: "email",
            error: () => "Email inválido",
            runValidate: (data) =>
                /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email),
        },
    ],
    notificationMappers: (rule, data) => ({
        success: false,
        key: rule.key,
        error: rule.error(data),
        field: String(rule.key),
        message: rule.error(data),
    }),
    resultMappers: (data, notif) => ({
        success: notif.length === 0,
        notification: notif as NotificationPattern[],
        data,
        status: notif.length === 0 ? 200 : 400,
        message: notif.length === 0 ? "OK" : "Erro de validação",
        payload: notif.length === 0 ? data : undefined,
        errors: notif.map((n) => ({
            field: n.field,
            reason: n.message,
        })),
    }),
});

const result = await apiValidator.execute({
    name: "João",
    email: "[email protected]",
});

// result.status === 200
// result.errors === []
// Uso em um controller Express/Fastify:
// res.status(result.status).json(result);

Passar contexto para as regras

O parâmetro genérico C permite passar um objeto de contexto (context) que fica disponível em todos os callbacks das regras. Isso é útil para injetar dependências como conexão de banco, objetos de request, ou qualquer dado externo.

interface DatabaseCtx {
    db: DatabaseConnection;
    userId: string;
}

interface User {
    email: string;
}

const validator = new SchemaValidator<
    User,
    NotificationPattern,
    ResultPattern<User>,
    DatabaseCtx
>({
    schema: [
        {
            key: "email",
            error: (data, ctx) =>
                `Email já cadastrado para o usuário ${ctx.userId}`,
            description: (data, ctx) =>
                `Verifica unicidade do email no tenant ${ctx.userId}`,
            runValidate: async (data, ctx) => {
                const exists = await ctx.db.findByEmail(data.email);
                return !exists;
            },
        },
    ],
});

const db = await connectToDatabase();
const result = await validator.execute(
    { email: "[email protected]" },
    { db, userId: "usr_123" },
);

Por padrão, C = {} — se você não passa contexto, o TypeScript não exige o segundo argumento no execute() e os callbacks recebem {} como context.


Filtrar regras por groups, pick e omit

O terceiro parâmetro do execute permite filtrar quais regras serão executadas sem modificar o schema original.

groups: execute apenas regras que pertençam a determinados grupos.

interface User {
    name: string;
    email: string;
    password: string;
}

const validator = new SchemaValidator<User>({
    schema: [
        {
            key: "name",
            error: () => "Nome é obrigatório",
            groups: ["create", "update"],
            runValidate: (data) => data.name.trim().length > 0,
        },
        {
            key: "email",
            error: () => "Email inválido",
            groups: ["create", "update"],
            runValidate: (data) =>
                /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email),
        },
        {
            key: "password",
            error: () => "Mínimo de 8 caracteres",
            groups: ["create"],
            runValidate: (data) => data.password.length >= 8,
        },
    ],
});

// Apenas regras do grupo "create"
const result = await validator.execute(data, {}, { groups: ["create"] });

pick: execute apenas regras cujo key ou groups corresponda à lista.

// Por campo
const result = await validator.execute(data, {}, { pick: ["name", "email"] });

// Por grupo
const result = await validator.execute(data, {}, { pick: ["admin", "audit"] });

omit: execute todas as regras exceto as cujo key ou groups corresponda à lista.

// Por campo
const result = await validator.execute(data, {}, { omit: ["password"] });

// Por grupo
const result = await validator.execute(data, {}, { omit: ["admin"] });

Regras que não possuem groups são executadas independentemente do filtro de grupos. Os filtros podem ser combinados: { groups: ["create"], pick: ["name", "email"] }. Tanto pick quanto omit verificam simultaneamente rule.key e rule.groups — se qualquer um dos dois corresponder, a regra é incluída (pick) ou excluída (omit).


API Reference

SchemaValidator<T, N, R, C>

Classe principal. Implementa Command<T, R, C>.

| Parâmetro genérico | Default | Descrição | | ------------------------------- | --------------------- | --------------------------------------------------- | | T extends object | — | Tipo do objeto a ser validado | | N extends NotificationPattern | NotificationPattern | Tipo da notificação de erro | | R extends ResultPattern<T> | ResultPattern<T> | Tipo do resultado | | C extends object | {} | Contexto passado para todos os callbacks das regras |

Construtor:

new SchemaValidator<T, N, R, C>({
  schema: Rule<T, C>[];
  notificationMappers?: (rule: Rule<T, C>, data: T, context?: C) => N;
  resultMappers?: (data: T, notif: N[]) => R;
  abortEarly?: boolean;
})

| Opção | Obrigatório | Padrão | Descrição | | --------------------- | ----------- | ------------- | --------------------------------------- | | schema | Sim | — | Array de regras de validação | | notificationMappers | Não | mapper padrão | Customiza como notificações são geradas | | resultMappers | Não | mapper padrão | Customiza como o resultado é montado | | abortEarly | Não | false | Se true, para no primeiro erro |

Métodos:

| Método | Retorno | Descrição | | ----------------------------------------------------------- | ------------ | ------------------------------------------------------------- | | execute(data: T, context?: C, options?: OptionsCommand) | Promise<R> | Executa as regras (com filtro opcional) e retorna o resultado | | validation(data: T, context?: C, options?: OptionsCommand) | Promise<R> | Alias interno — mesmo comportamento de execute |

Tipos exportados

import type {
    NotificationPattern,
    ResultPattern,
    Rule,
    Command,
    OptionsCommand,
    RetryType,
} from "@felipe-lib/schema-local/types";

Rule<T, C>

interface Rule<T, C> {
    key: keyof T;
    error: (data: T, context?: C) => string;
    groups?: string[];
    retry?: RetryType;
    transform?: (data: T, context?: C) => T | Promise<T>;
    condition?: (data: T, context?: C) => boolean | Promise<boolean>;
    runValidate(data: T, context?: C): boolean | Promise<boolean>;
    description?: (data: T, context?: C) => string;
}

RetryType

interface RetryType {
    active: boolean;
    maxAttemps: number;
    multiply?: number;
    maxRetries: number;
}

NotificationPattern

interface NotificationPattern {
    success: boolean;
    key: string | number | symbol;
    error: string;
    description?: string;
}

ResultPattern<T>

interface ResultPattern<T> {
    success: boolean;
    notification: NotificationPattern[];
    data: T;
}

Command<T, R, C>

interface Command<
    T extends object,
    R extends object,
    C extends object = {},
> {
    execute(data: T, context?: C, options?: OptionsCommand): Promise<R>;
}

interface OptionsCommand {
    groups?: string[];
    pick?: string[];
    omit?: string[];
}

Funcionalidades

  • Zero dependências — não requer bibliotecas externas
  • NotificationPattern — notificações de erro padronizadas
  • ResultPattern — resultado padronizado com success, notification[] e data
  • Mappers customizáveis — personalize os formatos de notificação e resultado
  • Suporte sync/async — todos os callbacks aceitam funções síncronas e assíncronas
  • transform — modifique os dados antes da validação (encadeado entre regras)
  • condition — execute regras apenas quando uma condição for atendida
  • abortEarly — interrompa no primeiro erro
  • retry — backoff automático com repetição em caso de falha
  • Validação contínua — execute todas as regras e colete todos os erros (padrão)
  • Totalmente tipado — TypeScript nativo com genéricos
  • Command Pattern — interface Command<T, R> para integração com Command Bus / Mediator
  • Filtro por groups, pick e omit — execute apenas regras específicas sem modificar o schema
  • Agnóstico — funciona com qualquer formato de objeto

Licença

MIT © Felca