UNPKG

@horizon-apps/domain-schema-core

Version:

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

1,459 lines (1,447 loc) 112 kB
import { format } from 'date-fns'; import ptBR from 'date-fns/locale/pt-BR/index.js'; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/search-state-to-url-parser/seo-slug-generator/index.ts var seo_slug_generator_exports = {}; __export(seo_slug_generator_exports, { analyzeSeoFields: () => analyzeSeoFields, generateSeoSlug: () => generateSeoSlug, generateSeoSlugWithOptions: () => generateSeoSlugWithOptions, generateSeoUrl: () => generateSeoUrl, testSlugGeneration: () => testSlugGeneration, validateSeoSlug: () => validateSeoSlug }); function slugify(text) { return text.toString().toLowerCase().trim().replace(/[àáâãäå]/g, "a").replace(/[èéêë]/g, "e").replace(/[ìíîï]/g, "i").replace(/[òóôõö]/g, "o").replace(/[ùúûü]/g, "u").replace(/[ç]/g, "c").replace(/[ñ]/g, "n").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, ""); } function extractFieldValue(filters, fieldNames) { for (const fieldName of fieldNames) { if (filters[fieldName]) { const value = filters[fieldName]; if (typeof value === "string") { return value; } if (value.in && Array.isArray(value.in) && value.in.length > 0) { return value.in[0]; } if (value.equals && typeof value.equals === "string") { return value.equals; } } if (fieldName.includes(".")) { const parts = fieldName.split("."); let current = filters; for (const part of parts) { if (current && typeof current === "object" && current[part]) { current = current[part]; } else { current = null; break; } } if (current && typeof current === "string") { return current; } } } return null; } function generateSeoSlug(filters) { const slugParts = []; const extractionOrder = [ "tipo", "subtipo", "operacao", "bairro", "cidade", "estado" ]; for (const fieldType of extractionOrder) { const fieldNames = SEO_FIELDS[fieldType]; const value = extractFieldValue(filters, fieldNames); if (value) { const slugified = slugify(value); if (slugified && slugified.length > 0) { slugParts.push(slugified); } } } if (slugParts.length >= 2) { return slugParts.join("-"); } return null; } function generateSeoSlugWithOptions(filters, options) { const baseSlug = generateSeoSlug(filters); if (!baseSlug) { return null; } let finalSlug = baseSlug; if (options?.slugPrefix) { const prefix = slugify(options.slugPrefix); finalSlug = `${prefix}-${finalSlug}`; } const maxLength = options?.maxSlugLength || 100; if (finalSlug.length > maxLength) { const truncated = finalSlug.substring(0, maxLength); const lastHyphen = truncated.lastIndexOf("-"); finalSlug = lastHyphen > 0 ? truncated.substring(0, lastHyphen) : truncated; } return finalSlug; } function generateSeoUrl(filters, queryString = "", baseUrl = "/imoveis", options) { const slug = generateSeoSlugWithOptions(filters, options); if (slug) { const url = `${baseUrl}/${slug}`; return queryString ? `${url}?${queryString}` : url; } else { const url = baseUrl; return queryString ? `${url}?${queryString}` : url; } } function testSlugGeneration(filters) { const extractedFields = {}; for (const [fieldType, fieldNames] of Object.entries(SEO_FIELDS)) { extractedFields[fieldType] = extractFieldValue(filters, fieldNames); } return { filters, basicSlug: generateSeoSlug(filters), withPrefix: generateSeoSlugWithOptions(filters, { slugPrefix: "imoveis" }), withOptions: generateSeoSlugWithOptions(filters, { slugPrefix: "imoveis", maxSlugLength: 50 }), extractedFields }; } function validateSeoSlug(slug) { const issues = []; const suggestions = []; if (slug.length < 10) { issues.push("Slug muito curto (< 10 caracteres)"); suggestions.push("Adicione mais informa\xE7\xF5es relevantes"); } if (slug.length > 100) { issues.push("Slug muito longo (> 100 caracteres)"); suggestions.push("Use maxSlugLength para limitar o tamanho"); } if (!/^[a-z0-9-]+$/.test(slug)) { issues.push("Cont\xE9m caracteres inv\xE1lidos"); suggestions.push("Use apenas letras min\xFAsculas, n\xFAmeros e h\xEDfens"); } if (slug.includes("--") || slug.startsWith("-") || slug.endsWith("-")) { issues.push("H\xEDfens mal formatados"); suggestions.push("Evite h\xEDfens consecutivos ou nas bordas"); } const parts = slug.split("-"); if (parts.length < 2) { issues.push("Poucas informa\xE7\xF5es no slug"); suggestions.push("Inclua pelo menos tipo e localiza\xE7\xE3o"); } return { isValid: issues.length === 0, issues, suggestions }; } function analyzeSeoFields(filters) { const availableFields = {}; const missingFields = []; const recommendations = []; for (const [fieldType, fieldNames] of Object.entries(SEO_FIELDS)) { const value = extractFieldValue(filters, fieldNames); availableFields[fieldType] = value; if (!value) { missingFields.push(fieldType); } } const availableCount = Object.values(availableFields).filter((v) => v !== null).length; let slugPotential; if (availableCount >= 4) { slugPotential = "high"; } else if (availableCount >= 2) { slugPotential = "medium"; recommendations.push("Adicione mais campos de localiza\xE7\xE3o para melhor SEO"); } else if (availableCount === 1) { slugPotential = "low"; recommendations.push("Adicione pelo menos mais um campo relevante"); } else { slugPotential = "none"; recommendations.push("Adicione campos como tipo, cidade ou opera\xE7\xE3o"); } if (!availableFields.tipo) { recommendations.push("Adicione o tipo do im\xF3vel (casa, apartamento, etc.)"); } if (!availableFields.cidade) { recommendations.push("Adicione a cidade para melhor SEO local"); } if (!availableFields.operacao) { recommendations.push("Especifique a opera\xE7\xE3o (venda, aluguel)"); } return { availableFields, missingFields, slugPotential, recommendations }; } var SEO_FIELDS; var init_seo_slug_generator = __esm({ "src/search-state-to-url-parser/seo-slug-generator/index.ts"() { SEO_FIELDS = { tipo: ["tipo", "type"], subtipo: ["subtipo", "subtype", "tipo_imovel"], operacao: ["operacao", "operation", "finalidade"], cidade: ["cidade", "city", "endereco_cidade", "endereco.cidade"], bairro: ["bairro", "neighborhood", "endereco_bairro", "endereco.bairro"], estado: ["estado", "state", "endereco_estado", "endereco.estado"] }; } }); function formatCurrency(value, currency = "BRL", locale = "pt-BR") { if (value === null || value === void 0) { return "Valor sob consulta"; } const numValue = Number(value); if (isNaN(numValue)) return String(value); return numValue.toLocaleString(locale, { style: "currency", currency, minimumFractionDigits: 2 }); } function formatDateTime(value, locale = "pt-BR") { try { const date = value instanceof Date ? value : new Date(value); if (isNaN(date.getTime())) return String(value); if (locale === "pt-BR") { return format(date, "d 'de' MMMM 'de' yyyy", { locale: ptBR }); } return date.toLocaleDateString(locale); } catch { return String(value); } } function formatArea(value, unit = "m\xB2") { if (value === null || value === void 0) return ""; const unitMap = { "m2": "m\xB2", "m\xB2": "m\xB2", "ft2": "ft\xB2", "hectare": "hectares", "hectares": "hectares", "km2": "km\xB2" }; const displayUnit = unitMap[unit] || unit; return `${value} ${displayUnit}`; } function formatDistance(value, unit = "m") { if (value === null || value === void 0) return ""; const unitMap = { "m": "m", "meters": "m", "km": "km", "mi": "mi", "miles": "mi" }; const displayUnit = unitMap[unit] || unit; return `${value}${displayUnit}`; } function formatPercent(value) { if (value === null || value === void 0) return ""; const numValue = Number(value); if (isNaN(numValue)) return String(value); if (numValue > 1) { return `${numValue}%`; } return `${(numValue * 100).toFixed(2)}%`; } function formatCount(value) { if (value === null || value === void 0) return "0"; return String(Math.floor(Number(value))); } function formatYear(value) { if (value === null || value === void 0) return ""; return String(value); } function processTemplate(template, value, valueLabel) { if (!template) return void 0; let result = template; result = result.replace(/{{value}}/g, String(value)); result = result.replace(/{{valueLabel}}/g, valueLabel); result = result.replace(/{{p:(.*?)}}/g, (_, plural) => { const numValue = Number(value); return numValue !== 1 ? plural : ""; }); result = result.replace(/{{s:(.*?)}}/g, (_, singular) => { const numValue = Number(value); return numValue === 1 ? singular : ""; }); return result; } function generateValueLabel(field, options = {}) { const { value, format: format2, unit, type, enum: enumValues } = field; const { locale = "pt-BR", currency = "BRL" } = options; if (value === null || value === void 0) { if (format2 === "currency") return "Valor sob consulta"; return void 0; } if (field.valueLabel) { return field.valueLabel; } switch (format2) { case "currency": return formatCurrency(value, unit || currency, locale); case "date": case "datetime": return formatDateTime(value, locale); case "area": return formatArea(value, unit); case "distance": return formatDistance(value, unit); case "percent": return formatPercent(value); case "count": return formatCount(value); case "year": return formatYear(value); } if (type === "Boolean") { return value ? "Sim" : "N\xE3o"; } if (type === "String[]" && Array.isArray(value)) { if (!enumValues || typeof enumValues !== "object") { return void 0; } let values = value.map((v) => enumValues[v] || v); if (values.length === 0) return ""; if (values.length === 1) return values[0]; if (values.length === 2) return `${values[0]} e ${values[1]}`; const lastItem = values[values.length - 1]; const otherItems = values.slice(0, -1).join(", "); return `${otherItems} e ${lastItem}`; } if (type === "String" && enumValues && typeof enumValues === "object") { return enumValues[value] || value; } if (type === "Json") { return void 0; } if (type === "Array" || type === "Json[]") { if (Array.isArray(value) && value.every((item) => typeof item === "string")) { return value.join(", "); } return void 0; } return void 0; } function enrichField(fieldValue, metadata, options = {}) { const { composedLabel: originalComposedLabel, iconName: originalIconName, ...baseMetadata } = metadata; const result = {}; if (baseMetadata.key) result.key = baseMetadata.key; if (baseMetadata.label) result.label = baseMetadata.label; if (baseMetadata.description) result.description = baseMetadata.description; if (baseMetadata.enum) result.enum = baseMetadata.enum; if (baseMetadata.type) result.type = baseMetadata.type; if (baseMetadata.format) result.format = baseMetadata.format; if (baseMetadata.unit) result.unit = baseMetadata.unit; if (baseMetadata.validation) result.validation = baseMetadata.validation; if (baseMetadata.db) result.db = baseMetadata.db; if (baseMetadata.searchable !== void 0) result.searchable = baseMetadata.searchable; if (baseMetadata.filterable !== void 0) result.filterable = baseMetadata.filterable; if (baseMetadata.sortable !== void 0) result.sortable = baseMetadata.sortable; if (baseMetadata.parent) result.parent = baseMetadata.parent; if (baseMetadata.conditions) result.conditions = baseMetadata.conditions; result.value = fieldValue; const valueLabel = generateValueLabel({ value: fieldValue, ...metadata }, options); if (valueLabel) { result.valueLabel = valueLabel; } if (originalComposedLabel) { const processed = processTemplate(originalComposedLabel, fieldValue, valueLabel || String(fieldValue)); if (processed) { result.composedLabel = processed; } } if (originalIconName) result.iconName = originalIconName; if (originalIconName && options.getIcon) { const icon = options.getIcon(originalIconName); if (icon) result.icon = icon; } if (baseMetadata.mask) result.mask = baseMetadata.mask; if (baseMetadata.placeholder) result.placeholder = baseMetadata.placeholder; if (baseMetadata.categories) result.categories = baseMetadata.categories; if (baseMetadata.origin) result.origin = baseMetadata.origin; if (baseMetadata.modifiedBy) result.modifiedBy = baseMetadata.modifiedBy; return result; } function domainDataDisplayEnricher(options) { const { data, metadata, ...enrichOptions } = options; const enrichedData = { ...data }; metadata.forEach((fieldMetadata) => { const fieldKey = fieldMetadata.key; if (fieldKey in enrichedData) { const fieldValue = enrichedData[fieldKey]; if (fieldValue === void 0) { return; } enrichedData[fieldKey] = enrichField(fieldValue, fieldMetadata, enrichOptions); } }); return enrichedData; } var formatters = { currency: formatCurrency, date: formatDateTime, area: formatArea, distance: formatDistance, percent: formatPercent, count: formatCount, year: formatYear }; var templateProcessor = processTemplate; function EnrichFieldsWithMetadata({ data, metadata }) { return domainDataDisplayEnricher({ data, metadata }); } // src/json-schema-to-zod-generator/index.ts var JsonToZodGenerator = class { /** * Aplica inferência inteligente em um campo */ static applyInference(field, options) { const inferredField = { ...field }; if (options.logInference) { this.inferenceLog = []; } if (field.validation?.precision && options.enablePrecisionValidation) { const multipleOf = Math.pow(10, -field.validation.precision); this.log(`\u{1F522} ${field.key}: precision ${field.validation.precision} \u2192 multipleOf ${multipleOf}`); } if (field.mask) { this.log(`\u{1F3AD} ${field.key}: mask "${field.mask}" \u2192 FRONTEND ONLY (backend receives clean data)`); } if (field.type === "Json") { this.log(`\u{1F4E6} ${field.key}: Json type \u2192 z.any() or z.record()`); } if (field.type === "String[]" && field.searchable) { this.log(`\u{1F50D} ${field.key}: String[] + searchable \u2192 GIN index (DB only)`); } if (field.type === "String" && field.searchable && !field.validation?.maxLength) { this.log(`\u{1F4DD} ${field.key}: String + searchable + no maxLength \u2192 fulltext index (DB only)`); } return inferredField; } static log(message) { this.inferenceLog.push(message); } /** * Converte um campo para Zod */ static fieldToZod(field, options) { const processedField = this.applyInference(field, options); const { key, type, label, validation, enum: enumValues } = processedField; let zodType = ""; let validations = []; switch (type) { case "String": if (enumValues && Object.keys(enumValues).length > 0) { const enumArray = Object.keys(enumValues).map((k) => `"${k}"`).join(", "); zodType = `z.enum([${enumArray}])`; } else { zodType = "z.string()"; } if (validation?.minLength) validations.push(`.min(${validation.minLength})`); if (validation?.maxLength) validations.push(`.max(${validation.maxLength})`); break; case "Number": zodType = "z.number()"; if (validation?.min !== void 0) validations.push(`.min(${validation.min})`); if (validation?.max !== void 0) validations.push(`.max(${validation.max})`); if (validation?.precision && options.enablePrecisionValidation) { const multipleOf = Math.pow(10, -validation.precision); validations.push(`.multipleOf(${multipleOf})`); } break; case "Boolean": zodType = "z.boolean()"; break; case "String[]": if (enumValues && Object.keys(enumValues).length > 0) { const enumArray = Object.keys(enumValues).map((k) => `"${k}"`).join(", "); zodType = `z.array(z.enum([${enumArray}]))`; } else { zodType = "z.array(z.string())"; } break; case "Array": zodType = "z.array(z.any())"; break; case "Json": zodType = "z.any()"; break; case "Json[]": zodType = "z.array(z.any())"; break; default: zodType = "z.any()"; } zodType += validations.join(""); if (options.addDescriptions && label) { zodType += `.describe("${label}")`; } if (!validation?.required) { zodType += ".optional()"; } return ` ${key}: ${zodType},`; } /** * Gera o schema Zod completo */ static generate(fields, options) { const { schemaName, sortFields: sortFields2 = false, exportType = true, logInference = false } = options; if (logInference) { console.log(` \u{1F9E0} INICIANDO GERA\xC7\xC3O COM INFER\xCANCIA - ${schemaName}`); console.log(`\u{1F4CA} Total de campos: ${fields.length}`); } const processedFields = sortFields2 ? [...fields].sort((a, b) => a.key.localeCompare(b.key)) : fields; const imports = `import { z } from "zod" `; const schemaFields = processedFields.map((field) => this.fieldToZod(field, options)).join("\n"); const schema = ` // Schema Horizon v2.2.0 para ${schemaName} // Gerado automaticamente com infer\xEAncia inteligente export const ${schemaName}Zod = z.object({ ${schemaFields} }) `; const typeExport = exportType ? ` // Tipo inferido a partir do schema export type ${schemaName}Type = z.infer<typeof ${schemaName}Zod> // Fun\xE7\xE3o helper para valida\xE7\xE3o export const validate${schemaName} = (data: unknown): ${schemaName}Type => { return ${schemaName}Zod.parse(data) } // Fun\xE7\xE3o helper para valida\xE7\xE3o safe export const safeValidate${schemaName} = (data: unknown) => { return ${schemaName}Zod.safeParse(data) } // Fun\xE7\xE3o helper para valida\xE7\xE3o parcial export const validatePartial${schemaName} = (data: unknown) => { return ${schemaName}Zod.partial().parse(data) } ` : ""; if (logInference && this.inferenceLog.length > 0) { console.log(` \u{1F9E0} INFER\xCANCIAS APLICADAS:`); this.inferenceLog.forEach((log) => console.log(log)); console.log(` \u2705 Gera\xE7\xE3o conclu\xEDda com ${this.inferenceLog.length} infer\xEAncias`); } return imports + schema + typeExport; } /** * Gera schema a partir de arquivo JSON v2.2.0 */ static async generateFromFile(jsonPath, options) { const fs = await import('fs/promises'); const jsonContent = await fs.readFile(jsonPath, "utf-8"); const data = JSON.parse(jsonContent); const fields = data.fields || data; if (!Array.isArray(fields)) { throw new Error('JSON deve conter um array de campos ou objeto com propriedade "fields"'); } return this.generate(fields, options); } /** * Salva o schema gerado */ static async saveToFile(fields, options, outputPath) { const fs = await import('fs/promises'); const path = await import('path'); const generated = this.generate(fields, options); const dir = path.dirname(outputPath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(outputPath, generated, "utf-8"); if (options.logInference) { console.log(` \u{1F4BE} Schema salvo: ${outputPath}`); } } /** * Analisa um schema e retorna estatísticas */ static analyzeSchema(fields) { const analysis = { totalFields: fields.length, fieldTypes: {}, fieldFormats: {}, fieldCategories: {}, requiredFields: 0, fieldsWithValidation: 0, fieldsWithMask: 0, fieldsWithEnum: 0, fieldsWithConditions: 0, fieldsWithParent: 0 }; fields.forEach((field) => { analysis.fieldTypes[field.type] = (analysis.fieldTypes[field.type] || 0) + 1; if (field.format) { analysis.fieldFormats[field.format] = (analysis.fieldFormats[field.format] || 0) + 1; } if (field.categories) { field.categories.forEach((cat) => { analysis.fieldCategories[cat] = (analysis.fieldCategories[cat] || 0) + 1; }); } if (field.validation?.required) analysis.requiredFields++; if (field.validation) analysis.fieldsWithValidation++; if (field.mask) analysis.fieldsWithMask++; if (field.enum) analysis.fieldsWithEnum++; if (field.conditions) analysis.fieldsWithConditions++; if (field.parent) analysis.fieldsWithParent++; }); return analysis; } }; JsonToZodGenerator.inferenceLog = []; // src/domain-data-filters/index.ts function getFieldsByCategory(fields, category, asObject = false, preserveOrder = false) { const matchesCategory = (fieldCategory) => { if (!fieldCategory) return false; const fieldCats = Array.isArray(fieldCategory) ? fieldCategory : [fieldCategory]; const searchCats = Array.isArray(category) ? category : [category]; return fieldCats.some((cat) => searchCats.includes(cat)); }; const getFieldCategory = (field) => { return field?.categories || field?.category; }; let matchingFields; if (preserveOrder && Array.isArray(category)) { const allFields = Array.isArray(fields) ? fields : Object.values(fields); matchingFields = []; for (const cat of category) { const categoryFields = allFields.filter((field) => { const fieldCat = getFieldCategory(field); if (!fieldCat) return false; if (Array.isArray(fieldCat)) { return fieldCat.includes(cat); } return fieldCat === cat; }); categoryFields.forEach((field) => { if (!matchingFields.some((f) => f.key === field.key)) { matchingFields.push(field); } }); } } else { if (Array.isArray(fields)) { matchingFields = fields.filter( (field) => matchesCategory(getFieldCategory(field)) ); } else { matchingFields = Object.values(fields).filter( (field) => matchesCategory(getFieldCategory(field)) ); } } if (asObject) { const result = {}; matchingFields.forEach((field) => { result[field.key] = field; }); return result; } return matchingFields; } function getFieldsByKeys(fields, keys, asObject = false) { if (!Array.isArray(keys) || keys.length === 0) { return asObject ? {} : []; } const hasValidValue = (field) => { if (!field) return false; const value = field.value; if (value === null || value === void 0 || value === "" || Number.isNaN(value)) { return false; } if (Array.isArray(value) && value.length === 0) { return false; } return true; }; let fieldsMap; if (Array.isArray(fields)) { fieldsMap = new Map(fields.map((field) => [field.key, field])); } else { fieldsMap = new Map(Object.entries(fields)); } const matchingFields = keys.map((key) => fieldsMap.get(key)).filter((field) => hasValidValue(field)); if (asObject) { const result = {}; matchingFields.forEach((field) => { result[field.key] = field; }); return result; } return matchingFields; } // src/domain-data-transformers/index.ts function expandArrayFieldToBoolean(field) { if (Array.isArray(field)) { return field.map((item) => ({ key: String(item), value: true, type: "Boolean", label: String(item) })); } if (field && field.value && Array.isArray(field.value)) { return field.value.map((item) => ({ key: String(item), value: true, type: "Boolean", label: String(item) })); } return []; } function sortFields(fields, sortKeys = []) { return [...fields].sort((a, b) => { for (const rule of sortKeys) { const [field, direction] = rule.split(":"); const valA = a[field]; const valB = b[field]; if (valA === void 0 || valB === void 0) continue; const isAsc = direction === "asc"; const isDesc = direction === "desc"; if (!isAsc && !isDesc) { console.warn(`Dire\xE7\xE3o inv\xE1lida: "${direction}". Use "asc" ou "desc".`); continue; } if (valA < valB) return isAsc ? -1 : 1; if (valA > valB) return isAsc ? 1 : -1; } return 0; }); } // src/search-state-to-url-parser/search-to-url-query-serializer/index.ts var SearchUrlBuilder = class { constructor(config) { this.config = { enableSlug: false, fieldConfig: { fieldMapping: { search_text: "q" // Mapeamento padrão }, overrides: {} }, ...config }; if (!this.config.rootUrl.endsWith("/")) { this.config.rootUrl += "/"; } this.fieldMapping = this.config.fieldConfig?.fieldMapping || { search_text: "q" }; this.defaults = { page: 1, limit: 20, sort: { updated_at: "desc" }, // ✅ OBJETO por padrão ...this.config.defaults }; } // ========================================== // 🔗 SERIALIZAÇÃO - Estado → URL // ========================================== /** * Constrói URL de busca com query params visíveis * * @example * ```typescript * const builder = new SearchUrlBuilder({ rootUrl: '/imoveis/' }); * const result = builder.buildUrl({ * filters: { * search_text: { search: "casa moderna" }, * tipo: "Casa", * cidade: "Ponta Grossa" * }, * page: 2 * }); * * // result.url: "/imoveis/?q=casa+moderna&tipo=Casa&cidade=Ponta+Grossa&page=2" * ``` */ buildUrl(params) { let slug = null; if (this.config.enableSlug && params.filters) { slug = this.generateSlugFromFilters(params.filters); } const queryParts = []; if (params.search && params.search.trim()) { const mappedField = this.fieldMapping.search_text || "q"; const encodedValue = encodeURIComponent(params.search.trim()).replace(/%20/g, "+"); queryParts.push(`${mappedField}=${encodedValue}`); } if (params.filters) { Object.entries(params.filters).forEach(([key, value]) => { const serializedValue = this.serializeComplexValue(value); const encodedValue = encodeURIComponent(serializedValue).replace(/%20/g, "+"); queryParts.push(`${key}=${encodedValue}`); }); } if (params.page && params.page !== this.defaults.page) { queryParts.push(`page=${params.page}`); } if (params.limit && params.limit !== this.defaults.limit) { queryParts.push(`limit=${params.limit}`); } if (params.sort) { let sortString; if (typeof params.sort === "string") { sortString = params.sort.trim(); } else if (typeof params.sort === "object" && params.sort !== null) { const entries = Object.entries(params.sort); if (entries.length === 1) { const [field, direction] = entries[0]; sortString = `${field}:${direction}`; } else { const [field, direction] = entries[0]; sortString = `${field}:${direction}`; console.warn("Sort com m\xFAltiplos campos, usando apenas o primeiro:", { field, direction }); } } else { sortString = String(params.sort); } if (sortString && sortString.trim()) { const defaultSortString = this.sortObjectToString(this.defaults.sort); if (sortString.trim() !== defaultSortString) { queryParts.push(`sort=${sortString.trim()}`); } } } const queryString = queryParts.join("&"); let url = this.config.rootUrl.replace(/\/$/, ""); if (slug) { url += `/${slug}`; } if (queryString) { url += `?${queryString}`; } return { url, slug, queryString, hasSlug: !!slug }; } // ========================================== // 🔗 DESERIALIZAÇÃO - URL → Estado // ========================================== /** * Extrai parâmetros de busca da URL (todos visíveis) * * @example * ```typescript * const builder = new SearchUrlBuilder({ rootUrl: '/imoveis/' }); * const params = builder.parseUrl(context); * * // Retorna exatamente o que estava na URL: * // { * // filters: { search_text: { search: "casa" }, tipo: "Casa" }, * // page: 2, * // limit: 25, * // sort: 'valor_venda_desc' * // } * ``` */ parseUrl(context) { const { slug, ...query } = context.query; const result = {}; let searchQuery; const reverseMappingForSearchText = Object.entries(this.fieldMapping).find(([field]) => field === "search_text")?.[1] || "q"; searchQuery = query[reverseMappingForSearchText]; delete query[reverseMappingForSearchText]; const page = query.page ? parseInt(query.page, 10) : void 0; const limit = query.limit ? parseInt(query.limit, 10) : void 0; let sort = void 0; if (query.sort && typeof query.sort === "string") { const sortMatch = query.sort.match(/^(.+):(asc|desc)$/); if (sortMatch) { const [, field, direction] = sortMatch; sort = { [field]: direction }; } else { const legacyMatch = query.sort.match(/^(.+)_(asc|desc)$/); if (legacyMatch) { const [, field, direction] = legacyMatch; sort = { [field]: direction }; } else { sort = query.sort; } } } delete query.page; delete query.limit; delete query.sort; const filters = {}; Object.entries(query).forEach(([key, value]) => { filters[key] = this.parseComplexValue(value); }); if (Object.keys(filters).length > 0) { result.filters = filters; } if (searchQuery && searchQuery.trim()) { result.search = searchQuery.trim(); } result.page = page || this.defaults.page; result.limit = limit || this.defaults.limit; result.sort = sort || this.defaults.sort; return result; } // ========================================== // 🚀 ENRIQUECIMENTO PARA FRONTEND REQUEST // ========================================== /** * Enriquece estado Zustand para FrontendRequest (compatível com SearchRequestToPrismaMapper) * * @example * ```typescript * const builder = new SearchUrlBuilder({ rootUrl: '/imoveis/' }); * const enriched = builder.enrichForFrontendRequest(zustandState, { * fields: ["reference", "title"], * relations: { broker: ["name", "phone"] } * }); * * // enriched é compatível com SearchRequestToPrismaMapper * const prismaQuery = mapSearchRequestToPrisma(enriched); * ``` */ enrichForFrontendRequest(zustandState, options) { const { fields, relations } = options || {}; const { filters = {}, page, limit, sort } = zustandState; const enrichedFilters = this.enrichFiltersWithOperators(filters); return { filters: enrichedFilters, ...page && { page }, ...limit && { limit }, ...sort && { sort }, ...fields && { fields }, ...relations && { relations } }; } // ========================================== // 🔧 MÉTODOS AUXILIARES PRIVADOS // ========================================== // Métodos extractAndFlattenFilters e addParamToUrl removidos (não eram utilizados) /** * Parse valores da URL, tentando detectar o tipo correto */ parseUrlValue(value) { if (!value) return value; const stringValue = Array.isArray(value) ? value[0] : value; if (/^\d+$/.test(stringValue)) { return parseInt(stringValue, 10); } if (stringValue === "true") return true; if (stringValue === "false") return false; if (stringValue.startsWith("{") || stringValue.startsWith("[")) { try { return JSON.parse(stringValue); } catch { } } if (stringValue.includes(",") && !stringValue.startsWith("{") && !stringValue.startsWith("[")) { return stringValue.split(",").map((item) => item.trim()); } return stringValue; } /** * Converte sort object para string para comparações */ sortObjectToString(sort) { if (typeof sort === "string") { return sort; } if (typeof sort === "object" && sort !== null) { const entries = Object.entries(sort); if (entries.length === 1) { const [field, direction] = entries[0]; return `${field}:${direction}`; } } return ""; } // ========================================== // 🗜️ BASE64 PARA OBJETOS COMPLEXOS // ========================================== /** * Verifica se valor é um objeto complexo que precisa de base64 */ isComplexObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 1; } /** * Serializa valor complexo com base64 se necessário */ serializeComplexValue(value) { if (this.isComplexObject(value)) { return "base64:" + Buffer.from(JSON.stringify(value)).toString("base64"); } return String(value); } /** * Parse valor que pode ser base64 encoded */ parseComplexValue(value) { if (typeof value === "string" && value.startsWith("base64:")) { try { const decoded = Buffer.from(value.substring(7), "base64").toString(); return JSON.parse(decoded); } catch (e) { console.warn("Erro ao decodificar base64:", value); return value; } } return this.parseUrlValue(value); } /** * Gera slug SEO baseado nos filtros (integração com seo-slug-generator) */ generateSlugFromFilters(filters) { try { const { generateSeoSlugWithOptions: generateSeoSlugWithOptions2 } = (init_seo_slug_generator(), __toCommonJS(seo_slug_generator_exports)); return generateSeoSlugWithOptions2(filters, this.config.slugConfig); } catch (error) { console.warn("Erro ao gerar slug SEO:", error); return null; } } /** * Enriquece filtros do Zustand com operadores para API * v1.2.2: Usa lógica AND/OR ao invés de operadores Prisma específicos * Padrões hardcoded: strings = equals, arrays = or */ enrichFiltersWithOperators(filters) { console.log("\u{1F504} [enrichFiltersWithOperators] ENTRADA:", JSON.stringify(filters, null, 2)); const rangeProcessed = this.processRangeFields(filters); console.log("\u{1F4CA} [enrichFiltersWithOperators] Ap\xF3s processamento de ranges:", JSON.stringify(rangeProcessed, null, 2)); const enriched = {}; const fieldConfig = this.config.fieldConfig; const DEFAULT_STRING_OPERATOR = "equals"; const DEFAULT_ARRAY_OPERATOR = "or"; Object.entries(rangeProcessed).forEach(([key, value]) => { if (value === null || value === void 0) return; const override = fieldConfig.overrides?.[key]; const caseSensitive = override?.caseSensitive; if (key === "search_text") { if (typeof value === "string") { enriched[key] = { search: value }; console.log(`\u{1F50D} [enrichFiltersWithOperators] search_text: ${JSON.stringify(enriched[key])}`); } else if (typeof value === "object" && value.search) { enriched[key] = value; } } else if (Array.isArray(value)) { const operator = override?.operator || DEFAULT_ARRAY_OPERATOR; console.log(`\u{1F4CB} [enrichFiltersWithOperators] Array ${key}: operador=${operator}`); if (operator === "and") { enriched[key] = { and: value }; } else if (operator === "or") { enriched[key] = { or: value }; } else { enriched[key] = { [operator]: value }; } } else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) { const operator = override?.operator || DEFAULT_STRING_OPERATOR; console.log(`\u{1F4DD} [enrichFiltersWithOperators] ${typeof value} ${key}: operador=${operator}`); if (operator === "equals" || !operator) { enriched[key] = value; } else if (operator === "contains") { enriched[key] = caseSensitive ? { contains: value, caseSensitive: true } : { contains: value }; } else if (operator === "startsWith") { enriched[key] = caseSensitive ? { startsWith: value, caseSensitive: true } : { startsWith: value }; } else if (operator === "endsWith") { enriched[key] = caseSensitive ? { endsWith: value, caseSensitive: true } : { endsWith: value }; } else if (operator === "search") { enriched[key] = { search: value }; } else if (operator === "not") { enriched[key] = { not: value }; } else if (operator === "gt") { enriched[key] = { gt: value }; } else if (operator === "gte") { enriched[key] = { gte: value }; } else if (operator === "lt") { enriched[key] = { lt: value }; } else if (operator === "lte") { enriched[key] = { lte: value }; } else { enriched[key] = value; } } else if (typeof value === "object" && !Array.isArray(value)) { if (this.hasOperators(value)) { enriched[key] = value; console.log(`\u{1F527} [enrichFiltersWithOperators] Objeto com operadores mantido: ${key} = ${JSON.stringify(enriched[key])}`); } else { enriched[key] = value; console.log(`\u{1F4E6} [enrichFiltersWithOperators] Objeto simples mantido: ${key} = ${JSON.stringify(enriched[key])}`); } } else { enriched[key] = value; console.log(`\u{1F48E} [enrichFiltersWithOperators] Outro tipo mantido: ${key} = ${JSON.stringify(enriched[key])}`); } }); console.log("\u2705 [enrichFiltersWithOperators] SA\xCDDA:", JSON.stringify(enriched, null, 2)); return enriched; } /** * Verifica se um objeto já contém operadores * v1.2.2: Inclui operadores lógicos AND/OR */ hasOperators(obj) { const operators = [ // Operadores lógicos (v1.2.2) "and", "or", // Operadores de comparação "equals", "not", "gt", "gte", "lt", "lte", // Operadores de texto "contains", "startsWith", "endsWith", "search", // Operadores legacy (ainda suportados para compatibilidade) "in", "notIn", "has", "hasSome", "hasEvery" ]; return Object.keys(obj).some((key) => operators.includes(key)); } /** * Processa ranges automáticos no nível principal dos filtros * Identifica campos com _min/_max e os converte para operadores gte/lte */ processRangeFields(filters) { console.log("\u{1F4CA} [processRangeFields] ENTRADA:", JSON.stringify(filters, null, 2)); const processed = {}; const rangeFields = {}; Object.entries(filters).forEach(([key, value]) => { if (key.endsWith("_min")) { const baseField = key.replace("_min", ""); if (!rangeFields[baseField]) rangeFields[baseField] = {}; rangeFields[baseField].min = value; console.log(`\u{1F4C8} [processRangeFields] Range min encontrado: ${baseField}_min = ${value}`); } else if (key.endsWith("_max")) { const baseField = key.replace("_max", ""); if (!rangeFields[baseField]) rangeFields[baseField] = {}; rangeFields[baseField].max = value; console.log(`\u{1F4C9} [processRangeFields] Range max encontrado: ${baseField}_max = ${value}`); } else { processed[key] = value; } }); Object.entries(rangeFields).forEach(([baseField, range]) => { const rangeOperator = {}; if (range.min !== void 0 && range.min !== null) { rangeOperator.gte = range.min; console.log(`\u2B06\uFE0F [processRangeFields] Adicionando gte: ${baseField}.gte = ${range.min}`); } if (range.max !== void 0 && range.max !== null) { rangeOperator.lte = range.max; console.log(`\u2B07\uFE0F [processRangeFields] Adicionando lte: ${baseField}.lte = ${range.max}`); } if (Object.keys(rangeOperator).length > 0) { processed[baseField] = rangeOperator; console.log(`\u2705 [processRangeFields] Range convertido: ${baseField} = ${JSON.stringify(rangeOperator)}`); } }); console.log("\u2705 [processRangeFields] SA\xCDDA:", JSON.stringify(processed, null, 2)); return processed; } }; // src/postgre-search-sql-builder/filter/helpers.ts function toSnakeCase(str) { return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } function toCamelCase(str) { return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); } function getDbColumnName(fieldName, fieldInfo) { return typeof fieldInfo?.db === "string" ? fieldInfo.db : toSnakeCase(fieldName); } function isSimpleValue(value) { return typeof value === "string" || typeof value === "number" || typeof value === "boolean"; } function isArrayField(fieldType) { return fieldType?.endsWith("[]") || false; } function isFulltextFilter(value) { return value && typeof value === "object" && "search" in value; } function isGeospatialFilter(value) { if (!value || typeof value !== "object") return false; return "type" in value || // Structured format: { type: 'bbox', bounds: ... } "bounds" in value || // Legacy: { bounds: ... } "polygon" in value || // Legacy: { polygon: ... } "point" in value; } function isArrayFilter(value) { return value && typeof value === "object" && ("or" in value || "and" in value); } function hasRangeOperators(value) { return value && typeof value === "object" && (value.gte !== void 0 || value.gt !== void 0 || value.lte !== void 0 || value.lt !== void 0 || value.between !== void 0); } function hasExistsOperators(value) { return value && typeof value === "object" && "exists" in value; } function createSchemaFieldMap(schema) { const map = /* @__PURE__ */ new Map(); if (schema?.fields) { schema.fields.forEach((field) => { map.set(field.name, field); }); } return map; } function getFieldInfo(fieldName, schemaMap) { return schemaMap.get(fieldName); } function getFieldType(fieldName, schemaMap) { return getFieldInfo(fieldName, schemaMap)?.type || "string"; } function calculateComplexity(filterCount, hasFulltext, hasGeo, hasArrays, hasRanges) { let complexity = filterCount; if (hasFulltext) complexity += 2; if (hasGeo) complexity += 3; if (hasArrays) complexity += 1; if (hasRanges) complexity += 0.5; return Math.round(complexity * 10) / 10; } function analyzeFilters(filters) { const filterEntries = Object.entries(filters); const analysis = { hasFulltext: false, hasGeo: false, hasArrays: false, hasRanges: false, hasExists: false, filterCount: filterEntries.length, complexity: 0 }; filterEntries.forEach(([, value]) => { if (isFulltextFilter(value)) analysis.hasFulltext = true; if (isGeospatialFilter(value)) analysis.hasGeo = true; if (isArrayFilter(value)) analysis.hasArrays = true; if (hasRangeOperators(value)) analysis.hasRanges = true; if (hasExistsOperators(value)) analysis.hasExists = true; }); analysis.complexity = calculateComplexity( analysis.filterCount, analysis.hasFulltext, analysis.hasGeo, analysis.hasArrays, analysis.hasRanges ); return analysis; } function normalizeSearchText(searchText) { return searchText.trim().replace(/\s+/g, " ").replace(/[^\w\sáéíóúâêîôûàèìòùãõç-]/gi, "").toLowerCase(); } function validateGeoBounds(bounds) { if (!bounds || typeof bounds !== "object") return false; const { north, south, east, west } = bounds; return typeof north === "number" && typeof south === "number" && typeof east === "number" && typeof west === "number" && north > south && east > west; } function validateGeoFilter(geoFilter) { if (!geoFilter || typeof geoFilter !== "object") return false; if (geoFilter.bounds) { return validateGeoBounds(geoFilter.bounds); } if (geoFilter.polygon) { return typeof geoFilter.polygon === "string" && geoFilter.polygon.length > 0; } if (geoFilter.point) { const { lat, lng, radius } = geoFilter.point; return typeof lat === "number" && typeof lng === "number" && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 && (radius === void 0 || typeof radius === "number" && radius > 0); } return false; } function getFilterDebugInfo(key, value, schemaMap) { const fieldInfo = getFieldInfo(key, schemaMap); let filterType = "simple"; let complexity = 1; if (isFulltextFilter(value)) { filterType = "fulltext"; complexity = 2; } else if (isGeospatialFilter(value)) { filterType = "geospatial"; complexity = 3; } else if (isArrayFilter(value)) { filterType = "array"; complexity = 1.5; } else if (hasRangeOperators(value)) { filterType = "range"; complexity = 1.2; } else if (hasExistsOperators(value)) { filterType = "exists"; complexity = 0.5; } return { fieldName: key, dbColumn: getDbColumnName(key, fieldInfo), fieldType: getFieldType(key, schemaMap), filterType, complexity }; } function validateFiltersStructure(filters) { const errors = []; if (!filters) { errors.push("Filters object is required"); return { valid: false, errors }; } if (typeof filters !== "object") { errors.push("Filters must be an object"); return { valid: false, errors }; } Object.entries(filters).forEach(([key, value]) => { if (typeof key !== "string" || key.length === 0) { errors.push(`Invalid filter key: ${key}`); } if (isGeospatialFilter(value) && !validateGeoFilter(value)) { errors.push(`Invalid geospatial filter for ${key}`); } }); return { valid: errors.length === 0, errors }; } // src/postgre-search-sql-builder/filter/schema-adapter.ts function convertToHorizonFormat(ssotSchema) { return { entity: "property", // TODO: tornar dinâmico se necessário table: "property", // TODO: tornar dinâmico se necessário pk: "id", // TODO: tornar dinâmico se necessário fields: ssotSchema.fields.map((field) => convertFieldToHorizonFormat(field)) }; } function convertFieldToHorizonFormat(ssotField) { const horizonField = { name: ssotField.key, // key → name type: ssotField.type, // MANTER UPPERCASE: "String[]" não "string[]" searchable: ssotField.searchable || false, // Usar searchable se existe, não categories // DB mapping inteligente db: convertDbConfig(ssotField), // Inferir facetable baseado no tipo facetable: inferFacetable(ssotField), // Join é undefined para campos normais (só em casos específicos) join: void 0 }; horizonField._ssotMetadata = { label: ssotField.label, categories: ssotField.categories, format: ssotField.format, unit: ssotField.unit, validation: ssotField.validation, conditions: ssotField.conditions, parent: ssotField.parent, description: ssotField.description, origin: ssotField.origin }; return horizonField; } function convertDbConfig(ssotField) { if (!ssotField.db) { return ssotField.key; } if (typeof ssotField.db === "string") { return ssotField.db; } const dbConfig = { column: ssotField.key // ← SEMPRE usar key como coluna }; if (ssotField.db.fulltext_type) { dbConfig.fulltext_type = ssotField.db.fulltext_type; } return dbConfig; } function inferFacetable(ssotField) { if (ssotField.facetable !== void 0) { return ssotField.facetable; } const type = ssotField.type.toLowerCase(); if (type === "string" || type.startsWith("string")) { return { enabled: true, kind: "terms" }; } if (type === "number") { return { enabled: true, kind: "histogram" }; } if (type === "boolean") { return { enabled: true, kind: "terms" }; } if (type.includes("date")) { return { enabled: true, kind: "date_histogram" }; } return { enabled: true, kind: "terms" }; } // src/postgre-search-sql-builder/filter/processors/geospatial-processor.ts var GeospatialProcessor = class _GeospatialProcessor { constructor() { this.name = "geospatial"; } /** * Verifica se pode processar este filtro */ canProcess(_key, value, _fieldInfo) { if (!value || typeof value !== "object") { return false; } return this.isNewGeospatialFilter(value); } /** * Detecta nova estrutura geoespacial (operation + geometry) */ isNewGeospatialFilter(value) { return !!(value.operation && value.geometry && typeof value.geometry === "object" && this.isValidOperation(value.operation) && this.isVa