UNPKG

@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
# 🔍 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