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