UNPKG

@horizon-apps/domain-schema-core

Version:

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

1,657 lines (1,632 loc) 60.3 kB
import { format } from 'date-fns'; import ptBR from 'date-fns/locale/pt-BR/index.js'; import { useState, useMemo, useEffect } from 'react'; // src/domain-data-display-enricher/field-enricher.ts 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); const formatted = numValue.toLocaleString(locale, { style: "currency", currency, minimumFractionDigits: 2 }); return formatted.replace(/\u00A0/g, " "); } 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; let processedValueLabel; if (Array.isArray(valueLabel)) { return void 0; } else { processedValueLabel = valueLabel || String(value); } result = result.replace(/{{value}}/g, String(value)); result = result.replace(/{{valueLabel}}/g, processedValueLabel); 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(value, metadata, options = {}) { const { format: format2, unit, type, enum: enumValues } = metadata; const { locale = "pt-BR", currency = "BRL" } = options; if (value === null || value === void 0) { if (format2 === "currency") return "Valor sob consulta"; return void 0; } 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 value.map((v) => enumValues[v] || v); } return value; } if (type === "String" && enumValues && typeof enumValues === "object") { return enumValues[value] || value; } if (type === "Json" || type === "Json[]") { return void 0; } return void 0; } function enrichField(rawValue, metadata, options = {}) { let processedValue = rawValue; const result = { // SEMPRE PRIMEIRO (conforme solicitado) key: metadata.key, // VALORES CORE value: processedValue, label: metadata.ui?.label || metadata.key || "Campo" }; const valueLabel = generateValueLabel(rawValue, metadata, options); if (valueLabel !== void 0) { result.valueLabel = valueLabel; } if (metadata.ui?.displayTemplate && !Array.isArray(valueLabel)) { const displayLabel = processTemplate( metadata.ui.displayTemplate, processedValue, valueLabel ); if (displayLabel) { result.displayLabel = displayLabel; } } if (metadata.ui?.iconName) { result.iconName = metadata.ui.iconName; if (options.getIcon) { result.icon = options.getIcon(metadata.ui.iconName); } } if (metadata.categories?.length) { result.categories = metadata.categories; } if (metadata.type) { result.type = metadata.type; } if (metadata.format) { result.format = metadata.format; } if (metadata.unit) { result.unit = metadata.unit; } if (options.includeMetadata) { result.metadata = { ...metadata }; } return result; } var formatters = { currency: formatCurrency, date: formatDateTime, area: formatArea, distance: formatDistance, percent: formatPercent, count: formatCount, year: formatYear }; var templateProcessor = processTemplate; // src/domain-data-display-enricher/domain-enricher-engine.ts var DomainDataDisplayEnricher = class { /** * Construtor do motor de enriquecimento * * @param registry - Registry com configurações de todos os domínios * @param options - Opções globais de enriquecimento */ constructor(registry, options = {}) { this.registry = registry; this.options = { locale: options.locale || "pt-BR", currency: options.currency || "BRL", includeMetadata: options.includeMetadata, getIcon: options.getIcon }; } /** * Enriquece dados de um domínio específico * * @param rawData - Dados brutos do banco/API * @param domainKey - Chave do domínio (ex: "property", "broker") * @returns Dados enriquecidos com estrutura flat */ enrich(rawData, domainKey) { const domainConfig = this.registry[domainKey]; if (!domainConfig) { throw new Error(`Domain "${domainKey}" not found in registry`); } const { data, schema, computedSchema, computedData } = domainConfig.enrichMapper(rawData); const indexedSchema = this.arrayToIndexedSchema(schema); const indexedComputedSchema = this.arrayToIndexedSchema(computedSchema); const enrichedResult = {}; this.processMainFields(data, indexedSchema, enrichedResult); if (computedData && Object.keys(computedData).length > 0) { enrichedResult.computed = this.processComputedFields(computedData, indexedComputedSchema); } return enrichedResult; } /** * Enriquece uma lista inteira de dados * * @param rawDataArray - Array de dados brutos * @param domainKey - Chave do domínio ("property", "broker", etc.) * @returns Array de dados enriquecidos */ enrichList(rawDataArray, domainKey) { if (!Array.isArray(rawDataArray)) { throw new Error("enrichList expects an array as first parameter"); } return rawDataArray.map((item) => this.enrich(item, domainKey)); } /** * Converte array de fields para formato indexado interno */ arrayToIndexedSchema(fields) { const indexed = {}; fields.forEach((field) => { indexed[field.key] = field; }); return indexed; } /** * Processa campos principais (incluindo relacionamentos) */ processMainFields(data, indexedSchema, result) { for (const [fieldKey, fieldValue] of Object.entries(data)) { if (fieldValue === void 0 || fieldValue === null) { continue; } const fieldMetadata = indexedSchema[fieldKey]; if (fieldMetadata?.type === "Relation") { if (this.registry[fieldKey]) { if (Array.isArray(fieldValue)) { result[fieldKey] = fieldValue.map( (item) => this.enrich(item, fieldKey) ); } else { result[fieldKey] = this.enrich(fieldValue, fieldKey); } } else { result[fieldKey] = fieldValue; } continue; } if (fieldMetadata) { result[fieldKey] = enrichField(fieldValue, fieldMetadata, this.options); } else { result[fieldKey] = { value: fieldValue }; } } } /** * Processa campos computados */ processComputedFields(computedData, indexedComputedSchema) { const enrichedComputed = {}; for (const [fieldKey, fieldValue] of Object.entries(computedData)) { if (fieldValue === void 0 || fieldValue === null) { continue; } const fieldMetadata = indexedComputedSchema[fieldKey]; if (fieldMetadata) { enrichedComputed[fieldKey] = enrichField(fieldValue, fieldMetadata, this.options); } else { enrichedComputed[fieldKey] = { key: fieldKey, value: fieldValue, label: fieldKey }; } } return enrichedComputed; } /** * Atualiza o registry em runtime * * @param domainKey - Chave do domínio * @param config - Nova configuração do domínio */ registerDomain(domainKey, config) { this.registry[domainKey] = config; } /** * Remove um domínio do registry * * @param domainKey - Chave do domínio a remover */ unregisterDomain(domainKey) { delete this.registry[domainKey]; } /** * Atualiza opções globais * * @param options - Novas opções */ updateOptions(options) { this.options = { ...this.options, ...options }; } /** * Obtém o registry atual */ getRegistry() { return this.registry; } /** * Obtém as opções atuais */ getOptions() { return this.options; } }; function useEnrichList(rawData, domainKey, registry, options) { const [enrichedData, setEnrichedData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const enricher = useMemo(() => { return new DomainDataDisplayEnricher(registry, options); }, [registry, options]); useEffect(() => { if (!rawData?.length) { setEnrichedData([]); setLoading(false); setError(null); return; } if (!registry[domainKey]) { const err = new Error(`Domain '${domainKey}' not found in registry`); setError(err); setLoading(false); return; } setLoading(true); setError(null); try { const enriched = enricher.enrichList(rawData, domainKey); setEnrichedData(enriched); } catch (err) { setError(err instanceof Error ? err : new Error(String(err))); } finally { setLoading(false); } }, [rawData, domainKey, enricher]); return { enrichedData, loading, error }; } function useEnrich(rawData, domainKey, registry, options) { const [enrichedData, setEnrichedData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const enricher = useMemo(() => { return new DomainDataDisplayEnricher(registry, options); }, [registry, options]); useEffect(() => { if (!rawData) { setEnrichedData(null); setLoading(false); setError(null); return; } if (!registry[domainKey]) { const err = new Error(`Domain '${domainKey}' not found in registry`); setError(err); setLoading(false); return; } setLoading(true); setError(null); try { const enriched = enricher.enrich(rawData, domainKey); setEnrichedData(enriched); } catch (err) { setError(err instanceof Error ? err : new Error(String(err))); } finally { setLoading(false); } }, [rawData, domainKey, enricher]); return { enrichedData, loading, error }; } function useEnricher(registry, options) { return useMemo(() => { return new DomainDataDisplayEnricher(registry, options); }, [registry, options]); } // 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/src/resolvers/base/BaseResolver.ts var BaseResolver = class { constructor(config) { this.config = config || { resolver: "base" }; } /** * Obtém o nome do campo para usar na URL (considera alias) */ getUrlKey(key) { return this.config.alias || key; } /** * Helper para encodar valores na URL */ encodeValue(value) { if (value === null || value === void 0) { return ""; } return String(value); } /** * Helper para criar param no formato key=value */ createParam(key, value) { return `${key}=${this.encodeValue(value)}`; } }; // src/search-state-to-url-parser/src/identifiers/RangeIdentifier.ts var RangeIdentifier = class { constructor() { this.name = "range"; } /** * Detecta se é um objeto range (tem gte ou lte) */ detect(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } return "gte" in value || "lte" in value || "gt" in value || "lt" in value; } /** * Serializa para formato [min]/[max] */ serialize(key, value) { const params = []; if ("gte" in value && value.gte !== void 0 && value.gte !== null) { params.push(`${key}[min]=${value.gte}`); } else if ("gt" in value && value.gt !== void 0 && value.gt !== null) { params.push(`${key}[min]=${value.gt}`); } if ("lte" in value && value.lte !== void 0 && value.lte !== null) { params.push(`${key}[max]=${value.lte}`); } else if ("lt" in value && value.lt !== void 0 && value.lt !== null) { params.push(`${key}[max]=${value.lt}`); } return params; } /** * Deserializa de volta para objeto com gte/lte */ deserialize(params, key) { const minKey = `${key}[min]`; const maxKey = `${key}[max]`; const minValue = params[minKey]; const maxValue = params[maxKey]; if (!minValue && !maxValue) { return void 0; } const result = {}; if (minValue) { const min = Array.isArray(minValue) ? minValue[0] : minValue; const numMin = Number(min); result.gte = isNaN(numMin) ? min : numMin; } if (maxValue) { const max = Array.isArray(maxValue) ? maxValue[0] : maxValue; const numMax = Number(max); result.lte = isNaN(numMax) ? max : numMax; } return result; } }; // src/search-state-to-url-parser/src/identifiers/BetweenIdentifier.ts var BetweenIdentifier = class { constructor() { this.name = "between"; } /** * Detecta se é um objeto com AMBOS gte E lte */ detect(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } return "gte" in value && "lte" in value || "gt" in value && "lt" in value || "min" in value && "max" in value; } /** * Serializa para formato [entre]=min,max */ serialize(key, value) { let min = null; let max = null; if ("gte" in value) min = value.gte; else if ("gt" in value) min = value.gt; else if ("min" in value) min = value.min; if ("lte" in value) max = value.lte; else if ("lt" in value) max = value.lt; else if ("max" in value) max = value.max; if (min === null || max === null) { return []; } return [`${key}[entre]=${min},${max}`]; } /** * Deserializa de volta para objeto com gte/lte */ deserialize(params, key) { const entreKey = `${key}[entre]`; const entreValue = params[entreKey]; if (!entreValue) { return void 0; } const valueString = Array.isArray(entreValue) ? entreValue[0] : entreValue; const [minStr, maxStr] = valueString.split(","); if (!minStr || !maxStr) { return void 0; } const min = Number(minStr); const max = Number(maxStr); return { gte: isNaN(min) ? minStr : min, lte: isNaN(max) ? maxStr : max }; } }; // src/search-state-to-url-parser/src/identifiers/MinMaxIdentifier.ts var MinMaxIdentifier = class { constructor() { this.name = "min-max"; } /** * Detecta se é um objeto com min ou max */ detect(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } return "min" in value || "max" in value; } /** * Serializa para formato [min]/[max] */ serialize(key, value) { const params = []; if ("min" in value && value.min !== void 0 && value.min !== null) { params.push(`${key}[min]=${value.min}`); } if ("max" in value && value.max !== void 0 && value.max !== null) { params.push(`${key}[max]=${value.max}`); } return params; } /** * Deserializa de volta para objeto com min/max */ deserialize(params, key) { const minKey = `${key}[min]`; const maxKey = `${key}[max]`; const minValue = params[minKey]; const maxValue = params[maxKey]; if (!minValue && !maxValue) { return void 0; } const result = {}; if (minValue) { const min = Array.isArray(minValue) ? minValue[0] : minValue; const numMin = Number(min); result.min = isNaN(numMin) ? min : numMin; } if (maxValue) { const max = Array.isArray(maxValue) ? maxValue[0] : maxValue; const numMax = Number(max); result.max = isNaN(numMax) ? max : numMax; } return result; } }; // src/search-state-to-url-parser/src/identifiers/index.ts var defaultIdentifiers = { "range": new RangeIdentifier(), "between": new BetweenIdentifier(), "min-max": new MinMaxIdentifier() }; function getIdentifier(name) { return defaultIdentifiers[name]; } // src/search-state-to-url-parser/src/resolvers/Base64Resolver.ts var Base64Resolver = class extends BaseResolver { /** * Serializa valor complexo para base64 */ serialize(key, value) { if (value === null || value === void 0) { return []; } const urlKey = this.getUrlKey(key); try { const jsonString = JSON.stringify(value); const base64 = this.toBase64(jsonString); return [`${urlKey}=base64:${base64}`]; } catch (error) { console.error(`Failed to serialize ${key} to base64:`, error); return []; } } /** * Deserializa base64 de volta para valor original */ deserialize(params, key) { const urlKey = this.getUrlKey(key); const value = params[urlKey]; if (!value) { return void 0; } const base64String = Array.isArray(value) ? value[0] : value; if (!base64String.startsWith("base64:")) { return void 0; } try { const base64 = base64String.substring(7); const jsonString = this.fromBase64(base64); return JSON.parse(jsonString); } catch (error) { console.error(`Failed to deserialize base64 for ${key}:`, error); return void 0; } } /** * Converte string para base64 (compatível com browser e Node.js) */ toBase64(str) { if (typeof Buffer !== "undefined") { return Buffer.from(str).toString("base64"); } else { return btoa(unescape(encodeURIComponent(str))); } } /** * Converte base64 para string (compatível com browser e Node.js) */ fromBase64(base64) { if (typeof Buffer !== "undefined") { return Buffer.from(base64, "base64").toString(); } else { return decodeURIComponent(escape(atob(base64))); } } }; // src/search-state-to-url-parser/src/resolvers/FieldsResolver.ts var FieldsResolver = class extends BaseResolver { constructor(config) { super(config); this.base64Resolver = new Base64Resolver(); } /** * Serializa campos internos de "fields" para a raiz da URL */ serialize(_key, value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return []; } const params = []; for (const [fieldKey, fieldValue] of Object.entries(value)) { if (this.isSimpleValue(fieldValue)) { if (fieldValue !== null && fieldValue !== void 0 && fieldValue !== "") { if (typeof fieldValue === "string") { params.push(`${fieldKey}=${encodeURIComponent(fieldValue)}`); } else { params.push(`${fieldKey}=${fieldValue}`); } } } else if (Array.isArray(fieldValue)) { if (fieldValue.length > 0) { const arrayString = fieldValue.filter((v) => v !== null && v !== void 0 && v !== "").map((v) => encodeURIComponent(String(v))).join(","); if (arrayString) { params.push(`${fieldKey}=${arrayString}`); } } } else if (typeof fieldValue === "object") { const identified = this.identifyAndSerialize(fieldKey, fieldValue); params.push(...identified); } } return params; } /** * Deserializa params da URL de volta para o objeto fields */ deserialize(params, _key) { const fields = {}; const processedKeys = /* @__PURE__ */ new Set(); for (const paramKey of Object.keys(params)) { if (processedKeys.has(paramKey)) continue; const bracketMatch = paramKey.match(/^(.+?)\[(.+?)\]$/); if (bracketMatch) { const [, fieldName] = bracketMatch; const identifierNames = this.config.jsonIdentifiers || []; for (const identifierName of identifierNames) { const identifier = getIdentifier(identifierName); if (identifier) { const result = identifier.deserialize(params, fieldName); if (result !== void 0) { fields[fieldName] = result; for (const pk of Object.keys(params)) { if (pk.startsWith(`${fieldName}[`)) { processedKeys.add(pk); } } break; } } } } else if (typeof params[paramKey] === "string" && params[paramKey].startsWith("base64:")) { const result = this.base64Resolver.deserialize(params, paramKey); if (result !== void 0) { fields[paramKey] = result; processedKeys.add(paramKey); } } else if (!this.isSpecialField(paramKey)) { const value = params[paramKey]; const stringValue = Array.isArray(value) ? value[0] : value; fields[paramKey] = this.parseSimpleValue(stringValue); processedKeys.add(paramKey); } } return Object.keys(fields).length > 0 ? fields : void 0; } /** * Verifica se é valor simples */ isSimpleValue(value) { return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean"; } /** * Identifica formato JSON e serializa apropriadamente */ identifyAndSerialize(key, value) { const identifierNames = this.config.jsonIdentifiers || []; for (const identifierName of identifierNames) { const identifier = getIdentifier(identifierName); if (identifier && identifier.detect(value)) { return identifier.serialize(key, value); } } const fallbackResolver = this.config.unknownJson || "base64"; if (fallbackResolver === "base64") { return this.base64Resolver.serialize(key, value); } return []; } /** * Verifica se é campo especial (não pertence ao fields) */ isSpecialField(key) { const specialFields = [ "q", "fts", "geom", "page", "limit", "sort", "zoom", "center_lat", "center_lng", "bbox", "layout", "card" ]; return specialFields.includes(key) || key.startsWith("center_") || key.startsWith("bbox_"); } /** * Parse de valor simples com detecção de tipo */ parseSimpleValue(value) { const decoded = decodeURIComponent(value); const commaArrayEnabled = this.config.jsonIdentifiers?.includes("comma-array") || false; if (commaArrayEnabled && decoded.includes(",")) { const parts = decoded.split(",").map((part) => part.trim()).filter((part) => part !== ""); if (parts.length <= 1) { return decoded; } if (this.isLikelyAddress(decoded) || this.hasNumericParts(parts)) { return decoded; } return parts.map((part) => this.convertValue(part)); } return this.convertValue(decoded); } /** * Detecta se parece ser um endereço */ isLikelyAddress(value) { const addressPatterns = [ /\b\d+\b/, // Contém números (provável número da casa) /\b(rua|av|avenida|pça|praça|alameda|estrada|rodovia)\b/i, // Tipos de logradouro /\bcep\b/i, // CEP /-\s*\d{5}/ // Padrão CEP ]; return addressPatterns.some((pattern) => pattern.test(value)); } /** * Verifica se as partes contêm números (indicativo de endereço/medida) */ hasNumericParts(parts) { return parts.some((part) => /^\d+$/.test(part.trim())); } /** * Converte string para tipo apropriado */ convertValue(value) { if (value === "true") return true; if (value === "false") return false; if (/^-?\d+(\.\d+)?$/.test(value)) { const num = Number(value); if (!isNaN(num)) { return num; } } return value; } }; // src/search-state-to-url-parser/src/resolvers/SimpleValueResolver.ts var SimpleValueResolver = class extends BaseResolver { /** * Serializa valor simples para URL * Ex: fts="casa moderna" → ["q=casa+moderna"] */ serialize(key, value) { if (value === null || value === void 0 || value === "") { return []; } const urlKey = this.getUrlKey(key); if (typeof value === "boolean") { return [this.createParam(urlKey, value ? "true" : "false")]; } if (typeof value === "string") { return [`${urlKey}=${encodeURIComponent(value)}`]; } return [this.createParam(urlKey, value)]; } /** * Deserializa de volta para valor simples */ deserialize(params, key) { const urlKey = this.getUrlKey(key); const value = params[urlKey]; if (value === void 0) { return void 0; } const stringValue = Array.isArray(value) ? value[0] : value; if (stringValue === "true") return true; if (stringValue === "false") return false; if (/^-?\d+(\.\d+)?$/.test(stringValue)) { const num = Number(stringValue); if (!isNaN(num)) { return num; } } return decodeURIComponent(stringValue); } }; // src/search-state-to-url-parser/src/resolvers/PaginationSortResolver.ts var PaginationSortResolver = class extends BaseResolver { /** * Serializa objeto de sort para URL * Ex: { price: "desc", date: "asc" } → ["sort=price:desc,date:asc"] */ serialize(key, value) { if (!value || typeof value !== "object") { return []; } const urlKey = this.getUrlKey(key); const sortParts = []; for (const [field, direction] of Object.entries(value)) { if (direction === "asc" || direction === "desc") { sortParts.push(`${field}:${direction}`); } } if (sortParts.length === 0) { return []; } return [`${urlKey}=${sortParts.join(",")}`]; } /** * Deserializa string de sort para objeto * Ex: "price:desc,date:asc" → { price: "desc", date: "asc" } */ deserialize(params, key) { const urlKey = this.getUrlKey(key); const value = params[urlKey]; if (!value) { return void 0; } const rawValue = Array.isArray(value) ? value[0] : value; const sortString = decodeURIComponent(rawValue); const sortObject = {}; const sortParts = sortString.split(","); for (const part of sortParts) { const [field, direction] = part.split(":"); if (field && (direction === "asc" || direction === "desc")) { sortObject[field] = direction; } } return Object.keys(sortObject).length > 0 ? sortObject : void 0; } }; // src/search-state-to-url-parser/src/resolvers/LatLngResolver.ts var LatLngResolver = class extends BaseResolver { /** * Serializa objeto lat/lng para params separados */ serialize(key, value) { if (!value || typeof value !== "object") { return []; } const { lat, lng } = value; if (typeof lat !== "number" || typeof lng !== "number") { return []; } const urlKey = this.getUrlKey(key); return [ `${urlKey}_lat=${lat}`, `${urlKey}_lng=${lng}` ]; } /** * Deserializa params separados para objeto lat/lng */ deserialize(params, key) { const urlKey = this.getUrlKey(key); const latKey = `${urlKey}_lat`; const lngKey = `${urlKey}_lng`; const latValue = params[latKey]; const lngValue = params[lngKey]; if (!latValue || !lngValue) { return void 0; } const lat = Number(Array.isArray(latValue) ? latValue[0] : latValue); const lng = Number(Array.isArray(lngValue) ? lngValue[0] : lngValue); if (isNaN(lat) || isNaN(lng)) { return void 0; } return { lat, lng }; } }; // src/search-state-to-url-parser/src/resolvers/PassthroughResolver.ts var PassthroughResolver = class extends BaseResolver { /** * Serializa valor direto para URL */ serialize(key, value) { if (value === null || value === void 0 || value === "") { return []; } const urlKey = this.getUrlKey(key); if (typeof value === "string") { return [`${urlKey}=${encodeURIComponent(value)}`]; } return [`${urlKey}=${value}`]; } /** * Deserializa valor direto da URL */ deserialize(params, key) { const urlKey = this.getUrlKey(key); const value = params[urlKey]; if (value === void 0) { return void 0; } const stringValue = Array.isArray(value) ? value[0] : value; if (/^-?\d+(\.\d+)?$/.test(stringValue)) { const num = Number(stringValue); if (!isNaN(num)) { return num; } } return decodeURIComponent(stringValue); } }; // src/search-state-to-url-parser/src/resolvers/index.ts var defaultResolvers = { "fields": FieldsResolver, "simple-value": SimpleValueResolver, "pagination-sort": PaginationSortResolver, "lat-lng": LatLngResolver, "base64": Base64Resolver, "passthrough": PassthroughResolver }; function createResolver(name, config) { const ResolverClass = defaultResolvers[name]; if (!ResolverClass) { return void 0; } return new ResolverClass(config); } // src/search-state-to-url-parser/src/core/UrlStateSerializer.ts var UrlStateSerializer = class { constructor(config) { this.config = config; this.resolvers = /* @__PURE__ */ new Map(); this.initializeResolvers(); } /** * Inicializa resolvers baseado na configuração */ initializeResolvers() { for (const [fieldName, fieldConfig] of Object.entries(this.config)) { const resolver = createResolver(fieldConfig.resolver, fieldConfig); if (!resolver) { console.warn(`Resolver "${fieldConfig.resolver}" not found for field "${fieldName}"`); continue; } this.resolvers.set(fieldName, resolver); } } /** * Serializa estado para query string */ serialize(state) { const params = []; for (const [fieldName, fieldValue] of Object.entries(state)) { if (fieldValue === void 0 || fieldValue === null) { continue; } const resolver = this.resolvers.get(fieldName); if (!resolver) { console.warn(`No resolver configured for field "${fieldName}"`); continue; } try { const fieldParams = resolver.serialize(fieldName, fieldValue); params.push(...fieldParams); } catch (error) { console.error(`Error serializing field "${fieldName}":`, error); } } return params.join("&"); } /** * Deserializa query string para estado */ deserialize(queryString) { const params = this.parseQueryString(queryString); const state = {}; for (const [fieldName] of Object.entries(this.config)) { const resolver = this.resolvers.get(fieldName); if (!resolver) { continue; } try { const value = resolver.deserialize(params, fieldName); if (value !== void 0) { state[fieldName] = value; } } catch (error) { console.error(`Error deserializing field "${fieldName}":`, error); } } return state; } /** * Parse de query string para objeto de params */ parseQueryString(queryString) { const params = {}; const cleanQuery = queryString.startsWith("?") ? queryString.substring(1) : queryString; if (!cleanQuery) { return params; } const pairs = cleanQuery.split("&"); for (const pair of pairs) { const [key, value] = pair.split("="); if (!key) continue; const decodedKey = decodeURIComponent(key); const rawValue = value || ""; if (params[decodedKey]) { const existing = params[decodedKey]; if (Array.isArray(existing)) { existing.push(rawValue); } else { params[decodedKey] = [existing, rawValue]; } } else { params[decodedKey] = rawValue; } } return params; } /** * Configuração padrão recomendada */ static getDefaultConfig() { return { filters: { resolver: "fields", jsonIdentifiers: ["range", "between", "min-max"], unknownJson: "base64" }, fts: { resolver: "simple-value", alias: "q" }, page: { resolver: "passthrough" }, limit: { resolver: "passthrough" }, sort: { resolver: "pagination-sort" }, zoom: { resolver: "passthrough" }, center: { resolver: "lat-lng" }, layout: { resolver: "passthrough" }, card: { resolver: "passthrough" } }; } }; // src/search-state-to-url-parser/src/index.ts var defaultConfig = UrlStateSerializer.getDefaultConfig(); // src/seo-slug-generator/src/extractors/FieldExtractor.ts var FieldExtractor = class { constructor(fieldsConfig) { this.fieldsConfig = fieldsConfig; } /** * Extrai valor de um campo lógico dos dados de entrada * Procura por diferentes nomes de campo conforme configuração */ extractField(logicalField, searchFields) { const possibleFieldNames = this.fieldsConfig[logicalField]; if (!possibleFieldNames || !Array.isArray(possibleFieldNames)) { return null; } for (const fieldName of possibleFieldNames) { const value = this.extractFieldValue(searchFields, fieldName); if (value !== null) { return value; } } return null; } /** * Extrai valor de um campo específico, lidando com diferentes tipos */ extractFieldValue(searchFields, fieldName) { const value = searchFields[fieldName]; if (value === null || value === void 0) { return null; } if (typeof value === "string") { return value.trim() || null; } if (typeof value === "number") { return String(value); } if (typeof value === "boolean") { return value ? "sim" : "nao"; } if (Array.isArray(value) && value.length > 0) { const firstValue = value[0]; if (typeof firstValue === "string" && firstValue.trim()) { return firstValue.trim(); } if (typeof firstValue === "number") { return String(firstValue); } } return null; } /** * Extrai todos os campos configurados de uma vez * Retorna mapa com campos encontrados e valores */ extractAllFields(searchFields) { const extracted = {}; for (const logicalField of Object.keys(this.fieldsConfig)) { const value = this.extractField(logicalField, searchFields); if (value !== null) { extracted[logicalField] = value; } } return extracted; } /** * Lista os campos configurados que não foram encontrados nos dados */ getMissingFields(searchFields) { const missing = []; for (const logicalField of Object.keys(this.fieldsConfig)) { const value = this.extractField(logicalField, searchFields); if (value === null) { missing.push(logicalField); } } return missing; } }; // src/seo-slug-generator/src/utils/slugify.ts function slugify(text, preserveSlash = false) { if (text === null || text === void 0) { return ""; } return String(text).toLowerCase().trim().replace(/[àáâãäå]/g, "a").replace(/[èéêë]/g, "e").replace(/[ìíîï]/g, "i").replace(/[òóôõö]/g, "o").replace(/[ùúûü]/g, "u").replace(/[ç]/g, "c").replace(/[ñ]/g, "n").replace(/[ÿ]/g, "y").replace(/[&]/g, "e").replace(/[+]/g, "mais").replace(/[%]/g, "pct").replace(preserveSlash ? /[^\w\s-/]/g : /[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, ""); } function truncateSlug(slug, maxLength, separator = "-") { if (slug.length <= maxLength) { return slug; } const truncated = slug.substring(0, maxLength); const lastSeparator = truncated.lastIndexOf(separator); return lastSeparator > 0 ? truncated.substring(0, lastSeparator) : truncated; } function isValidSlug(slug) { return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug); } // src/seo-slug-generator/src/core/SlugGenerator.ts var SlugGenerator = class { constructor(config) { this.config = config; this.extractor = new FieldExtractor(config.fields); this.options = { prefix: config.options?.prefix || "", maxLength: config.options?.maxLength || 80, minParts: config.options?.minParts || 2, separator: this.resolveSeparator(config.options?.separator || "dash") }; } /** * Resolve o separador baseado na configuração */ resolveSeparator(separator) { if (separator === "dash") return "-"; if (separator === "slash") return "/"; return separator; } /** * Formata valor do campo para melhor legibilidade na URL */ formatFieldValue(logicalField, value) { if (logicalField === "quartos" || logicalField === "bedrooms") { if (/^\d+$/.test(value)) { return `${value}-quarto`; } } return value; } /** * Gera slug simples a partir dos campos de busca */ generate(searchFields) { const result = this.generateWithDetails(searchFields); return result.slug; } /** * Gera slug com detalhes completos do processo */ generateWithDetails(searchFields) { const extractedFields = this.extractor.extractAllFields(searchFields); const missingFields = this.extractor.getMissingFields(searchFields); const parts = []; const usedFields = {}; const isSlashMode = this.options.separator === "/"; for (const logicalField of this.config.order) { const value = extractedFields[logicalField]; if (value) { const formattedValue = this.formatFieldValue(logicalField, v