@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
JavaScript
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