@horizon-framework/api-nextjs-docs 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/BATCH_TOTALS_API.md +200 -0
- package/docs/DOMINIO_ENTIDADES.md +328 -0
- package/docs/MODULE_CREATION_PATTERN.md +624 -0
- package/docs/SEARCH_ENGINE_API.md +1038 -0
- package/docs/SSOT_SPECIFICATION_V2.md +333 -0
- package/docs/SYNC_API_PATTERN.md +268 -0
- package/package.json +10 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# SSOT (Single Source of Truth) - Especificação v2.0
|
|
2
|
+
|
|
3
|
+
## Visão Geral
|
|
4
|
+
|
|
5
|
+
O novo formato SSOT v2.0 organiza metadados de campos em **contextos específicos**, permitindo separação clara de responsabilidades e melhor organização dos dados.
|
|
6
|
+
|
|
7
|
+
### Filosofia do Formato
|
|
8
|
+
- **Contextos Separados**: Cada tipo de metadado tem seu lugar específico
|
|
9
|
+
- **Estrutura Hierárquica**: Agrupamento lógico das propriedades
|
|
10
|
+
- **Flexibilidade**: Campos podem ser vazios e preenchidos conforme necessário
|
|
11
|
+
- **Evolução Gradual**: Estrutura permite crescimento sem quebrar compatibilidade
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Estrutura dos Contextos
|
|
16
|
+
|
|
17
|
+
### 1. **RAIZ** - Identificação Básica
|
|
18
|
+
```typescript
|
|
19
|
+
{
|
|
20
|
+
"key": "campo_exemplo", // Identificador único (snake_case)
|
|
21
|
+
"type": "String", // Tipo de dado (String, Number, Boolean, String[], Json)
|
|
22
|
+
"enum": {"key": "Label"}, // Valores possíveis (quando aplicável)
|
|
23
|
+
"format": "currency", // Formatação de exibição
|
|
24
|
+
"unit": "BRL", // Unidade de medida
|
|
25
|
+
"categories": ["valores"] // Tags/categorias para filtros
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. **RULES** - Regras e Relações
|
|
30
|
+
```typescript
|
|
31
|
+
"rules": {
|
|
32
|
+
"parent": "tipo", // Campo pai hierárquico
|
|
33
|
+
"conditions": ["operacao:venda"] // Condições de visibilidade
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. **VALIDATION** - Validação Backend/Zod
|
|
38
|
+
```typescript
|
|
39
|
+
"validation": {
|
|
40
|
+
"required": true, // Campo obrigatório
|
|
41
|
+
"min": 0, // Valor mínimo (números)
|
|
42
|
+
"max": 999999.99, // Valor máximo (números)
|
|
43
|
+
"minLength": 3, // Tamanho mínimo (strings)
|
|
44
|
+
"maxLength": 200, // Tamanho máximo (strings)
|
|
45
|
+
"precision": 2 // Casas decimais
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 4. **DB** - Configurações de Banco
|
|
50
|
+
```typescript
|
|
51
|
+
"db": {
|
|
52
|
+
// VAZIO por enquanto - será preenchido conforme necessário
|
|
53
|
+
// Futuro: index, unique, default, etc.
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 5. **UI** - Interface e Experiência
|
|
58
|
+
```typescript
|
|
59
|
+
"ui": {
|
|
60
|
+
"label": "Campo Exemplo", // Rótulo para exibição
|
|
61
|
+
"description": "Texto de ajuda", // Helper text para formulários
|
|
62
|
+
"placeholder": "Digite...", // Placeholder para inputs
|
|
63
|
+
"icon": "home", // Ícone semântico
|
|
64
|
+
"displayTemplate": "{{value}} m²", // Template de exibição
|
|
65
|
+
"mask": "cpf", // Máscara de input (cpf, cnpj, cep, phone, email)
|
|
66
|
+
"searchable": true, // Pesquisável por texto
|
|
67
|
+
"filterable": true, // Aparece nos filtros
|
|
68
|
+
"sortable": true // Permite ordenação
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 6. **AUDIT** - Auditoria e Rastreamento
|
|
73
|
+
```typescript
|
|
74
|
+
"audit": {
|
|
75
|
+
"origin": "horizon-base/imovel", // Origem do campo
|
|
76
|
+
"modifiedBy": ["system"] // Histórico de modificações
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Tipos de Dados Suportados
|
|
83
|
+
|
|
84
|
+
| Tipo | Descrição | Exemplo |
|
|
85
|
+
|------|-----------|---------|
|
|
86
|
+
| `String` | Texto simples | "Casa no Centro" |
|
|
87
|
+
| `Number` | Números inteiros e decimais | 150000.50 |
|
|
88
|
+
| `Boolean` | Verdadeiro/Falso | true, false |
|
|
89
|
+
| `String[]` | Array de strings | ["piscina", "churrasqueira"] |
|
|
90
|
+
| `Json` | Objeto JSON | {"lat": -27.5, "lng": -48.5} |
|
|
91
|
+
| `Json[]` | Array de objetos | [{"url": "...", "caption": "..."}] |
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Formatos de Exibição
|
|
96
|
+
|
|
97
|
+
| Format | Unit | Exemplo Entrada | Exemplo Saída |
|
|
98
|
+
|--------|------|----------------|---------------|
|
|
99
|
+
| `currency` | `BRL` | 150000 | R$ 150.000,00 |
|
|
100
|
+
| `currency` | `USD` | 150000 | $ 150,000.00 |
|
|
101
|
+
| `area` | `m2` | 150 | 150 m² |
|
|
102
|
+
| `distance` | `km` | 1.5 | 1,5 km |
|
|
103
|
+
| `percent` | - | 0.15 | 15% |
|
|
104
|
+
| `count` | - | 3 | 3 itens |
|
|
105
|
+
| `date` | - | "2024-01-01" | 01/01/2024 |
|
|
106
|
+
| `datetime` | - | "2024-01-01T10:30:00Z" | 01/01/2024 10:30 |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Categorias Recomendadas
|
|
111
|
+
|
|
112
|
+
| Categoria | Descrição | Campos Típicos |
|
|
113
|
+
|-----------|-----------|----------------|
|
|
114
|
+
| `valores` | Campos monetários | valor_venda, valor_locacao |
|
|
115
|
+
| `localizacao` | Endereço e geolocalização | endereco_cidade, latitude, longitude |
|
|
116
|
+
| `estrutura` | Características físicas | quartos, banheiros, area_total |
|
|
117
|
+
| `identificacao` | Títulos e referências | title, reference |
|
|
118
|
+
| `comercial` | Informações comerciais | operacao, status |
|
|
119
|
+
| `caracteristicas` | Amenidades e comodidades | piscina, churrasqueira |
|
|
120
|
+
| `sistema` | Campos internos | id, created_at, updated_at |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Máscaras Disponíveis
|
|
125
|
+
|
|
126
|
+
### Automáticas (inferidas do format + unit)
|
|
127
|
+
- `currency-brl` → R$ 1.000,00 (format: currency + unit: BRL)
|
|
128
|
+
- `currency-usd` → $ 1,000.00 (format: currency + unit: USD)
|
|
129
|
+
- `area` → 100 m² (format: area + unit: m2)
|
|
130
|
+
- `distance` → 1,5 km (format: distance + unit: km)
|
|
131
|
+
- `percent` → 15% (format: percent)
|
|
132
|
+
- `date` → DD/MM/AAAA (format: date)
|
|
133
|
+
|
|
134
|
+
### Explícitas (para casos específicos)
|
|
135
|
+
- `cpf` → 000.000.000-00
|
|
136
|
+
- `cnpj` → 00.000.000/0000-00
|
|
137
|
+
- `cep` → 00000-000
|
|
138
|
+
- `phone` → (00) 00000-0000
|
|
139
|
+
- `email` → Validação de email
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Condições de Visibilidade
|
|
144
|
+
|
|
145
|
+
### Operadores Disponíveis
|
|
146
|
+
|
|
147
|
+
| Operador | Descrição | Exemplo |
|
|
148
|
+
|----------|-----------|---------|
|
|
149
|
+
| `:` (padrão) | Contém | `"operacao:venda"` |
|
|
150
|
+
| `.equals` | Igual a | `"tipo.equals:apartamento"` |
|
|
151
|
+
| `.not` | Diferente de | `"status.not:vendido"` |
|
|
152
|
+
| `.in` | Está na lista | `"tipo.in:casa,apartamento"` |
|
|
153
|
+
| `.gt` | Maior que | `"valor.gt:100000"` |
|
|
154
|
+
| `.gte` | Maior ou igual | `"valor.gte:100000"` |
|
|
155
|
+
| `.lt` | Menor que | `"quartos.lt:5"` |
|
|
156
|
+
| `.lte` | Menor ou igual | `"area.lte:200"` |
|
|
157
|
+
| `.exists` | Campo preenchido | `"condominio.exists:true"` |
|
|
158
|
+
|
|
159
|
+
### Exemplos de Uso
|
|
160
|
+
```typescript
|
|
161
|
+
"rules": {
|
|
162
|
+
"conditions": [
|
|
163
|
+
"operacao:venda", // operacao contém "venda"
|
|
164
|
+
"tipo.equals:apartamento", // tipo é exatamente "apartamento"
|
|
165
|
+
"valor.gte:100000", // valor >= 100000
|
|
166
|
+
"quartos.in:2,3,4" // quartos é 2, 3 ou 4
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Exemplos Práticos
|
|
174
|
+
|
|
175
|
+
### Campo Simples
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"key": "title",
|
|
179
|
+
"type": "String",
|
|
180
|
+
"categories": ["identificacao"],
|
|
181
|
+
"validation": {
|
|
182
|
+
"required": true,
|
|
183
|
+
"minLength": 3,
|
|
184
|
+
"maxLength": 200
|
|
185
|
+
},
|
|
186
|
+
"ui": {
|
|
187
|
+
"label": "Título",
|
|
188
|
+
"description": "Título do imóvel",
|
|
189
|
+
"placeholder": "Ex: Casa no Centro",
|
|
190
|
+
"searchable": true
|
|
191
|
+
},
|
|
192
|
+
"audit": {
|
|
193
|
+
"origin": "horizon-base/imovel"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Campo com Enum
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"key": "tipo",
|
|
202
|
+
"type": "String",
|
|
203
|
+
"enum": {
|
|
204
|
+
"casa": "Casa",
|
|
205
|
+
"apartamento": "Apartamento",
|
|
206
|
+
"terreno": "Terreno"
|
|
207
|
+
},
|
|
208
|
+
"categories": ["estrutura"],
|
|
209
|
+
"validation": {
|
|
210
|
+
"required": true
|
|
211
|
+
},
|
|
212
|
+
"ui": {
|
|
213
|
+
"label": "Tipo",
|
|
214
|
+
"filterable": true,
|
|
215
|
+
"sortable": true
|
|
216
|
+
},
|
|
217
|
+
"audit": {
|
|
218
|
+
"origin": "horizon-base/imovel"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Campo Monetário
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"key": "valor_venda",
|
|
227
|
+
"type": "Number",
|
|
228
|
+
"format": "currency",
|
|
229
|
+
"unit": "BRL",
|
|
230
|
+
"categories": ["valores"],
|
|
231
|
+
"validation": {
|
|
232
|
+
"min": 0,
|
|
233
|
+
"max": 999999999.99,
|
|
234
|
+
"precision": 2
|
|
235
|
+
},
|
|
236
|
+
"ui": {
|
|
237
|
+
"label": "Valor de Venda",
|
|
238
|
+
"displayTemplate": "{{value}}",
|
|
239
|
+
"filterable": true,
|
|
240
|
+
"sortable": true
|
|
241
|
+
},
|
|
242
|
+
"audit": {
|
|
243
|
+
"origin": "horizon-base/imovel"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Campo com Condição
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"key": "valor_locacao",
|
|
252
|
+
"type": "Number",
|
|
253
|
+
"format": "currency",
|
|
254
|
+
"unit": "BRL",
|
|
255
|
+
"categories": ["valores"],
|
|
256
|
+
"rules": {
|
|
257
|
+
"conditions": ["operacao:locacao"]
|
|
258
|
+
},
|
|
259
|
+
"validation": {
|
|
260
|
+
"min": 0,
|
|
261
|
+
"precision": 2
|
|
262
|
+
},
|
|
263
|
+
"ui": {
|
|
264
|
+
"label": "Valor de Locação",
|
|
265
|
+
"filterable": true,
|
|
266
|
+
"sortable": true
|
|
267
|
+
},
|
|
268
|
+
"audit": {
|
|
269
|
+
"origin": "horizon-base/imovel"
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Geração Automática
|
|
277
|
+
|
|
278
|
+
### Zod Schema
|
|
279
|
+
A partir do SSOT, é gerado automaticamente:
|
|
280
|
+
- Schema Zod para validação
|
|
281
|
+
- Tipos TypeScript inferidos
|
|
282
|
+
- Funções de validação (parse, safeParse, partial)
|
|
283
|
+
|
|
284
|
+
### Exemplo de Geração
|
|
285
|
+
```typescript
|
|
286
|
+
// Gerado automaticamente a partir do SSOT
|
|
287
|
+
export const ImovelZod = z.object({
|
|
288
|
+
id: z.number().optional(),
|
|
289
|
+
reference: z.string().min(1).max(50),
|
|
290
|
+
title: z.string().min(3).max(200),
|
|
291
|
+
tipo: z.enum(["casa", "apartamento", "terreno"]),
|
|
292
|
+
valor_venda: z.number().min(0).multipleOf(0.01).optional()
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
export type ImovelType = z.infer<typeof ImovelZod>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Vantagens do Novo Formato
|
|
301
|
+
|
|
302
|
+
1. **Organização Clara**: Cada contexto tem seu espaço definido
|
|
303
|
+
2. **Flexibilidade**: Campos podem ser vazios e evoluir gradualmente
|
|
304
|
+
3. **Separação de Responsabilidades**: UI, validação, DB em contextos isolados
|
|
305
|
+
4. **Manutenibilidade**: Fácil identificar onde cada metadado pertence
|
|
306
|
+
5. **Evolução**: Adicionar novos contextos sem quebrar existentes
|
|
307
|
+
6. **Reuso**: Mesmo campo pode ter comportamentos diferentes por contexto
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Checklist de Criação
|
|
312
|
+
|
|
313
|
+
### Campo Mínimo Obrigatório
|
|
314
|
+
- [ ] `key` - Identificador único
|
|
315
|
+
- [ ] `type` - Tipo de dado
|
|
316
|
+
- [ ] `ui.label` - Rótulo para exibição
|
|
317
|
+
- [ ] `audit.origin` - Origem do campo
|
|
318
|
+
|
|
319
|
+
### Campo Típico (recomendado)
|
|
320
|
+
- [ ] Validação básica (`required`, limites)
|
|
321
|
+
- [ ] Categoria para organização
|
|
322
|
+
- [ ] Formatação quando aplicável
|
|
323
|
+
- [ ] Configurações de UI (searchable, filterable)
|
|
324
|
+
|
|
325
|
+
### Campo Complexo
|
|
326
|
+
- [ ] Enum quando valores limitados
|
|
327
|
+
- [ ] Condições de visibilidade
|
|
328
|
+
- [ ] Display template customizado
|
|
329
|
+
- [ ] Relacionamentos hierárquicos
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
Esta especificação serve como **base definitiva** para criação de schemas SSOT v2.0 no sistema, garantindo consistência, flexibilidade e evolução controlada dos metadados.
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Sync API — Padrao de Sincronizacao de Modulos
|
|
2
|
+
|
|
3
|
+
> Endpoint /sync que TODO modulo backend Horizon implementa.
|
|
4
|
+
> Ponto de entrada de dados externos (CRM, integradores) para o banco.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Visao Geral
|
|
9
|
+
|
|
10
|
+
Cada modulo tem 3 operacoes no /sync:
|
|
11
|
+
|
|
12
|
+
| Metodo | Rota | Autenticacao | Funcao |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
| GET | /api/{entity}/sync | Publico (CORS *) | Listing — retorna IDs + refs + updated_at para diff |
|
|
15
|
+
| PUT | /api/{entity}/sync | API Key (x-api-key) | Upsert batch — recebe array, valida, transforma, persiste |
|
|
16
|
+
| DELETE | /api/{entity}/sync | API Key (x-api-key) | Delete por refs/keys — remove items do banco |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Arquitetura de Arquivos
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
apps/api/src/
|
|
24
|
+
├── app/api/{entity}/sync/
|
|
25
|
+
│ └── route.ts # "Ponte seca" — exporta controller
|
|
26
|
+
├── core/
|
|
27
|
+
│ ├── prisma.ts # Singleton Prisma Client
|
|
28
|
+
│ └── route-helpers.ts # withApiKeyProtection() + withPublicCors()
|
|
29
|
+
└── modules/{entity}/
|
|
30
|
+
├── schemas/
|
|
31
|
+
│ └── source-schema.zod.ts # Importa Zod do pacote CRM
|
|
32
|
+
├── controllers/
|
|
33
|
+
│ └── sync.controller.ts # GET/PUT/DELETE — valida body, chama service
|
|
34
|
+
├── services/
|
|
35
|
+
│ └── sync.service.ts # Logica: prepare → transaction → return
|
|
36
|
+
└── mappers/
|
|
37
|
+
├── dataModifier/
|
|
38
|
+
│ ├── index.ts # ObjectTransformer com regras do cliente
|
|
39
|
+
│ └── rules.ts # Regras especificas CRM/cliente (pode estar vazio)
|
|
40
|
+
└── computedFields/
|
|
41
|
+
├── index.ts # Orquestra todos os computed
|
|
42
|
+
├── seo-slug-gen.ts # Gera slug SEO
|
|
43
|
+
├── thumbnails.ts # Gera thumbnails das imagens
|
|
44
|
+
├── endereco-completo.ts# Monta endereco formatado
|
|
45
|
+
├── tipo-subtipo-gen.ts # Mapeia tipo → tipo_gen + subtipo_gen
|
|
46
|
+
└── valor-m2.ts # Calcula valor por m2
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Fluxo PUT (Upsert)
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
1. REQUEST → PUT /api/{entity}/sync (x-api-key header)
|
|
55
|
+
Body: { "{entity}s": [ {...item1}, {...item2} ] }
|
|
56
|
+
|
|
57
|
+
2. ROUTE → withApiKeyProtection() verifica x-api-key
|
|
58
|
+
Se invalido: 401 Unauthorized
|
|
59
|
+
|
|
60
|
+
3. CONTROLLER
|
|
61
|
+
→ Parse body JSON
|
|
62
|
+
→ Validacao Zod RIGOROSA do array
|
|
63
|
+
Property: usa Zod do pacote CRM (ex: HorizonPropertySchemaBySi9Zod)
|
|
64
|
+
Broker: schema inline com .passthrough()
|
|
65
|
+
Se falha: 400 com detalhes Zod
|
|
66
|
+
|
|
67
|
+
4. SERVICE
|
|
68
|
+
→ validateUpsertInput() — array nao vazio, cada item tem key/reference
|
|
69
|
+
→ Loop de preparacao (cada item):
|
|
70
|
+
- dataModifier(item) → Regras de negocio do cliente
|
|
71
|
+
- computeFields(item) → Campos calculados (slug, thumb, endereco, flags)
|
|
72
|
+
- stripUnknownFields(item) → Remove campos que nao existem no model Prisma
|
|
73
|
+
- Salva sync_raw_data → JSONB com snapshot completo
|
|
74
|
+
→ Prisma.$transaction():
|
|
75
|
+
- BACKUP first_synced_at dos existentes
|
|
76
|
+
- deleteMany({ where: { reference: { in: refs } } })
|
|
77
|
+
- createMany({ data: prepared, skipDuplicates: true })
|
|
78
|
+
- Restaura first_synced_at
|
|
79
|
+
|
|
80
|
+
5. RESPONSE
|
|
81
|
+
{ success: true, inserted: N, updated: N, total: N, references: [...] }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Por que delete + insert (e nao upsert nativo)?**
|
|
85
|
+
Prisma upsert opera 1 registro por vez. Para batches de 50+ itens, deleteMany + createMany numa transaction e mais performatico. Garante que campos removidos do CRM sao limpos.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Fluxo GET (Listing)
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
GET /api/{entity}/sync?source_key=si9-api-imoveis
|
|
93
|
+
|
|
94
|
+
→ findMany com select minimo: { id, reference, source_updated_at }
|
|
95
|
+
→ Filtro opcional por source_key
|
|
96
|
+
→ Ordenado por source_updated_at DESC
|
|
97
|
+
→ Mapeia source_updated_at → updated_at na saida
|
|
98
|
+
|
|
99
|
+
Response: { properties: [{ id, reference, updated_at }, ...] }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Proposito:** Integrador faz GET listing → compara updated_at com dados do CRM → envia PUT apenas com os que mudaram. Evita re-sincronizar tudo.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Fluxo DELETE
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
DELETE /api/{entity}/sync (x-api-key header)
|
|
110
|
+
Body: { "refs": ["ABC-123", "DEF-456"] }
|
|
111
|
+
(property usa "refs", broker usa "keys")
|
|
112
|
+
|
|
113
|
+
→ Validacao: array nao vazio
|
|
114
|
+
→ prisma.{entity}.deleteMany({ where: { reference: { in: refs } } })
|
|
115
|
+
|
|
116
|
+
Response: { success: true, deleted: N, references: [...] }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Pipeline de Transformacao
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Dados do CRM (formato CRM)
|
|
125
|
+
↓
|
|
126
|
+
Pacote CRM (@horizon-integrations/{crm}-crm) ← FORA da API, no automations
|
|
127
|
+
Converte formato CRM → formato Horizon
|
|
128
|
+
↓
|
|
129
|
+
PUT /api/{entity}/sync ← ENTRADA NA API
|
|
130
|
+
↓
|
|
131
|
+
source-schema.zod.ts ← Validacao Zod rigorosa
|
|
132
|
+
↓
|
|
133
|
+
dataModifier (mappers/dataModifier/) ← Regras de negocio do CLIENTE
|
|
134
|
+
ObjectTransformer com rules.ts
|
|
135
|
+
Pode estar VAZIO (passthrough)
|
|
136
|
+
↓
|
|
137
|
+
computedFields (mappers/computedFields/) ← Campos CALCULADOS
|
|
138
|
+
seo_slug_gen, thumbnails, endereco_completo,
|
|
139
|
+
tipo_gen, subtipo_gen, has_video, has_tour,
|
|
140
|
+
financiavel, first_synced_at
|
|
141
|
+
↓
|
|
142
|
+
stripUnknownFields() ← Protecao do Prisma
|
|
143
|
+
Filtra via Prisma.{Entity}ScalarFieldEnum
|
|
144
|
+
↓
|
|
145
|
+
sync_raw_data (JSONB) ← Snapshot completo
|
|
146
|
+
↓
|
|
147
|
+
Prisma $transaction (delete + insert) ← Persistencia
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Protecao de Rotas
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// route.ts — padrao para todo modulo
|
|
156
|
+
export const { OPTIONS, GET, PUT, DELETE } = withApiKeyProtection({
|
|
157
|
+
GET: ControllerGET, // Publico (CORS *)
|
|
158
|
+
PUT: ControllerPUT, // Protegido (x-api-key)
|
|
159
|
+
DELETE: ControllerDELETE // Protegido (x-api-key)
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Diferencas Property vs Broker
|
|
166
|
+
|
|
167
|
+
| Aspecto | Property | Broker |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| Chave unica | reference | key |
|
|
170
|
+
| Validacao Zod | Rigorosa (schema CRM) | Flexivel (.passthrough()) |
|
|
171
|
+
| dataModifier | ObjectTransformer com rules | Funcao simples |
|
|
172
|
+
| computedFields | 7+ campos | Poucos (slug, timestamps) |
|
|
173
|
+
| stripUnknownFields | Sim | Nao |
|
|
174
|
+
| sync_raw_data | Sim (JSONB) | Nao |
|
|
175
|
+
| first_synced_at | Sim (preservado) | Nao |
|
|
176
|
+
| GET source_key | Sim | Nao (ainda) |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Multi-Source Sync — Padrao source_key
|
|
181
|
+
|
|
182
|
+
### O Problema
|
|
183
|
+
|
|
184
|
+
Uma integracao pode ter MULTIPLAS fontes de dados para a mesma entidade:
|
|
185
|
+
|
|
186
|
+
| source_key | Fonte | Descricao |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| si9-api-imoveis | SI9 CRM | Imoveis proprios da imobiliaria |
|
|
189
|
+
| nifb-vrsync-imoveis | VRSync/NIFB | Imoveis do Nucleo de Imoveis |
|
|
190
|
+
|
|
191
|
+
Sem source_key, ao sincronizar imoveis do SI9, o deleteMany poderia deletar imoveis do NIFB.
|
|
192
|
+
|
|
193
|
+
### Regra Obrigatoria
|
|
194
|
+
|
|
195
|
+
**Toda integracao que enviar dados via PUT /sync DEVE incluir source_key em cada item.**
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"properties": [
|
|
200
|
+
{
|
|
201
|
+
"source_key": "si9-api-imoveis",
|
|
202
|
+
"reference": "ABC-123",
|
|
203
|
+
"title": "Casa 3 quartos"
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Onde o source_key atua
|
|
210
|
+
|
|
211
|
+
**PUT (Upsert):** Cada item chega com source_key preenchido, salvo no banco.
|
|
212
|
+
|
|
213
|
+
**GET (Listing):** Integrador filtra apenas seus registros:
|
|
214
|
+
```
|
|
215
|
+
GET /api/property/sync?source_key=si9-api-imoveis → So imoveis do SI9
|
|
216
|
+
GET /api/property/sync?source_key=nifb-vrsync-imoveis → So imoveis do NIFB
|
|
217
|
+
GET /api/property/sync → Sem filtro: retorna TODOS
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**DELETE — Scoping por fonte (EVOLUCAO NECESSARIA):**
|
|
221
|
+
Estado atual: DELETE opera por references SEM filtrar por source_key.
|
|
222
|
+
Evolucao: DELETE deveria aceitar source_key como filtro adicional:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// Evolucao: DELETE com scoping por source
|
|
226
|
+
await tx.property.deleteMany({
|
|
227
|
+
where: {
|
|
228
|
+
reference: { in: refs },
|
|
229
|
+
source_key: sourceKey // ← Protecao: so deleta da mesma fonte
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Upsert Transaction — Scoping (EVOLUCAO NECESSARIA):**
|
|
235
|
+
Estado atual: deleteMany na transaction deleta por reference sem filtrar source_key.
|
|
236
|
+
Evolucao: Adicionar source_key no where:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const sourceKey = preparedImoveis[0]?.source_key;
|
|
240
|
+
await tx.property.deleteMany({
|
|
241
|
+
where: {
|
|
242
|
+
reference: { in: references },
|
|
243
|
+
...(sourceKey && { source_key: sourceKey })
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Nomenclatura padrao de source_key
|
|
249
|
+
|
|
250
|
+
Formato: `{crm}-{api|sync|vrsync}-{entidade-plural}`
|
|
251
|
+
|
|
252
|
+
Exemplos:
|
|
253
|
+
- `si9-api-imoveis`
|
|
254
|
+
- `jetimob-api-imoveis`
|
|
255
|
+
- `nifb-vrsync-imoveis`
|
|
256
|
+
- `si9-api-corretores`
|
|
257
|
+
- `manual-import-imoveis` (importacoes manuais)
|
|
258
|
+
|
|
259
|
+
### Ao criar modulo com multi-source
|
|
260
|
+
|
|
261
|
+
1. Banco: adicionar coluna `source_key TEXT`
|
|
262
|
+
2. Prisma: campo `source_key String?`
|
|
263
|
+
3. entity-schema.ts: campo com `filterable: true` e enum das fontes
|
|
264
|
+
4. Controller GET: aceitar `?source_key=` como query param
|
|
265
|
+
5. Service listing: filtrar `where: { source_key }` quando presente
|
|
266
|
+
6. Service upsert: garantir que source_key vem no payload e e persistido
|
|
267
|
+
7. Service delete: usar source_key como filtro adicional no deleteMany
|
|
268
|
+
8. Automations: cada integracao DEVE enviar source_key
|
package/package.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@horizon-framework/api-nextjs-docs",
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "Documentacao do backend generico Horizon — sync, search engine, SSOT, batch totals",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
}
|
|
10
|
+
}
|