@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.
@@ -0,0 +1,1038 @@
1
+ # Search Engine API - Motor Genérico de Busca
2
+
3
+ > **Motor de busca reutilizável** que funciona IDENTICAMENTE para qualquer entidade.
4
+ >
5
+ > **Implementado atualmente em:**
6
+ > - `Property` (Imóveis) → `/api/property/search`
7
+ >
8
+ > **Futuramente:**
9
+ > - 🔜 `Broker` (Corretores) → `/api/broker/search`
10
+ > - 🔜 `Condominium` (Condomínios) → `/api/condominium/search`
11
+ > - 🔜 Qualquer outra entidade...
12
+ >
13
+ > **A estrutura de request/response é IDÊNTICA** - só muda o endpoint e os campos disponíveis.
14
+
15
+ ---
16
+
17
+ ## Quick Decision Tree
18
+
19
+ ```
20
+ Preciso de...
21
+ ├─ Scroll infinito (mobile/feed)? → CURSOR pagination
22
+ ├─ Botões de página (1, 2, 3...)? → OFFSET pagination
23
+ ├─ Filtros checkbox (múltipla seleção)? → FACETS com operator='or'
24
+ ├─ Filtros hierárquicos (intersecção)? → FACETS com operator='and'
25
+ └─ Apenas contagem rápida? → meta: {}
26
+ ```
27
+
28
+ ---
29
+
30
+ ## CURSOR vs OFFSET - DECISÃO CRÍTICA
31
+
32
+ ### **Como a API detecta o modo?**
33
+
34
+ ```typescript
35
+ // CURSOR MODE (infinite scroll)
36
+ {
37
+ list: {
38
+ limit: 20,
39
+ // SEM 'page' = CURSOR MODE
40
+ cursor: "123" // opcional - só na 2ª+ requisição
41
+ }
42
+ }
43
+
44
+ // OFFSET MODE (paginação tradicional)
45
+ {
46
+ list: {
47
+ page: 1, // COM 'page' = OFFSET MODE
48
+ limit: 20
49
+ }
50
+ }
51
+ ```
52
+
53
+ **Regra:** `Se request.list.page === undefined → CURSOR mode`
54
+
55
+ ### **Comparação Rápida**
56
+
57
+ | Feature | CURSOR | OFFSET |
58
+ |---------|---------|---------|
59
+ | **Use quando** | Scroll infinito, feeds | Botões de página (1, 2, 3) |
60
+ | **Performance** | Excelente | Degrada com páginas altas |
61
+ | **Request** | `{ limit, cursor }` | `{ page, limit }` |
62
+ | **Response** | `{ nextCursor, hasNextPage }` | `{ page, total, totalPages, hasNextPage }` |
63
+ | **Vantagens** | Rápido, consistente | Pode pular páginas, mostra total |
64
+ | **Desvantagens** | Não mostra total, só avança | Lento em páginas altas (ex: página 100) |
65
+
66
+ ---
67
+
68
+ ## SCROLL INFINITO (Cursor-based Pagination)
69
+
70
+ ### **1ª Requisição (inicial)**
71
+
72
+ ```json
73
+ POST /api/property/search
74
+ {
75
+ "filters": {
76
+ "tipo": "Apartamento"
77
+ },
78
+ "list": {
79
+ "limit": 20
80
+ // NÃO enviar 'page'
81
+ // NÃO enviar 'cursor' na primeira vez
82
+ }
83
+ }
84
+ ```
85
+
86
+ **Response:**
87
+ ```json
88
+ {
89
+ "results": {
90
+ "list": {
91
+ "data": [
92
+ { "id": 101, "title": "Apto 1..." },
93
+ { "id": 102, "title": "Apto 2..." },
94
+ // ... 18 mais
95
+ { "id": 120, "title": "Apto 20..." } // ← último item
96
+ ],
97
+ "meta": {
98
+ "limit": 20,
99
+ "hasNextPage": true,
100
+ "nextCursor": 120 // ← ID do último item
101
+ }
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ ### **2ª+ Requisições (scroll)**
108
+
109
+ ```json
110
+ POST /api/property/search
111
+ {
112
+ "filters": {
113
+ "tipo": "Apartamento" // MESMOS filtros
114
+ },
115
+ "list": {
116
+ "limit": 20,
117
+ "cursor": 120 // ← nextCursor da resposta anterior
118
+ }
119
+ }
120
+ ```
121
+
122
+ **Response:**
123
+ ```json
124
+ {
125
+ "results": {
126
+ "list": {
127
+ "data": [
128
+ { "id": 121, "title": "Apto 21..." },
129
+ // ... mais 19
130
+ { "id": 140, "title": "Apto 40..." }
131
+ ],
132
+ "meta": {
133
+ "limit": 20,
134
+ "hasNextPage": true,
135
+ "nextCursor": 140 // ← novo cursor
136
+ }
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ### **Última Requisição (fim dos dados)**
143
+
144
+ ```json
145
+ {
146
+ "results": {
147
+ "list": {
148
+ "data": [
149
+ { "id": 141, "title": "Apto 41..." },
150
+ { "id": 142, "title": "Apto 42..." } // só 2 itens restantes
151
+ ],
152
+ "meta": {
153
+ "limit": 20,
154
+ "hasNextPage": false, // ← SEM próxima página
155
+ "nextCursor": undefined // ← SEM cursor
156
+ }
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### **Implementação Frontend - Checklist**
163
+
164
+ ```typescript
165
+ // Estado
166
+ const [items, setItems] = useState([]);
167
+ const [cursor, setCursor] = useState(undefined);
168
+ const [hasMore, setHasMore] = useState(true);
169
+
170
+ // Primeira carga
171
+ async function loadInitial() {
172
+ const response = await fetch('/api/property/search', {
173
+ method: 'POST',
174
+ body: JSON.stringify({
175
+ filters: { tipo: "Apartamento" },
176
+ list: { limit: 20 } // SEM cursor, SEM page
177
+ })
178
+ });
179
+
180
+ const data = await response.json();
181
+ setItems(data.results.list.data);
182
+ setCursor(data.results.list.meta.nextCursor);
183
+ setHasMore(data.results.list.meta.hasNextPage);
184
+ }
185
+
186
+ // Scroll infinito
187
+ async function loadMore() {
188
+ if (!hasMore) return;
189
+
190
+ const response = await fetch('/api/property/search', {
191
+ method: 'POST',
192
+ body: JSON.stringify({
193
+ filters: { tipo: "Apartamento" }, // MESMOS filtros
194
+ list: {
195
+ limit: 20,
196
+ cursor: cursor // ← cursor da última resposta
197
+ }
198
+ })
199
+ });
200
+
201
+ const data = await response.json();
202
+ setItems(prev => [...prev, ...data.results.list.data]); // append
203
+ setCursor(data.results.list.meta.nextCursor);
204
+ setHasMore(data.results.list.meta.hasNextPage);
205
+ }
206
+
207
+ // Quando filtros mudam - RESETAR
208
+ function onFiltersChange(newFilters) {
209
+ setItems([]);
210
+ setCursor(undefined); // ← CRÍTICO: resetar cursor
211
+ setHasMore(true);
212
+ // carregar com novos filtros
213
+ }
214
+ ```
215
+
216
+ ### **ERROS COMUNS - Scroll Infinito**
217
+
218
+ #### **Erro 1: Enviando `page` junto com `cursor`**
219
+ ```json
220
+ // ERRADO
221
+ {
222
+ "list": {
223
+ "page": 1, // ← Remove isso!
224
+ "cursor": 120
225
+ }
226
+ }
227
+
228
+ // CORRETO
229
+ {
230
+ "list": {
231
+ "cursor": 120 // Só cursor
232
+ }
233
+ }
234
+ ```
235
+
236
+ #### **Erro 2: Não resetar cursor ao mudar filtros**
237
+ ```typescript
238
+ // ERRADO - vai trazer dados errados
239
+ function onFilterChange() {
240
+ // cursor ainda tem valor 120 dos filtros antigos!
241
+ loadMore(); // Dados misturados
242
+ }
243
+
244
+ // CORRETO
245
+ function onFilterChange() {
246
+ setCursor(undefined); // Reset
247
+ setItems([]);
248
+ loadInitial(); // Nova busca do zero
249
+ }
250
+ ```
251
+
252
+ #### **Erro 3: Cursor na primeira requisição**
253
+ ```json
254
+ // ERRADO
255
+ {
256
+ "list": {
257
+ "cursor": null // ← Não enviar
258
+ }
259
+ }
260
+
261
+ // CORRETO - primeira vez
262
+ {
263
+ "list": {
264
+ "limit": 20
265
+ // sem cursor
266
+ }
267
+ }
268
+ ```
269
+
270
+ ---
271
+
272
+ ## PAGINAÇÃO TRADICIONAL (Offset-based)
273
+
274
+ ### **Request**
275
+ ```json
276
+ POST /api/property/search
277
+ {
278
+ "filters": {
279
+ "tipo": "Casa"
280
+ },
281
+ "list": {
282
+ "page": 2, // ← Presença de 'page' ativa OFFSET mode
283
+ "limit": 20
284
+ }
285
+ }
286
+ ```
287
+
288
+ ### **Response**
289
+ ```json
290
+ {
291
+ "results": {
292
+ "list": {
293
+ "data": [...],
294
+ "meta": {
295
+ "page": 2,
296
+ "limit": 20,
297
+ "total": 156, // total de resultados
298
+ "totalPages": 8, // total de páginas
299
+ "hasNextPage": true,
300
+ "hasPrevPage": true
301
+ }
302
+ }
303
+ }
304
+ }
305
+ ```
306
+
307
+ ---
308
+
309
+ ## FACETS - Filtros Dinâmicos
310
+
311
+ ### **O que são Facets?**
312
+
313
+ Facets retornam **valores possíveis** para campos + **contagens**, permitindo criar filtros dinâmicos tipo:
314
+
315
+ ```
316
+ □ Apartamento (450)
317
+ □ Casa (280)
318
+ □ Terreno (89)
319
+ ```
320
+
321
+ ### **Facets Operators - Quando usar cada um**
322
+
323
+ #### **1. `operator: 'or'` - Checkbox (múltipla seleção)**
324
+
325
+ **Use quando:** Usuário pode selecionar múltiplas opções **do mesmo campo**
326
+
327
+ **Comportamento:** Remove filtro do campo para mostrar **todas opções disponíveis**
328
+
329
+ ```json
330
+ // Request - usuário JÁ filtrou tipo=Apartamento
331
+ {
332
+ "filters": {
333
+ "tipo": "Apartamento" // ← filtro aplicado
334
+ },
335
+ "facets": {
336
+ "fields": [
337
+ {
338
+ "type": "terms",
339
+ "field": "tipo",
340
+ "operator": "or" // ← Remove filtro de 'tipo' só para facet
341
+ }
342
+ ]
343
+ }
344
+ }
345
+
346
+ // Response - mostra TODAS opções (não só Apartamento)
347
+ {
348
+ "facets": {
349
+ "data": {
350
+ "tipo": [
351
+ { "value": "Apartamento", "count": 450 }, // ← filtrado
352
+ { "value": "Casa", "count": 280 }, // ← também aparece
353
+ { "value": "Terreno", "count": 89 } // ← também aparece
354
+ ]
355
+ }
356
+ }
357
+ }
358
+ ```
359
+
360
+ **UI típica:**
361
+ ```
362
+ Tipo de Imóvel:
363
+ ☑ Apartamento (450) ← selecionado
364
+ □ Casa (280) ← pode selecionar também
365
+ □ Terreno (89) ← pode selecionar também
366
+ ```
367
+
368
+ #### **2. `operator: 'and'` - Intersecção (arrays)**
369
+
370
+ **Use quando:** Valores são arrays e você quer mostrar **intersecções**
371
+
372
+ **Comportamento:** Mantém filtro, mostra quais outros valores coexistem
373
+
374
+ ```json
375
+ // Request - usuário filtrou características=["piscina"]
376
+ {
377
+ "filters": {
378
+ "caracteristicas": ["piscina"] // ← filtro aplicado
379
+ },
380
+ "facets": {
381
+ "fields": [
382
+ {
383
+ "type": "terms",
384
+ "field": "caracteristicas",
385
+ "operator": "and" // ← Mantém filtro, mostra intersecções
386
+ }
387
+ ]
388
+ }
389
+ }
390
+
391
+ // Response - mostra características que TÊM piscina
392
+ {
393
+ "facets": {
394
+ "data": {
395
+ "caracteristicas": [
396
+ { "value": "piscina", "count": 120 }, // ← filtrado
397
+ { "value": "churrasqueira", "count": 80 }, // ← 80 imóveis têm AMBOS
398
+ { "value": "academia", "count": 45 } // ← 45 imóveis têm AMBOS
399
+ ]
400
+ }
401
+ }
402
+ }
403
+ ```
404
+
405
+ **UI típica:**
406
+ ```
407
+ Imóveis com piscina também têm:
408
+ ☑ Piscina (120)
409
+ □ Churrasqueira (80) ← 80 imóveis têm piscina + churrasqueira
410
+ □ Academia (45) ← 45 imóveis têm piscina + academia
411
+ ```
412
+
413
+ #### **3. `operator: 'equals'` - Filtro único**
414
+
415
+ **Use quando:** Usuário pode selecionar **apenas 1 opção** (radio button)
416
+
417
+ **Comportamento:** Mantém filtro completo, mostra **só o selecionado**
418
+
419
+ ```json
420
+ // Request
421
+ {
422
+ "filters": {
423
+ "tipo": "Apartamento"
424
+ },
425
+ "facets": {
426
+ "fields": [
427
+ {
428
+ "type": "terms",
429
+ "field": "tipo",
430
+ "operator": "equals" // ← Mantém filtro
431
+ }
432
+ ]
433
+ }
434
+ }
435
+
436
+ // Response - mostra SÓ o filtrado
437
+ {
438
+ "facets": {
439
+ "data": {
440
+ "tipo": [
441
+ { "value": "Apartamento", "count": 450 } // ← só este
442
+ ]
443
+ }
444
+ }
445
+ }
446
+ ```
447
+
448
+ ### **Range Facets - Faixas Numéricas**
449
+
450
+ #### **Buckets Automáticos**
451
+ ```json
452
+ {
453
+ "facets": {
454
+ "fields": [
455
+ {
456
+ "type": "range",
457
+ "field": "valor_venda",
458
+ "bucketCount": 4 // gera 4 faixas automaticamente
459
+ }
460
+ ]
461
+ }
462
+ }
463
+ ```
464
+
465
+ **Response:**
466
+ ```json
467
+ {
468
+ "facets": {
469
+ "data": {
470
+ "valor_venda": [
471
+ { "from": 0, "to": 300000, "count": 150 },
472
+ { "from": 300000, "to": 600000, "count": 280 },
473
+ { "from": 600000, "to": 1000000, "count": 100 },
474
+ { "from": 1000000, "to": 5000000, "count": 39 }
475
+ ]
476
+ }
477
+ }
478
+ }
479
+ ```
480
+
481
+ #### **Buckets Customizados (com labels)**
482
+ ```json
483
+ {
484
+ "facets": {
485
+ "fields": [
486
+ {
487
+ "type": "range",
488
+ "field": "valor_venda",
489
+ "buckets": [
490
+ { "from": 0, "to": 300000, "label": "Econômico" },
491
+ { "from": 300000, "to": 700000, "label": "Médio" },
492
+ { "from": 700000, "to": 1500000, "label": "Alto" },
493
+ { "from": 1500000, "to": 999999999, "label": "Premium" }
494
+ ]
495
+ }
496
+ ]
497
+ }
498
+ }
499
+ ```
500
+
501
+ **Response:**
502
+ ```json
503
+ {
504
+ "facets": {
505
+ "data": {
506
+ "valor_venda": [
507
+ { "from": 0, "to": 300000, "label": "Econômico", "count": 150 },
508
+ { "from": 300000, "to": 700000, "label": "Médio", "count": 280 },
509
+ { "from": 700000, "to": 1500000, "label": "Alto", "count": 100 },
510
+ { "from": 1500000, "to": 999999999, "label": "Premium", "count": 39 }
511
+ ]
512
+ }
513
+ }
514
+ }
515
+ ```
516
+
517
+ ### **Range Facets - Comportamento Especial**
518
+
519
+ **Range facets SEMPRE removem o filtro do campo** para mostrar todos os buckets disponíveis.
520
+
521
+ ```json
522
+ // Request - usuário já filtrou valor_venda
523
+ {
524
+ "filters": {
525
+ "valor_venda": { "gte": 300000, "lte": 700000 } // ← filtro aplicado
526
+ },
527
+ "facets": {
528
+ "fields": [
529
+ {
530
+ "type": "range",
531
+ "field": "valor_venda",
532
+ "bucketCount": 4
533
+ }
534
+ ]
535
+ }
536
+ }
537
+
538
+ // Response - mostra TODOS os buckets (não só 300k-700k)
539
+ {
540
+ "facets": {
541
+ "data": {
542
+ "valor_venda": [
543
+ { "from": 0, "to": 300000, "count": 150 }, // ← aparece
544
+ { "from": 300000, "to": 600000, "count": 280 }, // ← filtrado
545
+ { "from": 600000, "to": 1000000, "count": 100 }, // ← aparece
546
+ { "from": 1000000, "to": 5000000, "count": 39 } // ← aparece
547
+ ]
548
+ }
549
+ }
550
+ }
551
+ ```
552
+
553
+ **Por quê?** Para permitir que usuário veja outras faixas e mude a seleção.
554
+
555
+ ---
556
+
557
+ ## Casos de Uso Completos
558
+
559
+ ### **1. Scroll Infinito + Facets**
560
+ ```json
561
+ POST /api/property/search
562
+ {
563
+ "filters": {
564
+ "tipo": "Apartamento"
565
+ },
566
+ "list": {
567
+ "limit": 20,
568
+ "cursor": 120 // omitir na primeira vez
569
+ },
570
+ "facets": {
571
+ "fields": [
572
+ { "type": "terms", "field": "dormitorios" },
573
+ { "type": "range", "field": "valor_venda", "bucketCount": 4 }
574
+ ]
575
+ }
576
+ }
577
+ ```
578
+
579
+ ### **2. Paginação + Total + Mapa**
580
+ ```json
581
+ POST /api/property/search
582
+ {
583
+ "filters": {
584
+ "operacao": { "or": ["venda", "locacao"] },
585
+ "endereco_cidade": "Torres"
586
+ },
587
+ "list": {
588
+ "page": 1,
589
+ "limit": 20,
590
+ "select": {
591
+ "reference": true,
592
+ "title": true,
593
+ "valor_venda": true
594
+ }
595
+ },
596
+ "map": {
597
+ "select": {
598
+ "reference": true,
599
+ "lat": true,
600
+ "lng": true
601
+ },
602
+ "limit": 100
603
+ },
604
+ "meta": {}
605
+ }
606
+ ```
607
+
608
+ ### **3. Full Text Search + Filtros Geo**
609
+ ```json
610
+ POST /api/property/search
611
+ {
612
+ "fts": {
613
+ "value": "apartamento centro piscina",
614
+ "operator": "websearch"
615
+ },
616
+ "geom": {
617
+ "operator": "within",
618
+ "value": {
619
+ "type": "bbox",
620
+ "bounds": {
621
+ "north": -29.3,
622
+ "south": -29.4,
623
+ "east": -49.65,
624
+ "west": -49.8
625
+ }
626
+ }
627
+ },
628
+ "list": {
629
+ "limit": 20
630
+ }
631
+ }
632
+ ```
633
+
634
+ ### **4. Apenas Contagem (super rápido)**
635
+ ```json
636
+ POST /api/property/search
637
+ {
638
+ "filters": {
639
+ "tipo": "Apartamento",
640
+ "dormitorios": { "gte": 2 }
641
+ },
642
+ "meta": {}
643
+ }
644
+
645
+ // Response em ~15ms
646
+ {
647
+ "results": {
648
+ "meta": { "total": 691 }
649
+ },
650
+ "metadata": {
651
+ "executionTime": 15
652
+ }
653
+ }
654
+ ```
655
+
656
+ ### **5. Batch Search (múltiplas queries paralelas)**
657
+ ```json
658
+ POST /api/property/batch-search
659
+ {
660
+ "queries": [
661
+ {
662
+ "key": "apt-2q",
663
+ "filters": { "tipo": "Apartamento", "dormitorios": 2 },
664
+ "meta": {}
665
+ },
666
+ {
667
+ "key": "apt-3q",
668
+ "filters": { "tipo": "Apartamento", "dormitorios": 3 },
669
+ "meta": {}
670
+ },
671
+ {
672
+ "key": "casas",
673
+ "filters": { "tipo": "Casa" },
674
+ "list": { "limit": 5 }
675
+ }
676
+ ]
677
+ }
678
+
679
+ // Response com resultados separados por key
680
+ {
681
+ "results": {
682
+ "apt-2q": {
683
+ "results": { "meta": { "total": 321 } },
684
+ "metadata": { "executionTime": 30 }
685
+ },
686
+ "apt-3q": {
687
+ "results": { "meta": { "total": 370 } },
688
+ "metadata": { "executionTime": 28 }
689
+ },
690
+ "casas": {
691
+ "results": { "list": {...}, "meta": {...} },
692
+ "metadata": { "executionTime": 45 }
693
+ }
694
+ }
695
+ }
696
+ ```
697
+
698
+ ---
699
+
700
+ ## TypeScript Schemas
701
+
702
+ ### **SearchRequest**
703
+ ```typescript
704
+ interface SearchRequest {
705
+ // Filtros
706
+ filters?: Record<string, FilterValue>;
707
+
708
+ // Full Text Search
709
+ fts?: {
710
+ value: string;
711
+ operator?: 'websearch' | 'plainto' | 'phrase' | 'boolean';
712
+ };
713
+
714
+ // Busca geográfica
715
+ geom?: {
716
+ operator: 'within' | 'intersects' | 'near';
717
+ value: {
718
+ type: 'bbox' | 'circle' | 'polygon';
719
+ bounds?: { north: number; south: number; east: number; west: number; };
720
+ center?: { lat: number; lng: number; };
721
+ radius?: number;
722
+ coordinates?: number[][];
723
+ };
724
+ };
725
+
726
+ // Serviços (escolha quais quer)
727
+ list?: {
728
+ // Offset-based
729
+ page?: number;
730
+ limit?: number;
731
+
732
+ // Cursor-based (mutuamente exclusivo com page)
733
+ cursor?: string | number;
734
+
735
+ // Comum
736
+ select?: Record<string, any>;
737
+ include?: Record<string, any>;
738
+ sort?: Record<string, 'asc' | 'desc'>;
739
+ };
740
+
741
+ map?: {
742
+ select?: Record<string, any>;
743
+ limit?: number;
744
+ bounds?: { north: number; south: number; east: number; west: number; };
745
+ };
746
+
747
+ meta?: {};
748
+
749
+ facets?: {
750
+ includeCount?: boolean;
751
+ fields: Array<{
752
+ type: 'terms' | 'range';
753
+ field: string;
754
+ operator?: 'or' | 'and' | 'equals';
755
+ buckets?: number[] | Array<{ from: number; to: number; label: string; }>;
756
+ bucketCount?: number;
757
+ }>;
758
+ };
759
+ }
760
+ ```
761
+
762
+ ### **SearchResponse**
763
+ ```typescript
764
+ interface SearchResponse {
765
+ results: {
766
+ list?: {
767
+ data: any[];
768
+ meta: {
769
+ limit: number;
770
+ hasNextPage: boolean;
771
+
772
+ // Offset-based
773
+ page?: number;
774
+ total?: number;
775
+ totalPages?: number;
776
+ hasPrevPage?: boolean;
777
+
778
+ // Cursor-based
779
+ nextCursor?: string | number;
780
+ };
781
+ };
782
+
783
+ map?: {
784
+ data: any[];
785
+ meta: {
786
+ totalWithCoordinates: number;
787
+ };
788
+ };
789
+
790
+ meta?: {
791
+ total: number;
792
+ };
793
+
794
+ facets?: {
795
+ data: Record<string, Array<{
796
+ // Terms facet
797
+ value?: any;
798
+ label?: string;
799
+ count?: number;
800
+
801
+ // Range facet
802
+ from?: number;
803
+ to?: number;
804
+ }>>;
805
+ };
806
+ };
807
+
808
+ metadata: {
809
+ executionTime: number;
810
+ };
811
+ }
812
+ ```
813
+
814
+ ### **FilterValue**
815
+ ```typescript
816
+ type FilterValue =
817
+ | string // Valor simples
818
+ | number
819
+ | boolean
820
+ | { or: any[] } // OR lógico
821
+ | { and: any[] } // AND lógico
822
+ | { gte: number } // Maior ou igual
823
+ | { lte: number } // Menor ou igual
824
+ | { gt: number } // Maior que
825
+ | { lt: number } // Menor que
826
+ | { between: [number, number] } // Entre valores
827
+ ```
828
+
829
+ ---
830
+
831
+ ## Campos Disponíveis (Property)
832
+
833
+ ### **Principais campos para filtros/select**
834
+ ```typescript
835
+ {
836
+ // Identificação
837
+ reference: string;
838
+ title: string;
839
+ description: string;
840
+
841
+ // Comercial
842
+ operacao: string[]; // ["venda", "locacao", "temporada"]
843
+ tipo: string; // "Apartamento", "Casa", etc.
844
+ subtipo: string;
845
+ valor_venda: number;
846
+ valor_locacao: number;
847
+ valor_diaria: number;
848
+
849
+ // Estrutura
850
+ area_total: number;
851
+ dormitorios: number;
852
+ suites: number;
853
+ banheiros: number;
854
+ vagas_garagem: number;
855
+
856
+ // Localização
857
+ endereco_cidade: string;
858
+ endereco_bairro: string;
859
+ lat: number;
860
+ lng: number;
861
+
862
+ // Características
863
+ caracteristicas: string[];
864
+ mobiliado: boolean;
865
+
866
+ // Sistema
867
+ created_at: string;
868
+ updated_at: string;
869
+ }
870
+ ```
871
+
872
+ ---
873
+
874
+ ## Reference Rápida
875
+
876
+ ### **Operadores de Filtro**
877
+ | Operador | Exemplo |
878
+ |----------|---------|
879
+ | Simples | `"tipo": "Apartamento"` |
880
+ | OR | `"operacao": { "or": ["venda", "locacao"] }` |
881
+ | AND | `"caracteristicas": { "and": ["piscina", "churrasqueira"] }` |
882
+ | GTE | `"dormitorios": { "gte": 2 }` |
883
+ | LTE | `"valor_venda": { "lte": 500000 }` |
884
+ | BETWEEN | `"valor_venda": { "between": [300000, 800000] }` |
885
+
886
+ ### **Endpoints Disponíveis**
887
+ ```
888
+ POST /api/property/search # Busca multi-parte
889
+ POST /api/property/batch-search # Múltiplas buscas paralelas
890
+ ```
891
+
892
+ ### **Teste Rápido (curl)**
893
+ ```bash
894
+ curl -X POST http://localhost:5000/api/property/search \
895
+ -H "Content-Type: application/json" \
896
+ -d '{
897
+ "filters": { "tipo": "Apartamento" },
898
+ "list": { "limit": 5 },
899
+ "meta": {}
900
+ }'
901
+ ```
902
+
903
+ ---
904
+
905
+ ## Checklist Final
906
+
907
+ Antes de fazer request, verifique:
908
+
909
+ **Paginação:**
910
+ - [ ] Decidiu cursor vs offset?
911
+ - [ ] Cursor: NÃO enviar `page`
912
+ - [ ] Offset: SEMPRE enviar `page`
913
+ - [ ] Cursor: resetar ao mudar filtros
914
+
915
+ **Facets:**
916
+ - [ ] Checkbox múltipla? → `operator: 'or'`
917
+ - [ ] Intersecção (arrays)? → `operator: 'and'`
918
+ - [ ] Radio button (1 opção)? → `operator: 'equals'`
919
+ - [ ] Range: vai mostrar TODOS os buckets
920
+
921
+ **Geral:**
922
+ - [ ] Content-Type: `application/json`
923
+ - [ ] Campos válidos da entidade
924
+ - [ ] Operadores corretos (`or`, `and`, `gte`, `lte`)
925
+
926
+ ---
927
+
928
+ ## RELATIONS - Campos Virtualizados
929
+
930
+ ### **O que são Relations?**
931
+
932
+ Relations são campos que representam **relacionamentos entre entidades** definidos no Entity Schema. Por exemplo:
933
+ - Property → Broker (imóvel pertence a um corretor)
934
+ - Broker → Properties (corretor tem vários imóveis)
935
+
936
+ ### **Definindo Relations no Entity Schema**
937
+
938
+ ```typescript
939
+ // apps/api/src/modules/property/schemas/entity-schema.ts
940
+ {
941
+ key: "broker",
942
+ type: "Relation",
943
+ categories: ["relations"],
944
+ relation: {
945
+ model: "Broker", // Nome do modelo Prisma
946
+ field: "key", // Campo no modelo relacionado
947
+ labelField: "name", // Campo para exibição
948
+ displayTemplate: "{{name}}",
949
+ foreignKey: "corretor_key" // FK nesta entidade
950
+ },
951
+ ui: { label: "Corretor" },
952
+ audit: { origin: "local-relation" }
953
+ }
954
+
955
+ // apps/api/src/modules/broker/schemas/entity-schema.ts
956
+ {
957
+ key: "properties",
958
+ type: "Relation",
959
+ categories: ["relations"],
960
+ relation: {
961
+ model: "Property",
962
+ field: "corretor_key", // Campo no Property que referencia broker
963
+ labelField: "title",
964
+ displayTemplate: "{{title}}"
965
+ },
966
+ ui: { label: "Imóveis do Corretor" },
967
+ audit: { origin: "local-relation" }
968
+ }
969
+ ```
970
+
971
+ ### **resolveVirtualFilters - Filtros Virtuais**
972
+
973
+ O sistema suporta filtros em campos virtuais (Relations) através do `resolveVirtualFilters`.
974
+
975
+ **Exemplo de uso:**
976
+ ```json
977
+ POST /api/property/search
978
+ {
979
+ "filters": {
980
+ "broker": "joao-silva" // Filtro virtual pelo slug do corretor
981
+ }
982
+ }
983
+ ```
984
+
985
+ **Como funciona internamente:**
986
+ 1. Search Engine detecta `broker` como campo Relation
987
+ 2. `resolveVirtualFilters` transforma em query real
988
+ 3. Busca broker pelo slug/key
989
+ 4. Converte para filtro: `{ corretor_key: "key-do-broker" }`
990
+
991
+ ### **Facets em Relations**
992
+
993
+ Relations também suportam facets para criar filtros dinâmicos:
994
+
995
+ ```json
996
+ {
997
+ "facets": {
998
+ "fields": [
999
+ {
1000
+ "type": "terms",
1001
+ "field": "broker",
1002
+ "operator": "or"
1003
+ }
1004
+ ]
1005
+ }
1006
+ }
1007
+
1008
+ // Response
1009
+ {
1010
+ "facets": {
1011
+ "data": {
1012
+ "broker": [
1013
+ { "value": "joao-silva", "label": "João Silva", "count": 45 },
1014
+ { "value": "maria-santos", "label": "Maria Santos", "count": 32 }
1015
+ ]
1016
+ }
1017
+ }
1018
+ }
1019
+ ```
1020
+
1021
+ ---
1022
+
1023
+ ## Arquivos de Referência
1024
+
1025
+ ### **Código Fonte**
1026
+ - **Controller**: `/apps/api/src/modules/property/controllers/search.controller.ts`
1027
+ - **Service**: `/apps/api/src/modules/property/services/search.service.ts`
1028
+ - **Types**: `/apps/api/src/lib/advancedSearch/types.ts`
1029
+ - **Cursor Pagination**: `/apps/api/src/lib/advancedSearch/executeList.ts` (linha 46)
1030
+
1031
+ ### **Biblioteca Genérica**
1032
+ - **Advanced Search**: `/apps/api/src/lib/advancedSearch/index.ts`
1033
+ - **Facets System**: `/apps/api/src/lib/advancedSearch/facets/index.ts`
1034
+ - **Relations Enricher**: `/apps/api/src/lib/advancedSearch/facets/enrichers/relation.ts`
1035
+
1036
+ ### **Entity Schemas**
1037
+ - **Property**: `/apps/api/src/modules/property/schemas/entity-schema.ts`
1038
+ - **Broker**: `/apps/api/src/modules/broker/schemas/entity-schema.ts`