Schema Generator
O que é
O generator lê YAMLs de {module}/schemas/ e gera código em 3 camadas — tipos + clients compartilhados, frontend Angular e backend Fastify. Os YAMLs são a fonte única de verdade para modelos, campos, dicionários, query configs, actions, rotas e pages de UI.
Fluxo
{module}/schemas/{domain}.yaml
↓ pnpm generate
├── libs/shared/src/{module}/{domain}/_gen/ → Tipos TS, Zod schemas, dicionários, query configs, client tipado, base actions
├── {module}/web/src/app/domains/{domain}/_gen/ → Base services (frontend Angular)
└── {module}/api/src/routes/{domain}/_gen/ → Rotas e base action classes (backend Fastify)
Módulos suportados: cms, hub, platform, tenant, user.
Comandos
pnpm generate # Regenera tudo
pnpm generate:list # Visão geral de domínios, actions e mapeamento
pnpm generate:check # Verifica sincronização
O que é gerado por camada
Shared (libs/shared/src/{module}/{domain}/_gen/)
| Arquivo | Conteúdo |
|---|---|
types.ts | Interface TypeScript do model principal + embedded models |
schemas.ts | Zod schema para validação |
dictionaries.ts | Objetos de dicionário (status, enums) com label/color/icon |
query-config.ts | Config de query para Kysely (tabela, joins, colunas, filtros) |
client.ts | Facade tipada <Domain>Client + factory create<Domain>Client(http, baseUrl) |
actions/<name>.ts | Uma Base<Name> por action, com execute(...) tipado |
Frontend ({module}/web/src/app/domains/{domain}/_gen/)
| Arquivo | Conteúdo |
|---|---|
services/base-{domain}-service.ts | DomainService com pages, columns, filters, form fields |
Backend ({module}/api/src/routes/{domain}/_gen/)
| Arquivo | Conteúdo |
|---|---|
base-{domain}-routes.ts | Classe Base<Domain>Routes que instancia cada action e liga no Fastify |
actions/<name>.ts | Base<Name> — uma classe por action com template CRUD ou throw-stub |
base-{domain}-routes.test.ts | Contract tests — valida response com Zod derivado do tipo declarado |
Actions concretas ficam em {module}/api/src/routes/{domain}/actions/<name>.ts (só criadas se não existirem; preservam overrides).
Regras
- Nunca editar
_gen/— são sobrescritos a cada geração - Sempre editar o YAML para alterar modelo, campo, dicionário, action ou page
- Lógica custom (override de SQL, side effects, etc.) vai em
{domain}/actions/<name>.tsextendendo aBase<Name>gerada - Schemas compartilhados são expostos via
@hg/shared/{module}/{domain}(barrel do pacote)
Estrutura de um YAML
name: page
description: Páginas de sites com editor visual
service: cms
table: cms_pages
parent: site # Relacionamento hierárquico
tenant_scoped: true # RLS ativado no banco tenant
models:
Page: # Model principal
fields:
id:
type: string
label: ID
required: true
title:
type: string
label: Título
required: true
status:
type: enum
dictionary: page_status
label: Status
required: true
puck_data:
type: object
label: Dados do Editor
json: true # Mapeia para JSONB
default: '{}'
PageSeo: # Embedded model (sem tabela própria)
embedded: true
fields:
seo_title: { type: string, label: Título SEO }
seo_description: { type: string, label: Descrição SEO }
dictionaries:
page_status:
draft: { label: Rascunho, color: gray-soft, icon: file-edit }
published: { label: Publicado, color: success-soft, icon: check-circle }
actions:
- name: fetch
method: GET
path: ''
response: 'Page[]'
ui:
type: search
meta: { icon: file-text, empty: Nenhuma página encontrada }
columns:
- { key: title, label: Título, searchable: true }
- { key: status, label: Status, type: badge, dictionary: page_status }
filters:
status: { label: Status, type: select, mode: client, dictionary: page_status }
paginable: true
searchable: true
- { name: findById, method: GET, path: '/:id', response: 'Page' }
- name: create
method: POST
path: ''
request: 'Partial<Page>'
response: '{ id: string }'
ui:
type: form
label: Criar Página
icon: plus
fields:
slug: { label: Slug, required: true }
title: { label: Título, required: true }
- { name: update, method: PUT, path: '/:id', request: 'Partial<Page>', response: '{ ok: boolean }' }
- { name: delete, method: DELETE, path: '/:id', response: '{ ok: boolean }' }
- { name: publish, method: POST, path: '/:id/publish', response: '{ ok: boolean }' }
Action.ui — manifestação da action na UI
Cada action pode declarar ui: com sua forma de aparecer no frontend:
ui.type | Onde aparece | O que gera |
|---|---|---|
search | Lista/tabela do domínio | columns, filters, joins, paginação, sort |
form | Modal de criação/edição | Form com fields (TODO: depends, mask, source) |
view | Detalhe/visualização | Seções, campos read-only (TODO) |
button | Botão em row ou header | Label, ícone, confirm (TODO) |
O bloco pages: no topo do YAML é legacy — em transição pra ser removido. Toda UI nova vai em actions[].ui.
Ações canônicas vs declared
Os nomes fetch, findById, create, update, delete recebem template CRUD padrão no backend (SQL gerado a partir da tabela + fields). Qualquer outro nome (publish, reprocess, findBySlug, login, etc.) gera base throw-stub que obriga a classe concreta a fornecer implementação real.
A forma no YAML é sempre a mesma — objeto ActionDefinition completo, sem açúcar/shortcut.
Campos especiais no YAML
| Campo | Efeito |
|---|---|
service | Módulo dono (cms, hub, platform, tenant, user) |
table | Nome da tabela no banco (com prefixo de BC: cms_pages, plt_brands) |
parent | Domínio pai — rotas ficam aninhadas (/sites/:siteId/pages) |
base_path | Override do path padrão /<name>s (ex: /auth, /dlq, /gateway) |
frontend_path | Path customizado no frontend (default = nome do domínio) |
tenant_scoped | Ativa RLS (withTenant) nas queries geradas. Default: true |
short_id | Gera short IDs (12 chars) via generateId('short') no create |
embedded: true | Model sem tabela — gera tipo auxiliar |
json: true | Campo mapeado como JSONB |
dictionary | Referência a dicionário para enum fields |
Workflow
Alterar modelo existente
- Editar
{module}/schemas/{domain}.yaml pnpm generate- Se alterou esquema do banco: criar migration em
db/{public,tenant}/migrations/
Criar novo domínio
pnpm generate:list— verificar em qual módulo pertence- Criar
{module}/schemas/{novo}.yaml pnpm generate- Implementar lógica custom em
{module}/api/src/routes/{novo}/actions/<name>.ts(extendBase<Name>) - Criar pages/actions no frontend em
{module}/web/src/app/domains/{novo}/