UNPKG

@horizon-apps/domain-schema-core

Version:

Core domain schema utilities for Horizon Platform - Schema generators, data enrichers, converters and specifications

485 lines (399 loc) 14 kB
# 🔗 Integração Completa: URL Parser + SQL Builder (v1.2.8) Documentação do pipeline completo desde URLs SEO-friendly até SQLs PostgreSQL otimizados. ## 🏗️ **Arquitetura da Integração** ``` Frontend State → URL Builder → Browser URL → URL Parser → Search Adapter → SQL Builder → PostgreSQL ↓ ↓ ↓ ↓ ↓ ↓ ↓ Zustand Query Params SEO-Friendly Parse State Field Mapping GenericReq SQL Query { search: "", ?q=casa /imoveis/ { filters: { search: { { filters, SELECT * filters: {} } &tipo=Casa casa-sp { tipo } value: "", search, FROM... &page=2 ?q=... page: 2 } method: "" }, include: [] } filters: {} } ``` ## 🎯 **Componentes da Integração** ### **1. SearchUrlBuilder - Serialização Bidirecional** - **Entrada**: Estado Zustand/React - **Saída**: URLs SEO-friendly com query params visíveis - **Funcionalidade**: Serialização ↔ Deserialização completa ### **2. SearchAdapter - Mapeamento e Transformação** - **Entrada**: Estado parseado da URL - **Saída**: IntermediateSearchFormat para SQL Builder - **Funcionalidade**: Field mapping + transformações de formato ### **3. PostgreSQL SQL Builder - Geração SQL** - **Entrada**: GenericRequest padronizado - **Saída**: SQLs PostgreSQL otimizados - **Funcionalidade**: Filter Engine + Builders especializados ## 🚀 **Pipeline Completo - Exemplo Prático** ### **Frontend State (Zustand)** ```typescript const searchState = { search: 'casa moderna 3 quartos', // FORA de filters filters: { tipo: ['Casa', 'Sobrado'], cidade: 'Curitiba', valor_venda_min: 300000, // Será convertido para gte valor_venda_max: 800000, // Será convertido para lte caracteristicas: ['Piscina'] }, page: 2, limit: 20, sort: 'valor_venda:desc' }; ``` ### **1. URL Building (Frontend → URL)** ```typescript import { SearchUrlBuilder } from '@horizon-apps/domain-schema-core'; const urlBuilder = new SearchUrlBuilder({ rootUrl: '/imoveis/', enableSlug: true }); const urlResult = urlBuilder.buildUrl(searchState); // urlResult.url: "/imoveis/casa-sobrado-curitiba?q=casa+moderna+3+quartos&tipo=Casa,Sobrado&cidade=Curitiba&valor_venda_gte=300000&valor_venda_lte=800000&page=2&sort=valor_venda:desc" ``` ### **2. URL Parsing (Browser → State)** ```typescript // No Next.js getServerSideProps const parsedState = urlBuilder.parseUrl(context); // parsedState: { filters: { tipo: ['Casa', 'Sobrado'], cidade: 'Curitiba', ... }, search: 'casa moderna 3 quartos', page: 2, ... } ``` ### **3. Search Adapter (State → IntermediateFormat)** ```typescript import { createImoveisAdapter } from '@horizon-apps/domain-schema-core/url-to-search-adapter'; const adapter = createImoveisAdapter(); const adaptedState = adapter.adapt(parsedState); // adaptedState (IntermediateSearchFormat): // { // search: { value: 'casa moderna 3 quartos', method: 'vector' }, // filters: { // tipo: ['Casa', 'Sobrado'], // endereco_cidade: 'Curitiba', // 🎯 Field mapping! // valor_venda: { gte: 300000, lte: 800000 }, // 🎯 Range já processado! // caracteristicas: ['Piscina'] // }, // page: 2, // limit: 20, // sort: { field: 'valor_venda', direction: 'desc' } // } ``` ### **4. SQL Builder (IntermediateFormat → SQL)** ```typescript import { createSQLBuilder } from '@horizon-apps/domain-schema-core'; const sqlBuilder = createSQLBuilder({ schema: createTestPropertySchema(), logging: true }); const genericRequest = { filters: adaptedState.filters || {}, search: adaptedState.search, include: ['list'], list: { page: adaptedState.page, limit: adaptedState.limit, sort: adaptedState.sort } }; const sqlResult = sqlBuilder.build(genericRequest); ``` ### **5. SQL Final Gerado** ```sql WITH base_filtered AS ( SELECT * FROM "property" WHERE "tipo" = ANY(ARRAY['Casa', 'Sobrado']) AND "endereco_cidade" = 'Curitiba' AND "valor_venda" >= 300000 AND "valor_venda" <= 800000 AND "caracteristicas" && ARRAY['Piscina'] AND fulltext_vector @@ to_tsquery('casa & moderna & 3 & quartos') ) SELECT * FROM base_filtered ORDER BY "valor_venda" DESC LIMIT 20 OFFSET 20; ``` ## 🔧 **Configuração Completa do Pipeline** ### **Setup do Sistema** ```typescript import { SearchUrlBuilder } from '@horizon-apps/domain-schema-core/search-state-to-url-parser'; import { createImoveisAdapter } from '@horizon-apps/domain-schema-core/url-to-search-adapter'; import { createSQLBuilder } from '@horizon-apps/domain-schema-core/postgre-search-sql-builder'; import { createTestPropertySchema } from '@horizon-apps/domain-schema-core/filter/schema-adapter'; // 1. Configurar URL Builder const urlBuilder = new SearchUrlBuilder({ rootUrl: '/imoveis/', enableSlug: true, defaults: { page: 1, limit: 20, sort: { updated_at: 'desc' } }, fieldConfig: { fieldMapping: { search: 'q' }, overrides: { search: { operator: 'search' }, caracteristicas: { operator: 'and' } } } }); // 2. Configurar Adapter const adapter = createImoveisAdapter(); // 3. Configurar SQL Builder const schema = createTestPropertySchema(); const sqlBuilder = createSQLBuilder({ schema, logging: false }); // 4. Sistema Completo Pronto! export const searchSystem = { urlBuilder, adapter, sqlBuilder }; ``` ### **Função Helper Completa** ```typescript export async function executeCompleteSearch( searchState: any, context?: any ): Promise<{ url: string; sql: string; results: any[]; }> { // 1. Build URL const urlResult = urlBuilder.buildUrl(searchState); // 2. Parse (simular round-trip) const mockContext = { query: Object.fromEntries(new URLSearchParams(urlResult.queryString)) }; const parsed = urlBuilder.parseUrl(mockContext); // 3. Adapt const adapted = adapter.adapt(parsed); // 4. Build SQL const genericRequest = { filters: adapted.filters || {}, search: adapted.search, include: ['list'], list: { page: adapted.page, limit: adapted.limit, sort: adapted.sort } }; const sqlResult = sqlBuilder.build(genericRequest); // 5. Execute (mock) // const results = await db.query(sqlResult.sqls.list); return { url: urlResult.url, sql: sqlResult.sqls.list, results: [] // Mock results }; } ``` ## 🎯 **Field Mapping - Mapeamento de Campos** ### **Configuração do Adapter** ```typescript const fieldMapping = { // Frontend → Backend 'cidade': 'endereco_cidade', 'bairro': 'endereco_bairro', 'estado': 'endereco_estado', 'cep': 'endereco_cep', // Mantém iguais 'tipo': 'tipo', 'valor_venda': 'valor_venda', 'caracteristicas': 'caracteristicas' }; // No adapter export function createImoveisAdapter(): SearchAdapter { return { adapt(searchParams: SearchParams): IntermediateSearchFormat { return { search: searchParams.search ? { value: searchParams.search, method: 'vector' as const } : undefined, filters: mapFields(searchParams.filters || {}, fieldMapping), page: searchParams.page || 1, limit: searchParams.limit || 20, sort: parseSort(searchParams.sort) }; } }; } ``` ### **Transformações Automáticas** | Frontend | URL | Adapter | SQL Builder | |----------|-----|---------|-------------| | `cidade: 'SP'` | `cidade=SP` | `endereco_cidade: 'SP'` | `"endereco_cidade" = 'SP'` | | `valor_min: 300k` | `valor_gte=300000` | `valor_venda: { gte: 300000 }` | `"valor_venda" >= 300000` | | `search: 'casa'` | `q=casa` | `search: { value: 'casa', method: 'vector' }` | `fulltext_vector @@ to_tsquery('casa')` | ## 🧪 **Testes de Integração** ### **Teste Completo do Pipeline** ```typescript import { describe, it, expect } from 'vitest'; describe('🔥 Pipeline Completo', () => { it('deve processar busca imobiliária completa', () => { const estadoInicial = { search: 'casa moderna piscina', // FORA de filters filters: { tipo: ['Casa', 'Sobrado'], cidade: 'Curitiba', valor_venda_min: 300000, // Será processado valor_venda_max: 800000 }, page: 1, limit: 20, sort: 'valor_venda:desc' }; // 1. URL Build const urlResult = urlBuilder.buildUrl(estadoInicial); expect(urlResult.url).toContain('q=casa+moderna+piscina'); expect(urlResult.url).toContain('tipo=Casa%2CSobrado'); // 2. URL Parse const mockContext = { query: Object.fromEntries(new URLSearchParams(urlResult.queryString)) }; const parsed = urlBuilder.parseUrl(mockContext); expect(parsed.search).toBe('casa moderna piscina'); // 3. Adapter const adapted = adapter.adapt(parsed); expect(adapted.search?.value).toBe('casa moderna piscina'); expect(adapted.search?.method).toBe('vector'); expect(adapted.filters?.endereco_cidade).toBe('Curitiba'); // Mapeado! // 4. SQL Builder const genericRequest = { filters: adapted.filters || {}, search: adapted.search, include: ['list'], list: { page: adapted.page, limit: adapted.limit, sort: adapted.sort } }; const sqlResult = sqlBuilder.build(genericRequest); expect(sqlResult.type).toBe('parallel'); expect(sqlResult.sqls.list).toContain('SELECT'); expect(sqlResult.sqls.list).toContain('fulltext_vector @@'); expect(sqlResult.sqls.list).toContain('ORDER BY "valor_venda" DESC'); console.log('✅ Pipeline completo funcionando!'); }); }); ``` ### **Round-trip Validation** ```typescript export function validateRoundTrip(originalState: any): boolean { // State → URL → State const url1 = urlBuilder.buildUrl(originalState); const parsed1 = urlBuilder.parseUrl({ query: Object.fromEntries(new URLSearchParams(url1.queryString)) }); // State → URL → State (novamente) const url2 = urlBuilder.buildUrl(parsed1); const parsed2 = urlBuilder.parseUrl({ query: Object.fromEntries(new URLSearchParams(url2.queryString)) }); // URLs devem ser idênticas return url1.url === url2.url && JSON.stringify(parsed1) === JSON.stringify(parsed2); } ``` ## 🔥 **Casos de Uso Reais** ### **1. Busca Simples** ```typescript const simpleSearch = { search: 'apartamento 2 quartos', filters: { cidade: 'São Paulo' } }; // Pipeline: State → URL → SQL // URL: /imoveis?q=apartamento+2+quartos&cidade=São+Paulo // SQL: SELECT * FROM property WHERE endereco_cidade='São Paulo' AND fulltext_vector @@ to_tsquery('apartamento & 2 & quartos') ``` ### **2. Filtros Complexos** ```typescript const complexSearch = { filters: { tipo: ['Casa', 'Sobrado'], valor_venda_min: 500000, valor_venda_max: 1000000, caracteristicas: ['Piscina', 'Churrasqueira'], location: { operation: 'within', geometry: { type: 'circle', center: { lat: -25.4372, lng: -49.2697 }, radius: 5000 } } }, sort: 'location[proximity]:asc' }; // Pipeline gera SQL com ST_DWithin para geolocalização e ordenação por proximidade ``` ### **3. Facetas para Interface** ```typescript const facetSearch = { filters: { cidade: 'Rio de Janeiro' }, include: ['list', 'facets'], facets: { fields: ['tipo', 'bairro', 'caracteristicas'], ranges: { valor_venda: { min: 0, max: 5000000, buckets: 10 } } } }; // Pipeline gera 2 SQLs: um para lista, outro para agregações ``` ## ⚡ **Performance e Otimizações** ### **Execução Paralela** ```typescript async function executeParallelQueries(genericRequest: GenericRequest) { const sqlResult = sqlBuilder.build(genericRequest); // Executar SQLs em paralelo const [listResults, facetsResults] = await Promise.all([ db.query(sqlResult.sqls.list), db.query(sqlResult.sqls.facets) ]); return { list: listResults, facets: facetsResults }; } ``` ### **Cache de URLs** ```typescript const urlCache = new Map<string, any>(); function getCachedResults(searchState: any) { const urlKey = urlBuilder.buildUrl(searchState).url; if (urlCache.has(urlKey)) { return urlCache.get(urlKey); } // Process pipeline... const results = executeCompleteSearch(searchState); urlCache.set(urlKey, results); return results; } ``` ## 🚨 **Troubleshooting** ### **Problemas Comuns** ```typescript // ❌ ERRO: Campo não mapeado filters: { cidade_inexistente: 'SP' } // ✅ SOLUÇÃO: Verificar fieldMapping no adapter // ❌ ERRO: Search vazio gera SQL inválido search: '' // ✅ SOLUÇÃO: Adapter ignora search vazio automaticamente // ❌ ERRO: Sort inválido sort: 'campo_inexistente:asc' // ✅ SOLUÇÃO: SQL Builder ignora sorts inválidos // ❌ ERRO: URL muito longa filters: { array_muito_grande: [...1000_items] } // ✅ SOLUÇÃO: Usar base64 automático para objetos complexos ``` ### **Debug do Pipeline** ```typescript export function debugPipeline(searchState: any) { console.log('🏁 ENTRADA:', searchState); const url = urlBuilder.buildUrl(searchState); console.log('🌐 URL:', url.url); const parsed = urlBuilder.parseUrl({ query: Object.fromEntries(new URLSearchParams(url.queryString)) }); console.log('📥 PARSED:', parsed); const adapted = adapter.adapt(parsed); console.log('🔄 ADAPTED:', adapted); const genericRequest = { filters: adapted.filters || {}, search: adapted.search, include: ['list'] }; const sql = sqlBuilder.build(genericRequest); console.log('💾 SQL:', sql.sqls.list); } ``` --- **🎉 Pipeline completo URL ↔ SQL funcionando perfeitamente em produção!** Versão 1.2.8 com 56 testes passando (25 URL Parser + 31 SQL Builder) ✅