@horizon-apps/domain-schema-core
Version:
Core domain schema utilities for Horizon Platform - Schema generators, data enrichers, converters and specifications
528 lines (439 loc) • 14.9 kB
Markdown
# 🚀 Domain Data Display Enricher - Guia Frontend
> Motor de enriquecimento para transformar dados brutos em objetos enriquecidos para display na UI
## 📋 Índice
1. [Como Usar no Frontend](#-como-usar-no-frontend)
2. [Formato dos EnrichMappers](#-formato-dos-enrichmappers)
3. [Estrutura de Saída](#-estrutura-de-saída)
4. [Schemas SSOT](#-schemas-ssot)
5. [Exemplos Completos](#-exemplos-completos)
6. [Troubleshooting](#-troubleshooting)
## 🎯 Como Usar no Frontend
### **1. Instalação**
```typescript
import { DomainDataDisplayEnricher } from '@horizon-apps/domain-schema-core'
```
### **2. Criar Registry de Domínios**
```typescript
const registry = {
property: {
key: "property",
enrichMapper: propertyEnrichMapper
},
broker: {
key: "broker",
enrichMapper: brokerEnrichMapper
}
}
```
### **3. Instanciar Motor**
```typescript
const enricher = new DomainDataDisplayEnricher(registry, {
locale: "pt-BR",
currency: "BRL",
includeMetadata: false, // true = copia COMPLETA do FieldMetadata (todos os campos)
getIcon: (iconName: string) => `<icon-${iconName} />`
})
```
### **4. Processar Dados**
```typescript
const rawData = {
id: 1001,
title: "Casa teste",
valor: 1500000,
broker: { name: "João Silva", creci: "12345" }
}
const enrichedData = enricher.enrich(rawData, "property")
```
## 🏗️ Formato dos EnrichMappers
**FORMATO OBRIGATÓRIO** que cada domínio deve implementar:
```typescript
type EnrichMapper = (rawData: Record<string, any>) => EnrichMapperResult
interface EnrichMapperResult {
data: Record<string, any> // Dados originais
schema: FieldMetadata[] // Schema SSOT - SÓ ARRAY!
computedSchema: FieldMetadata[] // Schema dos campos computados - SÓ ARRAY!
computedData: Record<string, any> // Dados computados
}
```
### **Exemplo Property EnrichMapper:**
```typescript
const propertyEnrichMapper = (rawData: Record<string, any>): EnrichMapperResult => {
// Cálculos complexos
const area_total = (rawData.area_privativa || 0) + (rawData.area_comum || 0)
const valor_m2 = area_total > 0 ? Math.round(rawData.valor / area_total) : 0
// Categorizar preço
let categoria_preco = "economico"
if (valor_m2 > 15000) categoria_preco = "luxo"
else if (valor_m2 > 10000) categoria_preco = "alto"
else if (valor_m2 > 6000) categoria_preco = "medio"
return {
data: rawData, // ← Dados originais
schema: propertySchemaArray, // ← Schema principal ARRAY!
computedSchema: propertyComputedSchemaArray, // ← Schema computados ARRAY!
computedData: { // ← Valores computados
area_total,
valor_m2,
categoria_preco
}
}
}
```
### **Exemplo Broker EnrichMapper:**
```typescript
const brokerEnrichMapper = (rawData: Record<string, any>): EnrichMapperResult => {
const display_name = `${rawData.name} (CRECI: ${rawData.creci})`
const contact_preference = rawData.phone ? "WhatsApp/Telefone" : "E-mail"
return {
data: rawData,
schema: brokerSchemaArray, // ARRAY DIRETO!
computedSchema: brokerComputedSchemaArray, // ARRAY DIRETO!
computedData: {
display_name,
contact_preference
}
}
}
```
## 📤 Estrutura de Saída
### **EnrichedField (Campo Individual):**
```typescript
interface EnrichedField {
// SEMPRE PRIMEIRO
key: string // Chave original do campo (sempre presente, primeiro)
// VALORES CORE (sempre presente)
value: any // RAW para números/medidas, RESOLVIDO para enums
label: string // ui.label do schema
// VALORES PROCESSADOS (quando aplicável)
valueLabel?: string | string[] // Formatado: moeda, data, enum resolvido
displayLabel?: string // Template processado com emojis
// VISUAL
iconName?: string // Nome do ícone (nunca resolvido)
icon?: string // Ícone resolvido pela função getIcon (só quando configurada)
// METADADOS BÁSICOS
categories?: string[] // Categorias para agrupamento/filtros
// METADADOS COMPLETOS (só quando includeMetadata: true)
// Cópia EXATA e COMPLETA do FieldMetadata original (incluindo campos undefined)
metadata?: FieldMetadata;
}
```
### **Lógica de Values:**
| Tipo Campo | Value | ValueLabel | Exemplo |
|------------|-------|------------|---------|
| **Número/Medida** | RAW | Formatado | `value: 1500000` → `valueLabel: "R$ 1.500.000,00"` |
| **Enum String** | RAW | Resolvido | `value: "economico"` → `valueLabel: "Econômico"` |
| **Enum Array** | RAW | Resolvido | `value: ["wifi", "piscina"]` → `valueLabel: ["WiFi", "Piscina"]` |
| **Boolean** | RAW | Traduzido | `value: true` → `valueLabel: "Sim"` |
### **Exemplo de Saída Completa:**
```typescript
{
// Campo simples
valor: {
key: "valor", // Key sempre primeiro
value: 1500000, // RAW para conversões
label: "Valor de Venda",
valueLabel: "R$ 1.500.000,00", // Formatado para display
displayLabel: "💰 R$ 1.500.000,00", // Com template
// METADADOS BÁSICOS (sempre presentes quando existem)
categories: ["valores"],
type: "Number", // Tipo do campo
format: "currency", // Formato do campo
unit: "BRL", // Unidade do campo
// METADADOS COMPLETOS (só com includeMetadata: true)
// CÓPIA COMPLETA do FieldMetadata - TODO campo que vier no schema
metadata: {
key: "valor",
type: "Number",
format: "currency",
unit: "BRL",
categories: ["valores"],
validation: undefined,
ui: { label: "Valor de Venda", displayTemplate: "💰 {{valueLabel}}", filterable: true, sortable: true },
audit: undefined,
enum: undefined,
rules: undefined,
db: undefined
}
},
// Campo com enum
tags: {
key: "tags", // Key sempre primeiro
value: ["wifi", "piscina"], // RAW (chaves)
label: "Tags",
valueLabel: ["WiFi", "Piscina"] // RESOLVIDO (labels)
},
// Relacionamento nested
broker: {
name: {
key: "name",
value: "João Silva",
label: "Nome"
},
// ... outros campos do broker
computed: {
display_name: {
key: "display_name",
value: "João Silva (CRECI: 12345)",
label: "Nome Completo"
}
}
},
// Campos computados na raiz
computed: {
area_total: {
key: "area_total", // Key sempre primeiro
value: 225, // RAW para conversões
label: "Área Total",
valueLabel: "225 m²", // Formatado
displayLabel: "📏 225 m² total"
}
}
}
```
## 📋 Schemas SSOT
### **Estrutura Completa (Formato Array):**
```typescript
interface FieldMetadata {
// RAIZ - Identificação básica
key: string
type: "String" | "Number" | "Boolean" | "String[]" | "Json" | "Json[]"
enum?: Record<string, string> // Para resolução de chaves → labels
format?: "currency" | "date" | "area" | "distance" | "percent" | "count" | "year"
unit?: string // BRL, m2, etc
categories?: string[]
// CONTEXTOS
rules?: { parent?: string, conditions?: string[] }
validation?: { required?: boolean, min?: number, max?: number }
db?: { type?: string, unique?: boolean, index?: boolean }
ui?: UiContext
audit?: { origin?: string, modifiedBy?: string[] }
}
interface UiContext {
label?: string // Label para display
description?: string
placeholder?: string
iconName?: string // Nome do ícone (ui.iconName!)
displayTemplate?: string // Template com {{value}}, {{valueLabel}}
mask?: string
searchable?: boolean
filterable?: boolean
sortable?: boolean
}
```
### **Exemplo Schema Property (Array Format):**
```typescript
const propertySchemaArray: FieldMetadata[] = [
{
key: "valor",
type: "Number",
format: "currency",
unit: "BRL",
ui: {
label: "Valor de Venda",
displayTemplate: "💰 {{valueLabel}}",
filterable: true,
sortable: true
}
},
{
key: "tags",
type: "String[]",
enum: {
"wifi": "WiFi gratuito",
"piscina": "Piscina",
"garagem": "Garagem"
},
ui: {
label: "Tags",
displayTemplate: "🏷️ {{valueLabel}}",
filterable: true
}
},
{
key: "quartos",
type: "Number",
format: "count",
ui: {
label: "Quartos",
displayTemplate: "🛏️ {{value}} quarto{{p:s}}", // Pluralização
iconName: "bedroom"
}
}
]
```
## 🎨 Templates
### **Sintaxe Suportada:**
- `{{value}}` - Valor raw
- `{{valueLabel}}` - Valor formatado
- `{{p:texto}}` - Texto apenas no plural (valor ≠ 1)
- `{{s:texto}}` - Texto apenas no singular (valor = 1)
### **Exemplos:**
```typescript
// Pluralização
"{{value}} quarto{{p:s}}" // 1 quarto | 3 quartos
// Com emoji
"💰 {{valueLabel}}" // 💰 R$ 1.500.000,00
// Condicional
"{{s:⭐ }}{{valueLabel}}" // ⭐ Sim | Não
```
## 🔗 Relacionamentos Nested
O motor detecta automaticamente relacionamentos por:
1. **Nome do campo** (broker, images, user, etc)
2. **Estrutura** (objeto ou array de objetos)
### **Suportados:**
```typescript
// Relacionamento único
broker: { name: "João", creci: "123" }
// Array de relacionamentos
images: [
{ url: "img1.jpg", caption: "Fachada" },
{ url: "img2.jpg", caption: "Sala" }
]
```
## 🧮 Campos Computados
### **Como Funcionam:**
1. **EnrichMapper** calcula valores derivados
2. **Motor** aplica schema dos computados
3. **Resultado** fica em `computed: {}`
### **Exemplo:**
```typescript
// No EnrichMapper
computedData: {
area_total: 180 + 45, // = 225
valor_m2: 1500000 / 225 // = 6666
}
// Na saída
computed: {
area_total: {
value: 225, // RAW para conversões
label: "Área Total",
valueLabel: "225 m²", // Formatado
displayLabel: "📏 225 m² total"
}
}
```
## 🛠️ Troubleshooting
### **Problema: Campo não aparece enriquecido**
- ✅ Verificar se o campo existe no schema array
- ✅ Verificar se o EnrichMapper retorna os dados corretos
### **Problema: ValueLabel não resolve enum**
- ✅ Verificar se `enum` está definido no schema
- ✅ Verificar se os valores raw correspondem às chaves do enum
### **Problema: DisplayTemplate não funciona**
- ✅ Verificar sintaxe: `{{value}}`, `{{valueLabel}}`
- ✅ Arrays não processam templates (retorna `undefined`)
### **Problema: IconName não aparece**
- ✅ Verificar se `ui.iconName` está definido no schema (não mais `ui.icon`!)
- ✅ IconName sempre como string, nunca objeto resolvido
### **Problema: Schema não funciona**
- ✅ Verificar se está retornando `FieldMetadata[]` (array) e não objeto
- ✅ EnrichMapper deve retornar `schema` e `computedSchema` como arrays
### **Problema: Campos null sendo enriquecidos desnecessariamente**
- ✅ **Comportamento correto**: Campos `null` e `undefined` são **automaticamente pulados**
- ✅ **Resultado**: Campos null não aparecem no resultado (economiza payload)
- ✅ **Arrays vazios `[]`**: São enriquecidos normalmente (comportamento esperado)
- ✅ **Semântica**: `null` = "não aplicável", `[]` = "lista vazia"
## 📝 Checklist de Implementação Frontend
### **1. Criar EnrichMappers:**
- [ ] Implementar função `(rawData) => EnrichMapperResult`
- [ ] Definir schemas principal e computado como **arrays**
- [ ] Calcular campos derivados
- [ ] Usar `ui.iconName` (não `ui.icon`)
### **2. Configurar Registry:**
- [ ] Mapear domínios para EnrichMappers
- [ ] Configurar opções (locale, currency, getIcon)
### **3. Usar no Componente:**
- [ ] Instanciar `DomainDataDisplayEnricher`
- [ ] Chamar `enricher.enrich(data, domain)`
- [ ] Renderizar campos enriquecidos
### **4. Tratar Saída:**
- [ ] `value` para conversões/formulários
- [ ] `valueLabel` para display simples
- [ ] `displayLabel` para display com template
- [ ] `iconName` para resolução de ícones
## 🎣 React Hooks
### **useEnrichList() - Para Listas**
```typescript
// Grid de resultados de busca
export function GridResultado({ data }) {
const { enrichedData, loading, error } = useEnrichList(
data, // Array de dados brutos
'property', // Domínio
registry, // Registry de domínios
{ locale: 'pt-BR' } // Opções (opcional)
);
if (loading) return <div>Enriquecendo dados...</div>;
if (error) return <div>Erro: {error.message}</div>;
return (
<div className="grid">
{enrichedData.map((item, i) => (
<PropertyCard key={i} {...item} />
))}
</div>
);
}
```
### **useEnrich() - Para Item Único**
```typescript
// Página single de imóvel
export function PropertySingle({ propertyId }) {
const { data: rawProperty } = useQuery(`/property/${propertyId}`);
const { enrichedData, loading, error } = useEnrich(
rawProperty, // Dados brutos do imóvel
'property', // Domínio
registry // Registry de domínios
);
if (loading) return <div>Carregando imóvel...</div>;
if (error) return <div>Erro: {error.message}</div>;
if (!enrichedData) return null;
return (
<div className="property-single">
<h1>{enrichedData.title.displayLabel}</h1>
<div className="price">{enrichedData.valor.displayLabel}</div>
<div className="details">
<span>{enrichedData.quartos.displayLabel}</span>
<span>{enrichedData.area_privativa.displayLabel}</span>
</div>
{/* Campos computados */}
{enrichedData.computed && (
<div className="computed">
<span>{enrichedData.computed.area_total.displayLabel}</span>
<span>{enrichedData.computed.valor_m2.displayLabel}</span>
</div>
)}
{/* Relacionamento broker */}
{enrichedData.broker && (
<div className="broker">
<h3>{enrichedData.broker.name.displayLabel}</h3>
<p>{enrichedData.broker.phone.displayLabel}</p>
</div>
)}
</div>
);
}
```
### **useEnricher() - Para Controle Manual**
```typescript
// Quando quiser controle total
export function CustomComponent({ data }) {
const enricher = useEnricher(registry, { locale: 'pt-BR' });
const handleEnrich = () => {
const enriched = enricher.enrich(data, 'property');
// fazer algo com enriched...
};
const handleEnrichList = () => {
const enrichedList = enricher.enrichList(dataArray, 'property');
// fazer algo com enrichedList...
};
return <button onClick={handleEnrich}>Enriquecer</button>;
}
```