@horizon-apps/domain-schema-core
Version:
Core domain schema utilities for Horizon Platform - Schema generators, data enrichers, converters and specifications
612 lines (527 loc) • 15.9 kB
Markdown
# 🔍 Search Schema Specification
## Visão Geral
O **Search Schema** é a especificação que define a estrutura completa de uma interface de busca, incluindo campos, comportamentos, regras condicionais e metadata de UI. Diferente do [Domain Schema](/__domain-data-schema-specification.md) que representa a estrutura do banco de dados, o Search Schema é focado na experiência de busca do usuário.
## 📋 Estrutura Principal
```typescript
interface SearchSchema {
// Campos principais com metadata completo
fields: Record<string, FieldMetadata>
// Campos especiais na raiz (não em "root")
fts?: FtsMetadata // Full-text search
geom?: GeomMetadata // Geometria/localização
page?: PaginationMetadata // Paginação
limit?: LimitMetadata // Limite de resultados
sort?: SortMetadata // Ordenação
layout?: LayoutMetadata // Layout da página
zoom?: ZoomMetadata // Zoom do mapa
bbox?: BboxMetadata // Bounding box
center?: CenterMetadata // Centro do mapa
card?: CardMetadata // Estilo dos cards
}
```
## 🎯 Field Metadata
Cada campo em `fields` possui metadata completo para UI e comportamento:
```typescript
interface FieldMetadata {
// === Identificação ===
key: string // Chave do campo no estado
// === UI/Renderização ===
label: string // Label exibido
type: FieldType // Tipo do componente
placeholder?: string // Placeholder
icon?: string // Ícone do campo
helperText?: string // Texto de ajuda
// === Comportamento ===
operator?: OperatorType // Operador para arrays/ranges
multiple?: boolean // Permite múltiplos valores
default?: any // Valor padrão
required?: boolean // Campo obrigatório
minSelected?: number // Mínimo de seleções
maxSelected?: number // Máximo de seleções
// === Relacionamentos ===
parent?: string // Campo pai (hierarquia)
// === Regras Condicionais ===
when?: string // Condição inline para visibilidade
disabled?: string // Condição para disabled
required?: string // Condição para required
// === Options ===
options?: FieldOption[] // Opções estáticas
optionsSource?: 'static' | 'faceted' | 'api' // Fonte das options
optionsProvider?: (state: any) => FieldOption[] // Provider dinâmico
// === Eventos ===
onChange?: 'submit' | 'default' // Comportamento ao mudar
debounce?: number // Delay antes de processar
// === Validação ===
validation?: ValidationRule[] // Regras de validação
min?: number // Valor mínimo (ranges)
max?: number // Valor máximo (ranges)
minLength?: number // Comprimento mínimo
maxLength?: number // Comprimento máximo
pattern?: string // Regex de validação
}
```
### Field Types
```typescript
type FieldType =
| 'select' // Dropdown simples
| 'multiselect' // Seleção múltipla
| 'range' // Range de valores
| 'search' // Campo de texto/busca
| 'boolean' // Checkbox/toggle
| 'date' // Seletor de data
| 'daterange' // Range de datas
| 'radio' // Radio buttons
| 'slider' // Slider numérico
| 'tags' // Input de tags
| 'autocomplete' // Autocomplete
```
### Operator Types
```typescript
type OperatorType =
| 'and' // Todos os valores (arrays)
| 'or' // Qualquer valor (arrays)
| 'equals' // Igualdade exata
| 'contains' // Contém texto
| 'between' // Entre min e max
| 'gte' // Maior ou igual
| 'lte' // Menor ou igual
| 'gt' // Maior que
| 'lt' // Menor que
| 'in' // Está em lista
| 'not' // Negação
```
## 🔀 Field Options
Options podem ter condições próprias:
```typescript
interface FieldOption {
value: any // Valor da option
label: string // Label exibido
icon?: string // Ícone opcional
description?: string // Descrição adicional
when?: string // Condição inline para exibir
disabled?: string // Condição para disabled
group?: string // Grupo da option
metadata?: any // Metadata adicional
}
```
## 📝 Conditions (Regras Inline)
Conditions usam expressões inline simples e legíveis:
### Sintaxe Básica
```javascript
// Igualdade
"operacao === 'venda'"
"tipo !== 'comercial'"
// Comparações
"quartos > 2"
"valor_venda >= 500000"
// Arrays/Contains
"operacao.includes('venda')"
"caracteristicas.length > 0"
// Lógica
"operacao === 'venda' && tipo === 'residencial'"
"quartos > 2 || area_total > 100"
// Existência
"valor_venda" // verifica se existe/tem valor
"!valor_locacao" // verifica se NÃO existe
// Múltiplas condições
"(operacao === 'venda' || operacao === 'permuta') && tipo === 'residencial'"
```
### Exemplos de Uso
```typescript
{
// Campo só aparece para venda
valor_venda: {
type: 'range',
when: "operacao === 'venda'"
},
// Option só aparece se tem filtro de área
sort: {
options: [
{
value: 'area_desc',
label: 'Maior área',
when: 'area_total > 0'
}
]
},
// Campo desabilitado condicionalmente
subtipo: {
type: 'select',
disabled: "!tipo" // disabled se tipo não selecionado
}
}
```
## 🌐 Root Fields (Campos Especiais)
### FTS (Full-Text Search)
```typescript
interface FtsMetadata {
placeholder?: string
operator?: 'websearch' | 'plainto' | 'phrase' | 'boolean'
debounce?: number
minLength?: number
onChange?: 'submit' | 'default'
}
```
### Geom (Geometria/Localização)
```typescript
interface GeomMetadata {
operation?: 'within' | 'intersects' | 'near'
defaultZoom?: number
defaultCenter?: { lat: number, lng: number }
enableDrawing?: boolean
enableClustering?: boolean
}
```
### Sort (Ordenação)
```typescript
interface SortMetadata {
default?: string
options?: SortOption[]
onChange?: 'submit' | 'default'
}
interface SortOption {
value: string
label: string
when?: string // Condição para exibir
}
```
### Page & Limit
```typescript
interface PaginationMetadata {
default?: number
min?: number
max?: number
onChange?: 'submit' | 'default'
}
interface LimitMetadata {
default?: number
options?: number[] | LimitOption[]
onChange?: 'submit' | 'default'
}
```
### Layout & Card
```typescript
interface LayoutMetadata {
default?: 'list' | 'map' | 'map-list'
options?: LayoutOption[]
}
interface CardMetadata {
default?: 'horizontal' | 'vertical' | 'compact'
options?: CardOption[]
}
```
## 🎯 Exemplo Completo: Busca de Imóveis
```typescript
const propertySearchSchema: SearchSchema = {
// === Fields ===
fields: {
operacao: {
key: 'operacao',
label: 'Operação',
type: 'select',
multiple: true,
operator: 'or',
default: ['venda'],
minSelected: 1,
options: [
{ value: 'venda', label: 'Venda' },
{ value: 'locacao', label: 'Locação' },
{ value: 'permuta', label: 'Permuta' }
],
onChange: 'submit'
},
tipo: {
key: 'tipo',
label: 'Tipo de Imóvel',
type: 'select',
placeholder: 'Selecione o tipo',
options: [
{ value: 'residencial', label: 'Residencial' },
{
value: 'comercial',
label: 'Comercial',
when: "operacao.includes('venda')" // Comercial só para venda
}
]
},
subtipo: {
key: 'subtipo',
label: 'Subtipo',
type: 'multiselect',
parent: 'tipo', // Relacionado ao tipo
operator: 'and',
disabled: "!tipo", // Disabled até selecionar tipo
options: [
{
value: 'apartamento',
label: 'Apartamento',
when: "tipo === 'residencial'"
},
{
value: 'casa',
label: 'Casa',
when: "tipo === 'residencial'"
},
{
value: 'loja',
label: 'Loja',
when: "tipo === 'comercial'"
},
{
value: 'sala',
label: 'Sala Comercial',
when: "tipo === 'comercial'"
}
]
},
endereco_cidade: {
key: 'endereco_cidade',
label: 'Cidade',
type: 'autocomplete',
placeholder: 'Digite a cidade...',
optionsSource: 'api',
onChange: 'submit'
},
endereco_bairro: {
key: 'endereco_bairro',
label: 'Bairros',
type: 'multiselect',
parent: 'endereco_cidade',
operator: 'and',
disabled: "!endereco_cidade",
optionsSource: 'faceted',
placeholder: 'Selecione os bairros'
},
valor_venda: {
key: 'valor_venda',
label: 'Valor de Venda',
type: 'range',
operator: 'between',
when: "operacao.includes('venda')", // Só aparece para venda
min: 50000,
max: 10000000,
placeholder: {
min: 'Valor mínimo',
max: 'Valor máximo'
}
},
valor_locacao: {
key: 'valor_locacao',
label: 'Valor de Locação',
type: 'range',
operator: 'between',
when: "operacao.includes('locacao')", // Só aparece para locação
min: 500,
max: 50000
},
quartos: {
key: 'quartos',
label: 'Quartos',
type: 'range',
operator: 'gte', // Maior ou igual
min: 1,
max: 10,
default: { min: 2 }
},
area_total: {
key: 'area_total',
label: 'Área Total (m²)',
type: 'range',
operator: 'between',
min: 20,
max: 1000
},
caracteristicas: {
key: 'caracteristicas',
label: 'Características',
type: 'multiselect',
operator: 'and',
optionsSource: 'faceted',
options: [
{ value: 'piscina', label: 'Piscina' },
{ value: 'churrasqueira', label: 'Churrasqueira' },
{ value: 'academia', label: 'Academia' },
{ value: 'playground', label: 'Playground' },
{ value: 'salao_festas', label: 'Salão de Festas' },
{
value: 'ar_condicionado',
label: 'Ar Condicionado',
when: "tipo === 'residencial'"
}
]
}
},
// === Root Fields ===
fts: {
placeholder: 'Buscar por endereço, código ou descrição...',
operator: 'websearch',
debounce: 500,
minLength: 3,
onChange: 'submit'
},
geom: {
operation: 'within',
enableDrawing: true,
enableClustering: true,
defaultZoom: 12
},
sort: {
default: 'updated_at_desc',
onChange: 'submit',
options: [
{ value: 'updated_at_desc', label: 'Mais recentes' },
{ value: 'updated_at_asc', label: 'Mais antigos' },
{
value: 'valor_venda_asc',
label: 'Menor preço',
when: "valor_venda && operacao.includes('venda')"
},
{
value: 'valor_venda_desc',
label: 'Maior preço',
when: "valor_venda && operacao.includes('venda')"
},
{
value: 'valor_locacao_asc',
label: 'Menor aluguel',
when: "valor_locacao && operacao.includes('locacao')"
},
{
value: 'area_total_desc',
label: 'Maior área',
when: "area_total > 0"
},
{
value: 'quartos_desc',
label: 'Mais quartos',
when: "quartos >= 1"
}
]
},
page: {
default: 1,
min: 1,
onChange: 'submit'
},
limit: {
default: 20,
onChange: 'submit',
options: [
{ value: 10, label: '10 por página' },
{ value: 20, label: '20 por página' },
{ value: 50, label: '50 por página' },
{ value: 100, label: '100 por página' }
]
},
layout: {
default: 'list',
options: [
{ value: 'list', label: 'Lista', icon: 'list' },
{ value: 'map', label: 'Mapa', icon: 'map' },
{ value: 'map-list', label: 'Mapa + Lista', icon: 'layout' }
]
},
card: {
default: 'horizontal',
options: [
{ value: 'horizontal', label: 'Horizontal' },
{ value: 'vertical', label: 'Vertical' },
{ value: 'compact', label: 'Compacto' }
]
}
}
```
## 🔄 Integração com Search State Enricher
O Search Schema é usado pelo `SearchStateEnricher` para enriquecer o estado da URL com operadores e metadata:
```typescript
// Estado simples da URL
const urlState = {
filters: {
operacao: ['venda', 'locacao'],
tipo: 'apartamento',
quartos: { min: 2 }
},
fts: 'centro',
page: 2
}
// Enriquecido com base no schema
const enrichedState = {
filters: {
operacao: { or: ['venda', 'locacao'] }, // operator: 'or' do schema
tipo: 'apartamento',
quartos: { gte: 2 } // operator: 'gte' do schema
},
fts: {
value: 'centro',
operator: 'websearch' // do schema.fts.operator
},
page: 2
}
```
## 🎮 Integração com Schema Controller
O `SearchStateSchemaController` usa o schema para:
1. **Avaliar visibilidade** - `when` conditions
2. **Filtrar options** - Options condicionais
3. **Determinar comportamento** - onChange submit/default
4. **Validar campos** - Validation rules
5. **Renderizar componentes** - Baseado no type
```typescript
// Controller processa campo com schema + estado
const controller = new SearchFieldController()
const output = controller.process({
fieldMetadata: schema.fields.valor_venda,
searchState: currentState,
globalConfig: { onChange: 'submit' }
})
// Output
{
isVisible: true, // operacao inclui 'venda'
isDisabled: false,
availableOptions: [],
shouldSubmitOnChange: true
}
```
## 📚 Diferenças do Domain Schema
| Aspecto | Domain Schema | Search Schema |
|---------|--------------|---------------|
| **Foco** | Estrutura do banco | Interface de busca |
| **Campos** | Todos do modelo | Seleção para busca |
| **Metadata** | Tipos, validações DB | UI, comportamento, conditions |
| **Uso** | Geração de código, migrations | Renderização de UI, enrichment |
| **Operadores** | SQL/Prisma | Busca/filtros |
| **Conditions** | Constraints DB | Regras de UI |
## 🔧 Configuração Global
O schema pode ser usado com configurações globais:
```typescript
const enricher = new SearchStateEnricher({
schema: propertySearchSchema,
options: {
defaults: {
arrays: { operator: 'and' }, // Default para todos os arrays
ranges: { operator: 'between' }, // Default para ranges
fts: { operator: 'websearch' } // Default para FTS
},
globalConfig: {
onChange: 'submit', // Todos os campos submetem ao mudar
debounce: 300, // Delay global
validateOnChange: true // Validar ao mudar
}
}
})
```
## 🎯 Best Practices
1. **Use conditions inline** - Mais legível que objetos
2. **Mantenha options simples** - Use facets para listas grandes
3. **Defina defaults úteis** - Melhora UX inicial
4. **Use parent relationships** - Para campos dependentes
5. **Configure onChange wisely** - Balance entre responsividade e performance
6. **Valide no schema** - Não no componente
7. **Use operators corretos** - AND para características, OR para operações
8. **Documente conditions** - Para manutenção futura
## 🚀 Próximos Passos
1. Implementar `SearchStateSchemaController`
2. Criar componentes genéricos por type
3. Integrar com `SearchContext`
4. Adicionar suporte a facets dinâmicos
5. Implementar validação em tempo real
6. Criar builders visuais de schema