- 55 arquivos .md renomeados para sem extensão - Convenção: artefatos de conhecimento sem extensão, scripts (.sh) e dados (.json) mantêm extensão - Scripts atualizados: gerar-manifesto.sh, atualizar-agente.sh, atualizar-agente.global.sh, gerar-agent-json.sh, verificar-linhas.sh - Documentação reescrita: AGENTS, README, EXTEND para refletir uso como submodule .agent direto - manifesto.json regenerado v3.0.0 - Todas as referências internas entre artefatos atualizadas BREAKING CHANGE: arquivos não possuem mais extensão .md
315 lines
8.9 KiB
Text
315 lines
8.9 KiB
Text
---
|
|
type: rule
|
|
name: zod-validacao-padrao
|
|
description: Estabelece Zod como biblioteca obrigatória de validação em todo o ecossistema Azteca
|
|
version: "1.0"
|
|
scope: global
|
|
priority: alta
|
|
trigger: always_on
|
|
globs:
|
|
- "**/*.ts"
|
|
- "**/*.tsx"
|
|
- "**/*.vue"
|
|
alwaysApply: true
|
|
updatedAt: "2026-06-01"
|
|
references:
|
|
- global/rules/typescript-sem-any
|
|
- global/skills/criar-schema-zod/SKILL
|
|
---
|
|
|
|
# Regra: Zod como Validador Padrão
|
|
|
|
## Contexto
|
|
|
|
Todo projeto do ecossistema Azteca que receba dados externos — API, formulário, webhook, importação, fila, variáveis de ambiente — **deve** validar esses dados com [Zod](https://zod.dev).
|
|
|
|
Zod é a única biblioteca de validação autorizada porque:
|
|
|
|
- Inferência nativa de tipos TypeScript (`z.infer<>`) — elimina duplicação entre schema e tipo
|
|
- Bundle pequeno (~13KB gzipped) — viável no frontend
|
|
- Compartilhável entre back e front via pacotes `*-comuns`
|
|
- Ecossistema maduro: `zod-to-json-schema`, `@anatine/zod-nestjs`, integração com `tRPC`
|
|
|
|
---
|
|
|
|
## Regra
|
|
|
|
### 1. Toda entrada externa deve ser validada com Zod
|
|
|
|
Entradas externas incluem:
|
|
|
|
- **Corpo de requisição** (API REST, webhook, GraphQL)
|
|
- **Query params e path params**
|
|
- **Formulários de frontend**
|
|
- **Dados de fila** (BullMQ, Redis, etc.)
|
|
- **Arquivos importados** (CSV, JSON, Excel)
|
|
- **Variáveis de ambiente**
|
|
- **Respostas de APIs externas** (quando a resposta não é confiável)
|
|
|
|
### 2. Schemas devem residir no pacote `*-comuns`
|
|
|
|
Para que front e back compartilhem os mesmos schemas, eles devem residir no pacote `<modulo>-comuns`:
|
|
|
|
```
|
|
<modulo>-comuns/src/
|
|
├── tabelas/
|
|
│ ├── usuarios/
|
|
│ │ ├── zUsuarioCriar.ts
|
|
│ │ ├── zUsuarioEditar.ts
|
|
│ │ └── index.ts
|
|
│ ├── empresas/
|
|
│ │ ├── zEmpresaCriar.ts
|
|
│ │ └── index.ts
|
|
│ └── index.ts
|
|
```
|
|
|
|
### 3. Tipos devem ser derivados dos schemas (nunca o inverso)
|
|
|
|
```typescript
|
|
// ✅ CORRETO — tipo derivado do schema
|
|
export const zUsuarioCriar = z.object({
|
|
nome: z.string().min(1),
|
|
email: z.string().email(),
|
|
ativo: z.boolean().default(true),
|
|
})
|
|
export type tipoUsuarioCriar = z.infer<typeof zUsuarioCriar>
|
|
|
|
// ❌ ERRADO — tipo manual separado do schema
|
|
interface UsuarioCriar {
|
|
nome: string
|
|
email: string
|
|
ativo?: boolean
|
|
}
|
|
const zUsuarioCriar = z.object({ ... }) // duplicação!
|
|
```
|
|
|
|
### 4. Nomenclatura obrigatória de schemas
|
|
|
|
| Padrão | Uso | Exemplo |
|
|
|--------|-----|---------|
|
|
| `z{Entidade}Criar` | Criação de registro | `zUsuarioCriar` |
|
|
| `z{Entidade}Editar` | Edição de registro | `zUsuarioEditar` |
|
|
| `z{Entidade}Filtro` | Filtros de listagem | `zUsuarioFiltro` |
|
|
| `z{Entidade}Resposta` | Resposta de API | `zUsuarioResposta` |
|
|
| `z{Entidade}Params` | Parâmetros de rota | `zUsuarioParams` |
|
|
| `z{Modulo}Config` | Configuração do módulo | `zGeeConfig` |
|
|
| `z{Modulo}Ambiente` | Variáveis de ambiente | `zBackAmbiente` |
|
|
|
|
Prefixo `z` minúsculo + nome em `PascalCase`.
|
|
|
|
### 5. Usar `safeParse` em vez de `parse` para erros controlados
|
|
|
|
```typescript
|
|
// ✅ CORRETO — erro controlado
|
|
const resultado = zUsuarioCriar.safeParse(dados)
|
|
if (!resultado.success) {
|
|
return respostaComuns.erro(resultado.error.issues[0].message)
|
|
}
|
|
const usuario = resultado.data // tipado automaticamente
|
|
|
|
// ❌ ERRADO — throw não controlado
|
|
const usuario = zUsuarioCriar.parse(dados) // lança ZodError
|
|
```
|
|
|
|
A exceção é em contextos onde o throw é desejável (middleware global, início de pipeline).
|
|
|
|
### 6. Nunca usar validação manual quando Zod resolve
|
|
|
|
```typescript
|
|
// ❌ ERRADO — validação manual
|
|
if (!dados.nome || typeof dados.nome !== "string") {
|
|
return erro("Nome é obrigatório")
|
|
}
|
|
if (!dados.email || !dados.email.includes("@")) {
|
|
return erro("Email inválido")
|
|
}
|
|
|
|
// ✅ CORRETO — Zod faz tudo
|
|
const resultado = zUsuarioCriar.safeParse(dados)
|
|
if (!resultado.success) {
|
|
return erro(resultado.error.issues[0].message)
|
|
}
|
|
```
|
|
|
|
### 7. Transformações e defaults pertencem ao schema
|
|
|
|
```typescript
|
|
// ✅ CORRETO — transformação no schema
|
|
export const zProdutoCriar = z.object({
|
|
nome: z.string().trim().min(1),
|
|
preco: z.coerce.number().positive(),
|
|
ativo: z.boolean().default(true),
|
|
codigo: z.string().toUpperCase().optional(),
|
|
tags: z.array(z.string()).default([]),
|
|
})
|
|
|
|
// ❌ ERRADO — transformação manual após validação
|
|
const produto = zProdutoCriar.parse(dados)
|
|
produto.nome = produto.nome.trim()
|
|
produto.codigo = produto.codigo?.toUpperCase()
|
|
```
|
|
|
|
### 8. Composição de schemas em vez de duplicação
|
|
|
|
```typescript
|
|
// ✅ CORRETO — composição
|
|
const zEndereco = z.object({
|
|
rua: z.string(),
|
|
cidade: z.string(),
|
|
uf: z.string().length(2),
|
|
})
|
|
|
|
const zEmpresaCriar = z.object({
|
|
nome: z.string(),
|
|
endereco: zEndereco,
|
|
})
|
|
|
|
const zEmpresaEditar = zEmpresaCriar.partial() // todos opcionais
|
|
|
|
const zEmpresaResposta = zEmpresaCriar.extend({
|
|
codigo: z.string().uuid(),
|
|
criadoEm: z.string().datetime(),
|
|
})
|
|
|
|
// ❌ ERRADO — copiar campos entre schemas
|
|
const zEmpresaEditar = z.object({
|
|
nome: z.string().optional(), // duplicou!
|
|
endereco: z.object({ // duplicou!
|
|
rua: z.string().optional(),
|
|
cidade: z.string().optional(),
|
|
}),
|
|
})
|
|
```
|
|
|
|
### 9. Enums Zod para valores de domínio
|
|
|
|
```typescript
|
|
// ✅ CORRETO — enum Zod quando os valores vêm de dados externos
|
|
export const zStatusLicenca = z.enum(["ativa", "vencida", "suspensa", "cancelada"])
|
|
export type tipoStatusLicenca = z.infer<typeof zStatusLicenca>
|
|
|
|
// Usar em outros schemas
|
|
const zLicencaCriar = z.object({
|
|
status: zStatusLicenca,
|
|
// ...
|
|
})
|
|
|
|
// ✅ TAMBÉM CORRETO — TypeScript enum quando já existe no projeto
|
|
// e o schema referencia o enum
|
|
import { StatusLicenca } from "./enums"
|
|
const zLicencaCriar = z.object({
|
|
status: z.nativeEnum(StatusLicenca),
|
|
})
|
|
```
|
|
|
|
### 10. Bibliotecas proibidas para validação
|
|
|
|
| ❌ Proibido | Motivo |
|
|
|-------------|--------|
|
|
| `joi` | Não infere tipos TS, bundle pesado |
|
|
| `yup` | Inferência fraca, API menos ergonômica |
|
|
| `class-validator` | Decorators, não compartilhável com frontend |
|
|
| `io-ts` | Verboso, curva de aprendizado alta |
|
|
| `ajv` (direto) | JSON Schema — usar `zod-to-json-schema` se precisar de JSON Schema |
|
|
| Validação manual com `if/typeof` | Quando Zod resolve o mesmo caso |
|
|
|
|
---
|
|
|
|
## Exemplos
|
|
|
|
### API backend (Express)
|
|
|
|
```typescript
|
|
// ✅ Correto
|
|
import { zUsuarioCriar } from "projeto-comuns/tabelas/usuarios"
|
|
|
|
app.post("/usuarios", async (req, res) => {
|
|
const resultado = zUsuarioCriar.safeParse(req.body)
|
|
if (!resultado.success) {
|
|
return res.status(400).json({ erro: resultado.error.issues })
|
|
}
|
|
const usuario = await criarUsuario(resultado.data)
|
|
return res.json(usuario)
|
|
})
|
|
```
|
|
|
|
### Webhook com fila
|
|
|
|
```typescript
|
|
// ✅ Correto — valida antes de enfileirar
|
|
const zPayloadWebhook = z.object({
|
|
tabela: z.enum(["empreendedores", "empreendimentos", "usuarios"]),
|
|
dados: z.array(z.unknown()).min(1),
|
|
})
|
|
|
|
app.post("/webhook/:tabela", async (req, res) => {
|
|
const resultado = zPayloadWebhook.safeParse({
|
|
tabela: req.params.tabela,
|
|
dados: req.body,
|
|
})
|
|
if (!resultado.success) {
|
|
return res.status(400).json({ erro: "Payload inválido" })
|
|
}
|
|
await fila.add("sync", resultado.data)
|
|
return res.json({ ok: true })
|
|
})
|
|
```
|
|
|
|
### Variáveis de ambiente
|
|
|
|
```typescript
|
|
// ✅ Correto — validar no boot da aplicação
|
|
const zAmbiente = z.object({
|
|
DATABASE_URL: z.string().url(),
|
|
REDIS_HOST: z.string().default("localhost"),
|
|
REDIS_PORT: z.coerce.number().default(6379),
|
|
BASE_URL: z.string().url(),
|
|
SESSAO_BACK: z.string().uuid(),
|
|
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
})
|
|
|
|
export const ambiente = () => zAmbiente.parse(process.env)
|
|
```
|
|
|
|
### Frontend (formulário Vue)
|
|
|
|
```typescript
|
|
// ✅ Correto — schema compartilhado validando antes de enviar
|
|
import { zUsuarioCriar } from "projeto-comuns/tabelas/usuarios"
|
|
|
|
const salvar = async () => {
|
|
const resultado = zUsuarioCriar.safeParse(formulario.value)
|
|
if (!resultado.success) {
|
|
erros.value = resultado.error.flatten().fieldErrors
|
|
return
|
|
}
|
|
await api.post("/usuarios", resultado.data)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Impacto
|
|
|
|
Sem esta regra:
|
|
|
|
- Cada desenvolvedor escolhe uma estratégia diferente de validação (if/typeof, joi, yup, etc.)
|
|
- Tipos e schemas ficam dessincronizados — mudança no schema não reflete no tipo e vice-versa
|
|
- Validações manuais são incompletas e inconsistentes entre endpoints
|
|
- Código de validação é duplicado entre front e back
|
|
- Dados inválidos passam despercebidos e corrompem a base
|
|
|
|
Com esta regra:
|
|
|
|
- Uma única fonte de verdade: o schema Zod
|
|
- Tipos TypeScript sempre sincronizados via `z.infer<>`
|
|
- Validação consistente em todos os pontos de entrada
|
|
- Reutilização entre front, back e workers via pacote `*-comuns`
|
|
- Erros de validação estruturados e internacionalizáveis
|
|
|
|
---
|
|
|
|
## Referências
|
|
|
|
- Rule: `global/rules/typescript-sem-any` (narrowing com Zod mencionado como alternativa)
|
|
- Skill: `global/skills/criar-schema-zod/SKILL` (guia prático para criar schemas)
|