From 1e9fcdeb36877f1fdf92023a2069324d6e38d410 Mon Sep 17 00:00:00 2001 From: Luiz Silva Date: Tue, 8 Sep 2026 11:51:29 -0300 Subject: [PATCH] nullish() --- global/rules/zod-validacao-padrao.md | 32 +++++++++++++++++++++++-- global/skills/criar-schema-zod/SKILL.md | 21 ++++++++++++++++ rules/zod-validacao-padrao.md | 32 +++++++++++++++++++++++-- skills/criar-schema-zod/SKILL.md | 21 ++++++++++++++++ 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/global/rules/zod-validacao-padrao.md b/global/rules/zod-validacao-padrao.md index 36fa24f..8388eaa 100644 --- a/global/rules/zod-validacao-padrao.md +++ b/global/rules/zod-validacao-padrao.md @@ -139,7 +139,7 @@ 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(), + codigo: z.string().toUpperCase().nullish(), tags: z.array(z.string()).default([]), }) @@ -149,7 +149,35 @@ produto.nome = produto.nome.trim() produto.codigo = produto.codigo?.toUpperCase() ``` -### 8. Composição e Enums Zod +### 8. Colunas e Registros que Podem ser Nulos Devem Usar `.nullish()` + +Em schemas de tabelas, entidades e contratos de dados que mapeiam colunas do banco de dados ou campos de formulário/API que podem ser nulos ou omitidos, **DEVE-SE obrigatoriamente usar `.nullish()`** (nunca `.optional()` ou `.nullable()` isolados). + +**Por que `.nullish()` é obrigatório?** +- **Bancos relacionais (PostgreSQL/Knex)**: colunas nulas retornam `null`. +- **Formulários e payloads parciais**: frequentemente omitem o campo (`undefined`) ou enviam `null` ao limpar. +- `.optional()` aceita apenas `undefined` — **falha** com `Expected string, received null` ao receber dados do banco ou payload com `null`. +- `.nullable()` aceita apenas `null` — **falha** com `Required` ao omitir o campo em atualizações parciais (`undefined`). +- `.nullish()` aceita ambos (`T | null | undefined`), garantindo interoperabilidade total entre banco, API e frontend sem erros de runtime. + +```typescript +// ✅ CORRETO — coluna/registro opcional ou nulo com .nullish() +export const zPontosBase = z.object({ + id: z.string().max(64).nullish(), + nome: z.string().min(1), + descricao: z.string().max(5000).nullish(), + codigo_empreendimento: zUUID.nullish(), + coordenadas: zCoordenadas.nullish(), +}) + +// ❌ ERRADO — quebra ao ler do banco quando a coluna for NULL +descricao: z.string().optional() + +// ❌ ERRADO — quebra se o campo for omitido no payload (undefined) +descricao: z.string().nullable() +``` + +### 9. Composição e Enums Zod ```typescript // ✅ Composição de schemas via extend/partial diff --git a/global/skills/criar-schema-zod/SKILL.md b/global/skills/criar-schema-zod/SKILL.md index e517f51..c9ed425 100644 --- a/global/skills/criar-schema-zod/SKILL.md +++ b/global/skills/criar-schema-zod/SKILL.md @@ -72,6 +72,26 @@ export const zUsuarioCriar = z.object({ | `z{Modulo}Config` | Configuração | | `z{Modulo}Ambiente` | Variáveis de ambiente | +### 2.4. Colunas e Campos que Podem ser Nulos: Usar `.nullish()` + +Toda coluna do banco de dados relacional ou campo de entidade/registro que puder ser nulo ou omitido **DEVE** utilizar `.nullish()` (nunca `.optional()` ou `.nullable()` isolados): + +```typescript +// ✅ CORRETO — aceita null (do banco PostgreSQL) e undefined (payload omitido) +export const zPontoCriar = z.object({ + nome: z.string().min(1), + descricao: z.string().max(5000).nullish(), + codigo_empreendimento: zUUID.nullish(), + coordenadas: zCoordenadas.nullish(), +}) + +// ❌ ERRADO — quebra com null retornado pelo Knex/PostgreSQL +descricao: z.string().optional(), + +// ❌ ERRADO — quebra quando a chave é omitida no payload (undefined) +descricao: z.string().nullable(), +``` + --- ## PASSO 3 — Derivar o Tipo com `z.infer<>` @@ -175,6 +195,7 @@ Zero erros = concluído. - [ ] A validação usa `safeParse` (exceto em boot/middleware)? - [ ] A nomenclatura segue o padrão `z{Entidade}{Acao}`? - [ ] Transformações (trim, coerce, default) estão no schema, não no handler? +- [ ] Colunas e campos que podem ser nulos/opcionais utilizam `.nullish()` (em vez de `.optional()` ou `.nullable()` isolados)? - [ ] Schemas derivados usam composição (.partial, .extend, .pick), não duplicação? - [ ] `tsc --noEmit` passa sem erros? diff --git a/rules/zod-validacao-padrao.md b/rules/zod-validacao-padrao.md index 36fa24f..8388eaa 100644 --- a/rules/zod-validacao-padrao.md +++ b/rules/zod-validacao-padrao.md @@ -139,7 +139,7 @@ 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(), + codigo: z.string().toUpperCase().nullish(), tags: z.array(z.string()).default([]), }) @@ -149,7 +149,35 @@ produto.nome = produto.nome.trim() produto.codigo = produto.codigo?.toUpperCase() ``` -### 8. Composição e Enums Zod +### 8. Colunas e Registros que Podem ser Nulos Devem Usar `.nullish()` + +Em schemas de tabelas, entidades e contratos de dados que mapeiam colunas do banco de dados ou campos de formulário/API que podem ser nulos ou omitidos, **DEVE-SE obrigatoriamente usar `.nullish()`** (nunca `.optional()` ou `.nullable()` isolados). + +**Por que `.nullish()` é obrigatório?** +- **Bancos relacionais (PostgreSQL/Knex)**: colunas nulas retornam `null`. +- **Formulários e payloads parciais**: frequentemente omitem o campo (`undefined`) ou enviam `null` ao limpar. +- `.optional()` aceita apenas `undefined` — **falha** com `Expected string, received null` ao receber dados do banco ou payload com `null`. +- `.nullable()` aceita apenas `null` — **falha** com `Required` ao omitir o campo em atualizações parciais (`undefined`). +- `.nullish()` aceita ambos (`T | null | undefined`), garantindo interoperabilidade total entre banco, API e frontend sem erros de runtime. + +```typescript +// ✅ CORRETO — coluna/registro opcional ou nulo com .nullish() +export const zPontosBase = z.object({ + id: z.string().max(64).nullish(), + nome: z.string().min(1), + descricao: z.string().max(5000).nullish(), + codigo_empreendimento: zUUID.nullish(), + coordenadas: zCoordenadas.nullish(), +}) + +// ❌ ERRADO — quebra ao ler do banco quando a coluna for NULL +descricao: z.string().optional() + +// ❌ ERRADO — quebra se o campo for omitido no payload (undefined) +descricao: z.string().nullable() +``` + +### 9. Composição e Enums Zod ```typescript // ✅ Composição de schemas via extend/partial diff --git a/skills/criar-schema-zod/SKILL.md b/skills/criar-schema-zod/SKILL.md index e517f51..c9ed425 100644 --- a/skills/criar-schema-zod/SKILL.md +++ b/skills/criar-schema-zod/SKILL.md @@ -72,6 +72,26 @@ export const zUsuarioCriar = z.object({ | `z{Modulo}Config` | Configuração | | `z{Modulo}Ambiente` | Variáveis de ambiente | +### 2.4. Colunas e Campos que Podem ser Nulos: Usar `.nullish()` + +Toda coluna do banco de dados relacional ou campo de entidade/registro que puder ser nulo ou omitido **DEVE** utilizar `.nullish()` (nunca `.optional()` ou `.nullable()` isolados): + +```typescript +// ✅ CORRETO — aceita null (do banco PostgreSQL) e undefined (payload omitido) +export const zPontoCriar = z.object({ + nome: z.string().min(1), + descricao: z.string().max(5000).nullish(), + codigo_empreendimento: zUUID.nullish(), + coordenadas: zCoordenadas.nullish(), +}) + +// ❌ ERRADO — quebra com null retornado pelo Knex/PostgreSQL +descricao: z.string().optional(), + +// ❌ ERRADO — quebra quando a chave é omitida no payload (undefined) +descricao: z.string().nullable(), +``` + --- ## PASSO 3 — Derivar o Tipo com `z.infer<>` @@ -175,6 +195,7 @@ Zero erros = concluído. - [ ] A validação usa `safeParse` (exceto em boot/middleware)? - [ ] A nomenclatura segue o padrão `z{Entidade}{Acao}`? - [ ] Transformações (trim, coerce, default) estão no schema, não no handler? +- [ ] Colunas e campos que podem ser nulos/opcionais utilizam `.nullish()` (em vez de `.optional()` ou `.nullable()` isolados)? - [ ] Schemas derivados usam composição (.partial, .extend, .pick), não duplicação? - [ ] `tsc --noEmit` passa sem erros?