funifier-mcp 0.2.26 → 0.2.28
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/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/check-update.d.ts +5 -0
- package/dist/mcp/check-update.d.ts.map +1 -1
- package/dist/mcp/check-update.js +21 -10
- package/dist/mcp/check-update.js.map +1 -1
- package/dist/mcp/check-update.test.d.ts +2 -0
- package/dist/mcp/check-update.test.d.ts.map +1 -0
- package/dist/mcp/check-update.test.js +33 -0
- package/dist/mcp/check-update.test.js.map +1 -0
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/prompts/templates.d.ts.map +1 -1
- package/dist/mcp/prompts/templates.js +35 -0
- package/dist/mcp/prompts/templates.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +28 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,53 +1,718 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `custom-object` — Database (Objeto Customizado)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Acesso Studio:** sem rota fixa — exposto através de páginas customizadas (`studio_page`) cujo `slug` é configurável (ex.: `studio/custom/car/list`). As páginas rodam HTML/AngularJS no Studio e consomem `/v3/database/<collection>` via HTTP.
|
|
4
|
+
**API Endpoint:** `/v3/database` (gamification) e `/v3/system/database` (banco de sistema)
|
|
5
|
+
**Coleção MongoDB:** qualquer nome — schemaless
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
> **Nota de engenharia (verificada no código):** "Custom Object" **não é** um módulo com entidade própria. Não existe classe `CustomObject*` no `funifier-service` (`find -iname '*CustomObject*'` retorna vazio). É o `com.funifier.rest.v3.rest.DatabaseRest` — um CRUD genérico e schemaless sobre o MongoDB do tenant. O sufixo `__c` é apenas convenção; o servidor **não** o valida nas operações CRUD. A única validação de `__c` está no parâmetro `out` do `aggregate` (`DatabaseRest.java:548`).
|
|
8
|
+
>
|
|
9
|
+
> Toda a documentação abaixo foi extraída por engenharia reversa do código no commit `830037e`. Onde o comportamento diverge do REST convencional ou de uma documentação anterior, isso está marcado explicitamente.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
**Classes-fonte:**
|
|
12
|
+
- `com.funifier.rest.v3.rest.DatabaseRest` — CRUD do tenant (`DatabaseRest.java`)
|
|
13
|
+
- `com.funifier.rest.v3.rest.system.DatabaseRest` — operações no banco de sistema
|
|
14
|
+
- `com.funifier.engine.database.DatabaseManager` — bulk insert e fila assíncrona
|
|
15
|
+
- `com.funifier.engine.database.DatabaseAsyncProcessor` / `DatabaseAsyncTask` — fila de bulk
|
|
16
|
+
- Dependências: `TriggerManager`, `AuditManager`, `CryptManager`/`AesCrypt`, `PaginationUtil`, `JsonUtil`, `Callback`, `Guid`, `AuthBean`
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
- Para criar listas customizadas (pontos turísticos, categorias, etc.)
|
|
11
|
-
- Para armazenar dados que não se encaixam nos módulos padrão
|
|
18
|
+
---
|
|
12
19
|
|
|
13
|
-
##
|
|
20
|
+
## 1. Visão Geral
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
O módulo `custom-object` expõe um **CRUD genérico schemaless** sobre o MongoDB da gamification. Permite criar, ler, atualizar, deletar, contar e agregar documentos em **qualquer coleção** — incluindo as coleções internas do framework (`player`, `action_log`, `challenge`, `trigger`, etc.).
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
**Papel arquitetural:**
|
|
25
|
+
- Armazenamento de dados de negócio que não se encaixam nos módulos padrão (produtos, veículos, categorias, imóveis…), por convenção em coleções `*__c`.
|
|
26
|
+
- Backend de páginas customizadas do Studio (`studio_page`): a página renderiza HTML/AngularJS e usa `/v3/database/<collection>` para listar/criar/editar/excluir (ex.: o exemplo "Car List/Car Form" usa `car__c`).
|
|
27
|
+
- Coleções de cache/configuração do próprio framework também moram aqui: `csv_config__c`, `csv_log__c`, `csv_format__c` (CsvManager), `find_cache_<id>__c` (FindRest/PreparedManager), `email_code_verifier_log__c` (CRM Email/Calendar/Doc).
|
|
28
|
+
- Destino materializado de agregações (parâmetro `out`).
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
**
|
|
23
|
-
**
|
|
30
|
+
**Relação com outros módulos (verificada por chamadas reais):**
|
|
31
|
+
- `TriggerManager.execute(...)` é chamado em **todos** os eventos CRUD → dispara scripts Groovy configurados na coleção `trigger` para a combinação `(entity=collection, event)`.
|
|
32
|
+
- `AuditManager.log(...)` é chamado em POST/PUT/DELETE (mas o registro é **condicional** — ver §6).
|
|
33
|
+
- `CryptManager`/`AesCrypt` encriptam/decriptam campos configurados por coleção (AES-256 ECB).
|
|
34
|
+
- O `aggregate` com `out` é o mecanismo que materializa relatórios em coleções `__c`.
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. Arquitetura e Fluxos
|
|
39
|
+
|
|
40
|
+
### 2.1 Pipeline de escrita — POST (`insert`) e PUT (`update`)
|
|
41
|
+
|
|
42
|
+
Sequência real (`DatabaseRest.java:169-247` para POST; `:290-388` para PUT):
|
|
43
|
+
|
|
44
|
+
1. **[Guard]** `authBean.getScope().indexOf("database") != -1` **e** `collection.length() > 3`. Se falhar → retorna `null` com **HTTP 201** (corpo vazio, sem erro).
|
|
45
|
+
2. **[Auto-`_id`]** Se `_id` ausente ou `null` → `object.put("_id", Guid.newShortGuid())`.
|
|
46
|
+
3. **[Trigger]** `triggerManager.execute(null, object, collection, before_create|before_update, player, null)`. O objeto é passado **por referência**; o script Groovy pode mutá-lo (a mutação persiste). Exceções no script são **engolidas** (não abortam — ver §6).
|
|
47
|
+
4. **[Encriptação — opcional]** Somente se `authBean.checkReadEncryptedValues()` for verdadeiro **e** existir config em `crypt_object_field` para a coleção **e** existir secret ativa:
|
|
48
|
+
- `JsonUtil.toBsonStrictMode(object)` → `fromJson` (normaliza marcações BSON)
|
|
49
|
+
- `AesCrypt.encryptFields(object, field.fields, secret.secret)`
|
|
50
|
+
- Se **qualquer** dessas condições falhar, o dado segue **em plaintext** (ver §5.7).
|
|
51
|
+
5. **[Marshalling]** `new JacksonMapper().getMarshaller().marshall(object)` → `BasicDBObject.parse(json)` (suporte a tipos BSON).
|
|
52
|
+
6. **[Player password — só PUT, só coleção `player`]** Se `collection == "player"` e `!checkReadPlayerPassword()`: relê o player atual e, se o objeto enviado **não tem** `password` (ausente ou `null`), reinsere a senha existente (`DatabaseRest.java:358-363`).
|
|
53
|
+
7. **[Persistência]** POST → `collection.insert(o)`; PUT → `collection.save(o)` (**upsert/replace** — não é patch).
|
|
54
|
+
8. **[Trigger]** `execute(..., after_create|after_update, ...)`.
|
|
55
|
+
9. **[Auditoria — condicional]** `auditManager.log(collection, create|update, authBean, o)` (ver §6 para as condições reais).
|
|
56
|
+
10. **[Decriptação de retorno]** Se houve encriptação no passo 4, decripta os campos **apenas para a resposta**.
|
|
57
|
+
11. **[Resposta]** HTTP **201**. `strict=true` → BSON extended JSON; caso contrário `JsonUtil.toJsonRemoveNullFields` (**remove campos `null` do retorno**).
|
|
58
|
+
|
|
59
|
+
#### Fluxograma — escrita (POST/PUT)
|
|
60
|
+
|
|
61
|
+
```mermaid
|
|
62
|
+
flowchart TD
|
|
63
|
+
A["POST/PUT /v3/database/{collection}"] --> B{"scope 'database'<br/>e collection.len > 3?"}
|
|
64
|
+
B -- Não --> Z["retorna null + HTTP 201<br/>(falha silenciosa)"]
|
|
65
|
+
B -- Sim --> C{"_id ausente/null?"}
|
|
66
|
+
C -- Sim --> D["_id = Guid.newShortGuid()"]
|
|
67
|
+
C -- Não --> E
|
|
68
|
+
D --> E["trigger before_create / before_update<br/>(mutável; exceção NÃO aborta)"]
|
|
69
|
+
E --> F{"checkReadEncryptedValues()<br/>+ config crypt + secret ativa?"}
|
|
70
|
+
F -- Sim --> G["AesCrypt.encryptFields()"]
|
|
71
|
+
F -- Não --> H["plaintext (sem aviso)"]
|
|
72
|
+
G --> I["marshall → BasicDBObject.parse"]
|
|
73
|
+
H --> I
|
|
74
|
+
I --> J{"PUT + collection=player<br/>+ sem scope password?"}
|
|
75
|
+
J -- Sim --> K["preserva password atual"]
|
|
76
|
+
J -- Não --> L
|
|
77
|
+
K --> L{"POST?"}
|
|
78
|
+
L -- POST --> M["collection.insert(o)"]
|
|
79
|
+
L -- PUT --> N["collection.save(o) (upsert/replace)"]
|
|
80
|
+
M --> O["trigger after_create / after_update"]
|
|
81
|
+
N --> O
|
|
82
|
+
O --> P["auditManager.log() (condicional)"]
|
|
83
|
+
P --> Q{"encriptou?"}
|
|
84
|
+
Q -- Sim --> R["decryptFields para o retorno"]
|
|
85
|
+
Q -- Não --> S
|
|
86
|
+
R --> S["HTTP 201 + objeto"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 2.2 Pipeline — leitura por ID (`find`, `DatabaseRest.java:59-118`)
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
[GET /v3/database/{collection}/{id}]
|
|
93
|
+
→ se id == "me": id = authBean.getPlayerFromTokenIfExist()
|
|
94
|
+
→ guard: scope 'database' + collection.len > 3 + id.len > 0 (senão → null/200)
|
|
95
|
+
→ c.findOne("{_id:#}", id).as(HashMap)
|
|
96
|
+
→ se checkReadEncryptedValues() e result != null e há config+secret:
|
|
97
|
+
(se strict) toBsonStrictMode → fromJson
|
|
98
|
+
AesCrypt.decryptFields(result, fields, secret)
|
|
99
|
+
→ strict=true → toBsonStrictMode(result)
|
|
100
|
+
strict=false → toJsonRemoveNullFields(result) ← remove campos null
|
|
101
|
+
→ HTTP 200
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2.3 Pipeline — listagem paginada (`findAll`, `DatabaseRest.java:120-160`)
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
[GET /v3/database/{collection}?q=...&max_results=N]
|
|
108
|
+
→ guard: scope 'database' + collection.len > 0 (senão → null/200)
|
|
109
|
+
→ maxResultsParsed = parseInt(max_results) | fallback 100 (silencioso)
|
|
110
|
+
→ pipeline inicial: { $match : { <q> } } (q injetado cru)
|
|
111
|
+
→ PaginationUtil.getPageResult(a, Range, 0, maxResultsParsed)
|
|
112
|
+
getRange(): SE header Range presente → skip/limit vêm do header (max_results IGNORADO)
|
|
113
|
+
SENÃO → skip=0, limit=maxResultsParsed
|
|
114
|
+
$facet: { result: [{$skip},{$limit}], pagination: [{$group:{_id:null,count:{$sum:1}}}] }
|
|
115
|
+
→ resposta: array (campos null removidos) + header Content-Range "items skip-limit/count"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> **Nota sobre `max_results` vs `Range`:** quando o header `Range` está presente, ele **sobrepõe** `max_results` (`PaginationUtil.getRange` lê `skip-limit` do header). `max_results` só é usado como limite quando **não** há header `Range`.
|
|
119
|
+
>
|
|
120
|
+
> **Divergência do padrão HTTP (importante):** `PaginationUtil.getRange` interpreta `items=0-100` como `skip=0, limit=100`, ou seja `items=<skip>-<limit>` — **não** o padrão HTTP `items=<início>-<fim>`. Para "10 documentos a partir do offset 5", um cliente HTTP padrão escreveria `items=5-14`; aqui o correto é `items=5-10` (skip 5, limit 10). O segundo número é a **quantidade**, não o índice final.
|
|
121
|
+
|
|
122
|
+
### 2.4 Pipeline — delete (`delete`, `DatabaseRest.java:453-484`)
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
[DELETE /v3/database/{collection}?q=...]
|
|
126
|
+
→ guard: scope 'database' + collection.len > 0 + q.len >= 3 (senão → null/200)
|
|
127
|
+
→ total = count("{" + q + "}")
|
|
128
|
+
→ SE total <= 20.000:
|
|
129
|
+
ids = distinct("_id").query("{" + q + "}")
|
|
130
|
+
→ SE ids.size() > 0:
|
|
131
|
+
trigger before_delete (recebe lista de ids)
|
|
132
|
+
remove("{" + q + "}")
|
|
133
|
+
trigger after_delete (recebe lista de ids)
|
|
134
|
+
auditManager.log(DELETE, ids) (condicional)
|
|
135
|
+
→ SE total > 20.000: ids fica vazio → NADA é deletado → HTTP 200 (silencioso)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
> **Armadilha crítica (confirmada no código):** se `q` casa com **mais de 20.000** documentos, o delete **não executa** e retorna **200 OK** sem erro. O comentário no código diz "menor que 1000" (`DatabaseRest.java:466`), mas a constante real é `20_000` (`:469`) — o comentário está **desatualizado**.
|
|
139
|
+
|
|
140
|
+
#### Fluxograma — delete
|
|
141
|
+
|
|
142
|
+
```mermaid
|
|
143
|
+
flowchart LR
|
|
144
|
+
A["DELETE ?q=..."] --> B{"scope + collection.len>0<br/>+ q.len>=3?"}
|
|
145
|
+
B -- Não --> Z["null + HTTP 200"]
|
|
146
|
+
B -- Sim --> C["total = count(q)"]
|
|
147
|
+
C --> D{"total <= 20.000?"}
|
|
148
|
+
D -- Não --> Y["ids vazio → NÃO deleta<br/>HTTP 200 (silencioso)"]
|
|
149
|
+
D -- Sim --> E["ids = distinct(_id)"]
|
|
150
|
+
E --> F{"ids.size > 0?"}
|
|
151
|
+
F -- Não --> Z
|
|
152
|
+
F -- Sim --> G["before_delete(ids)"]
|
|
153
|
+
G --> H["remove(q)"]
|
|
154
|
+
H --> I["after_delete(ids)"]
|
|
155
|
+
I --> J["audit(delete, ids) (condicional)"]
|
|
156
|
+
J --> K["HTTP 200"]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 2.5 Pipeline — bulk insert (`insertBulk` → `DatabaseManager.bulkInsert`)
|
|
160
|
+
|
|
161
|
+
`DatabaseRest.java:252-285` decide entre síncrono e assíncrono; o processamento real está em `DatabaseManager.bulkInsert` (`DatabaseManager.java:29-78`).
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
[POST /v3/database/{collection}/bulk?async=...]
|
|
165
|
+
→ guard: scope 'database' + collection.len > 3 + list.size() > 0 (senão → {}/201)
|
|
166
|
+
→ readPlayerPassword = (collection == "player" && !checkReadPlayerPassword())
|
|
167
|
+
→ async == "true":
|
|
168
|
+
cria DatabaseAsyncTask {task:"bulk", collection, list, readPlayerPassword}
|
|
169
|
+
DatabaseManager.addAsyncTask(task) → fila.offer(task)
|
|
170
|
+
retorna {"total": N} IMEDIATAMENTE (sem garantir processamento)
|
|
171
|
+
→ async != "true" (padrão):
|
|
172
|
+
DatabaseManager.bulkInsert(collection, list, readPlayerPassword) [síncrono]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`DatabaseManager.bulkInsert` (idêntico para sync e async — o worker chama o mesmo método):
|
|
176
|
+
```
|
|
177
|
+
→ trigger before_bulk (recebe a lista inteira)
|
|
178
|
+
→ para cada objeto:
|
|
179
|
+
marshall → BasicDBObject.parse
|
|
180
|
+
se _id ausente/null → Guid.newShortGuid()
|
|
181
|
+
se readPlayerPassword → relê player atual e preserva password
|
|
182
|
+
trigger before_create (por item)
|
|
183
|
+
collection.save(object) ← SAVE (upsert/replace), não insert
|
|
184
|
+
trigger after_create (por item)
|
|
185
|
+
→ trigger after_bulk (recebe a lista inteira)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> **Diferenças do POST simples (verificadas):** o bulk usa `save()` (upsert), **não** registra auditoria, e **não** aplica encriptação de campos — mesmo que a coleção tenha config em `crypt_object_field`. A preservação de password **é** aplicada quando `readPlayerPassword=true`.
|
|
189
|
+
|
|
190
|
+
#### Sequência — bulk insert (síncrono)
|
|
191
|
+
|
|
192
|
+
```mermaid
|
|
193
|
+
sequenceDiagram
|
|
194
|
+
participant C as Cliente HTTP
|
|
195
|
+
participant R as DatabaseRest
|
|
196
|
+
participant DM as DatabaseManager
|
|
197
|
+
participant T as TriggerManager
|
|
198
|
+
participant M as MongoDB (Jongo)
|
|
199
|
+
|
|
200
|
+
C->>R: POST /{collection}/bulk
|
|
201
|
+
R->>R: guard scope + collection.len>3 + list>0
|
|
202
|
+
R->>DM: bulkInsert(collection, list, readPlayerPassword)
|
|
203
|
+
DM->>T: execute(before_bulk, list)
|
|
204
|
+
loop para cada objeto da lista
|
|
205
|
+
DM->>DM: marshall + auto _id + preserva password?
|
|
206
|
+
DM->>T: execute(before_create, item)
|
|
207
|
+
DM->>M: collection.save(item) (upsert)
|
|
208
|
+
DM->>T: execute(after_create, item)
|
|
209
|
+
end
|
|
210
|
+
DM->>T: execute(after_bulk, list)
|
|
211
|
+
R-->>C: HTTP 201 {"total": N}
|
|
212
|
+
Note over DM,M: SEM auditoria, SEM encriptação
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 2.6 Pipeline — aggregate com materialização (`aggregate`, `DatabaseRest.java:490-587`)
|
|
216
|
+
|
|
217
|
+
```mermaid
|
|
218
|
+
flowchart LR
|
|
219
|
+
A["POST /{collection}/aggregate"] --> B{"scope + collection.len > 2?"}
|
|
220
|
+
B -- Não --> Z["null + HTTP 200"]
|
|
221
|
+
B -- Sim --> C{"q presente?"}
|
|
222
|
+
C -- Sim --> D["estágio inicial { $match : { q } }"]
|
|
223
|
+
C -- Não --> E["estágios = corpo da requisição"]
|
|
224
|
+
D --> F["allowDiskUse(true)"]
|
|
225
|
+
E --> F
|
|
226
|
+
F --> G{"collection=player<br/>e sem scope password?"}
|
|
227
|
+
G -- Sim --> H["adiciona {$project:{password:0}}"]
|
|
228
|
+
G -- Não --> I
|
|
229
|
+
H --> I{"out termina com '__c'?"}
|
|
230
|
+
I -- Sim --> J["getRange(Range) → $skip/$limit"]
|
|
231
|
+
J --> K{"out_operation == 'replace'?"}
|
|
232
|
+
K -- Sim --> L["collection(out).drop()"]
|
|
233
|
+
K -- Não --> M["mantém docs existentes"]
|
|
234
|
+
L --> N["para cada doc: out.save(doc)"]
|
|
235
|
+
M --> N
|
|
236
|
+
N --> O["retorna {total, out} + HTTP 200"]
|
|
237
|
+
I -- Não --> P["getPageResultThrowsException ($facet)<br/>retorno paginado + Content-Range"]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
> Síncrono em todos os caminhos. `allowDiskUse(true)` está habilitado em todas as agregações. Se `out` está presente mas **não** termina com `__c`, é **ignorado** e o resultado é retornado paginado normalmente.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 3. Estrutura dos Objetos
|
|
245
|
+
|
|
246
|
+
### 3.1 Documento customizado — sem schema fixo
|
|
247
|
+
|
|
248
|
+
Não existe classe Java mapeada para o documento. O corpo é desserializado como `HashMap<String,Object>` e persistido como `BasicDBObject` após `JacksonMapper.marshall`.
|
|
249
|
+
|
|
250
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
251
|
+
|---|---|---|---|---|
|
|
252
|
+
| `_id` | String | `Guid.newShortGuid()` | — | Gerado automaticamente se ausente/`null` em POST, PUT e em cada item do bulk |
|
|
253
|
+
| `*` | qualquer | — | — | Todos os demais campos são schemaless e preservados integralmente |
|
|
254
|
+
|
|
255
|
+
**Comportamento do `_id`:**
|
|
256
|
+
- POST sem `_id`: gera automaticamente.
|
|
257
|
+
- PUT sem `_id`: gera automaticamente → **cria um documento novo** (não atualiza nada).
|
|
258
|
+
- PUT com `_id` existente: `save()` substitui o **documento inteiro** (não é patch — campos ausentes são removidos).
|
|
259
|
+
|
|
260
|
+
**Campos não-persistidos / computados:**
|
|
261
|
+
- Valores **decriptados** aparecem **apenas na resposta** de GET/POST/PUT; no MongoDB permanecem encriptados.
|
|
262
|
+
|
|
263
|
+
**Sanitização silenciosa de retorno:**
|
|
264
|
+
- Em modo **não-strict**, toda resposta passa por `JsonUtil.toJsonRemoveNullFields` (Jackson `Include.NON_NULL`) → **campos com valor `null` somem da resposta**. Use `strict=true` para inspecionar tipos BSON e manter a estrutura.
|
|
265
|
+
|
|
266
|
+
### 3.2 `CryptObjectField` — configuração de criptografia (coleção `crypt_object_field`, banco da gamification)
|
|
267
|
+
|
|
268
|
+
`com.funifier.engine.crypt.CryptObjectField`:
|
|
269
|
+
|
|
270
|
+
| Campo | Tipo | Descrição |
|
|
271
|
+
|---|---|---|
|
|
272
|
+
| `_id` | String | **Nome da coleção** a proteger. No Java o atributo chama-se `entity` mas é mapeado para `_id` via `@JsonProperty("_id")` |
|
|
273
|
+
| `fields` | List\<String\> | Lista de **JSONPaths** (sem o prefixo `$.`) a encriptar — ex.: `cpf`, `extra.documento` |
|
|
274
|
+
|
|
275
|
+
`CryptManager.findObjectField(collection)` busca por `{_id: collection}` (`CryptManager.java:395-398`).
|
|
276
|
+
|
|
277
|
+
### 3.3 `CryptTenantSecret` — segredo AES (coleção `crypt_tenant_secret`, **banco de SISTEMA**)
|
|
278
|
+
|
|
279
|
+
| Campo | Tipo | Descrição |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `_id` | String | Id do segredo |
|
|
282
|
+
| `status` | String | `active` / `arquived` (`STATUS_ACTIVE` / `STATUS_ARCHIVED`) |
|
|
283
|
+
| `gamification` | String | apiKey do tenant |
|
|
284
|
+
| `secret` | String | Chave AES usada por `AesCrypt` |
|
|
285
|
+
| `created` / `updated` | Date | — |
|
|
286
|
+
|
|
287
|
+
`CryptManager.findTenantSecretActive()` busca por `{gamification:#, status:'active'}` em `SystemFactory.getInstance()` — ou seja, **o segredo vive no banco de sistema, não no banco do tenant**.
|
|
288
|
+
|
|
289
|
+
#### Relação entre as coleções de criptografia
|
|
290
|
+
|
|
291
|
+
```mermaid
|
|
292
|
+
erDiagram
|
|
293
|
+
CRYPT_OBJECT_FIELD ||--o| TARGET_COLLECTION : "protege (_id = nome da coleção)"
|
|
294
|
+
CRYPT_TENANT_SECRET ||--o{ CRYPT_OBJECT_FIELD : "fornece chave AES por gamification"
|
|
295
|
+
CRYPT_OBJECT_FIELD {
|
|
296
|
+
string _id "nome da coleção (entity)"
|
|
297
|
+
string[] fields "JSONPaths a encriptar"
|
|
298
|
+
}
|
|
299
|
+
CRYPT_TENANT_SECRET {
|
|
300
|
+
string _id
|
|
301
|
+
string status "active|arquived"
|
|
302
|
+
string gamification "apiKey"
|
|
303
|
+
string secret "chave AES"
|
|
304
|
+
}
|
|
305
|
+
TARGET_COLLECTION {
|
|
306
|
+
string _id
|
|
307
|
+
object campos_encriptados
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
> Banco da gamification: `crypt_object_field`. Banco de sistema: `crypt_tenant_secret`. (`CryptManager` usa `manager.getJongoConnection()` para o primeiro e `SystemFactory.getInstance().getJongoConnection()` para o segundo.)
|
|
312
|
+
|
|
313
|
+
### 3.4 `DatabaseAsyncTask` — item da fila de bulk assíncrono
|
|
314
|
+
|
|
315
|
+
| Campo | Tipo | Descrição |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| `task` | String | Sempre `"bulk"` (`TASK_BULK_INSERT`) |
|
|
318
|
+
| `collection` | String | Coleção destino |
|
|
319
|
+
| `list` | List\<Object\> | Objetos a inserir |
|
|
320
|
+
| `readPlayerPassword` | boolean | Aciona preservação de password do player |
|
|
321
|
+
|
|
322
|
+
A fila é uma `LinkedBlockingQueue` **por tenant** (`DatabaseAsyncProcessor`), consumida por uma thread `Funifier-Database-Async-<apiKey>` que faz `poll()` em laço (sleep de 100ms quando vazia). Não há persistência da fila — itens pendentes se perdem em restart.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 4. Endpoints
|
|
327
|
+
|
|
328
|
+
> Convenção de "falha silenciosa": quando o guard de scope/parâmetro falha, a maioria dos métodos chama `Callback.callback(null, status)` → corpo é o literal JSON `null` com o status indicado (200 nos GET/DELETE, 201 nos POST/PUT), **sem mensagem de erro**. Da mesma forma, `GET /{collection}/{id}` para um id inexistente retorna **HTTP 200 com corpo `null`** (não 404).
|
|
329
|
+
|
|
330
|
+
### `GET /v3/database/{collection}/{id}` — buscar por id
|
|
331
|
+
|
|
332
|
+
| Aspecto | Detalhe |
|
|
333
|
+
|---|---|
|
|
334
|
+
| Finalidade | Busca um documento pelo `_id` |
|
|
335
|
+
| Scope | `database` |
|
|
336
|
+
| Restrições | `collection.length() > 3`, `id.length() > 0` |
|
|
337
|
+
| Falha de guard | `null` + HTTP 200 |
|
|
338
|
+
|
|
339
|
+
**Query params:** `strict` (`"true"` → BSON extended JSON).
|
|
340
|
+
**Comportamentos especiais:** `id=me` resolve para o player do token; decripta campos se `checkReadEncryptedValues()`; em modo não-strict remove campos `null`.
|
|
341
|
+
|
|
342
|
+
### `GET /v3/database/{collection}` — listar/filtrar
|
|
343
|
+
|
|
344
|
+
| Aspecto | Detalhe |
|
|
345
|
+
|---|---|
|
|
346
|
+
| Scope | `database` |
|
|
347
|
+
| Restrição | `collection.length() > 0` |
|
|
348
|
+
|
|
349
|
+
**Query params:** `q` (filtro MongoDB inline, sem chaves externas), `strict`, `max_results` (default 100, fallback silencioso). **Header:** `Range: items=skip-limit`.
|
|
350
|
+
**Comportamento real:** `{ $match: {<q>} }` + `$facet` (paginação + contagem em uma query). Header de resposta `Content-Range: items skip-limit/total`. `Range` sobrepõe `max_results`.
|
|
351
|
+
|
|
352
|
+
### `POST /v3/database/{collection}` — criar
|
|
353
|
+
|
|
354
|
+
| Aspecto | Detalhe |
|
|
355
|
+
|---|---|
|
|
356
|
+
| Scope | `database` · Restrição `collection.length() > 3` · Status **201** · Operação `insert()` |
|
|
357
|
+
|
|
358
|
+
`strict` aceita/retorna BSON. Dispara `before_create`/`after_create`, encripta (se habilitado), audita (condicional). Ver §2.1.
|
|
359
|
+
|
|
360
|
+
### `PUT /v3/database/{collection}` — substituir (upsert)
|
|
361
|
+
|
|
362
|
+
| Aspecto | Detalhe |
|
|
363
|
+
|---|---|
|
|
364
|
+
| Scope | `database` · Restrição `collection.length() > 3` · Status **201** · Operação `save()` (full replace) |
|
|
365
|
+
|
|
366
|
+
> **Armadilha:** PUT é **replace completo**, não patch. Campos ausentes no body **somem** do documento. Sem `_id`, cria documento novo. Proteção de password aplicada para `collection=player` sem scope de password.
|
|
367
|
+
|
|
368
|
+
### `POST /v3/database/{collection}/bulk` — inserção em lote
|
|
369
|
+
|
|
370
|
+
| Aspecto | Detalhe |
|
|
371
|
+
|---|---|
|
|
372
|
+
| Scope | `database` · Restrição `collection.length() > 3` e `list.size() > 0` · Status **201** |
|
|
373
|
+
|
|
374
|
+
**Query param:** `async` (`"true"` → enfileira e retorna `{"total":N}` imediatamente). Usa `save()` por item. **Sem auditoria, sem encriptação.** Dispara `before_bulk`/(`before_create`+`after_create` por item)/`after_bulk`. Ver §2.5.
|
|
375
|
+
|
|
376
|
+
### `DELETE /v3/database/{collection}` — remover por filtro
|
|
377
|
+
|
|
378
|
+
| Aspecto | Detalhe |
|
|
379
|
+
|---|---|
|
|
380
|
+
| Scope | `database` · Restrições `collection.length() > 0` e `q.length() >= 3` · Status **200** |
|
|
381
|
+
|
|
382
|
+
**Limite 20.000:** se o filtro casa mais de 20.000 docs, **não deleta** e retorna 200. Ver §2.4.
|
|
383
|
+
|
|
384
|
+
### `POST /v3/database/{collection}/aggregate` — agregação (com materialização opcional)
|
|
385
|
+
|
|
386
|
+
| Aspecto | Detalhe |
|
|
387
|
+
|---|---|
|
|
388
|
+
| Scope | `database` · Restrição `collection.length() > 2` |
|
|
389
|
+
|
|
390
|
+
**Query params:** `q` (`$match` inicial), `out` (coleção destino — **deve terminar em `__c`**), `out_operation` (`"replace"` → `drop()` antes), `strict`. **Header:** `Range` (define `$skip`/`$limit` quando há `out`).
|
|
391
|
+
**Comportamentos:** `allowDiskUse(true)`; para `collection=player` sem scope de password adiciona `{$project:{password:0}}`; `out` que não termina em `__c` é ignorado (cai no retorno paginado). Body = `List<Object>` (estágios do pipeline). Ver §2.6.
|
|
392
|
+
|
|
393
|
+
### `POST /v3/database/{collection}/aggregate_test` — agregação sem paginação
|
|
394
|
+
|
|
395
|
+
| Aspecto | Detalhe |
|
|
396
|
+
|---|---|
|
|
397
|
+
| Scope | `database` · Restrição `collection.length() > 3` |
|
|
398
|
+
|
|
399
|
+
**Query param:** `strict`. **Body:** `List<Object>` (estágios). Diferenças de `/aggregate`:
|
|
400
|
+
- **Não** usa `$facet`; retorna **todos** os documentos via `IteratorUtils.toList()` (sem paginação).
|
|
401
|
+
- **Não** suporta `out` (sem materialização).
|
|
402
|
+
- **Não** aceita `q` (não há parâmetro `q` no método; o `$match` inicial nunca é montado).
|
|
403
|
+
- `allowDiskUse(true)`.
|
|
404
|
+
|
|
405
|
+
> **Correção importante (verificada em `DatabaseRest.java:605-668`):** ao contrário de `/aggregate`, o `aggregate_test` **NÃO** adiciona `{$project:{password:0}}` para a coleção `player`. Logo, um pipeline de teste sobre `player` **pode expor o campo `password`** mesmo sem o scope `read_encrypted_player_password`. Documentações anteriores afirmavam o contrário — está errado.
|
|
406
|
+
|
|
407
|
+
### `DELETE /v3/database/{collection}/drop` — dropar coleção
|
|
408
|
+
|
|
409
|
+
| Aspecto | Detalhe |
|
|
410
|
+
|---|---|
|
|
411
|
+
| Scope | **NENHUM** — não verifica `SCOPE_DATABASE` · Restrição `collection.length() > 0` · Status **200** |
|
|
412
|
+
|
|
413
|
+
> **Risco crítico:** qualquer token autenticado (com apiKey válida) pode dropar **qualquer** coleção, inclusive `player`, `challenge`, `action_log`. Ver §8.
|
|
414
|
+
|
|
415
|
+
### `GET /v3/database/collections` — listar coleções
|
|
416
|
+
|
|
417
|
+
| Aspecto | Detalhe |
|
|
418
|
+
|---|---|
|
|
419
|
+
| Scope | **NENHUM** — sem verificação de `SCOPE_DATABASE` |
|
|
420
|
+
|
|
421
|
+
Retorna `getCollectionNames()` — inclui coleções internas do sistema.
|
|
422
|
+
|
|
423
|
+
### `GET /v3/database/count` — contar
|
|
424
|
+
|
|
425
|
+
| Aspecto | Detalhe |
|
|
426
|
+
|---|---|
|
|
427
|
+
| Scope | `database` · `collection.length() > 0` |
|
|
428
|
+
|
|
429
|
+
**Query params:** `collection`, `q`. **Resposta:** `{"total": N}`.
|
|
430
|
+
|
|
431
|
+
### `POST /v3/database/{collection}/index` — criar índice(s)
|
|
432
|
+
|
|
433
|
+
Scope `database`, `collection.length() > 0`. **Body:** `Map<campo, direção>` (ex.: `{"brand":1,"year":-1}`). Retorna `getIndexInfo()`.
|
|
434
|
+
|
|
435
|
+
### `DELETE /v3/database/{collection}/index/{id}` — remover índice
|
|
436
|
+
|
|
437
|
+
Scope `database`, `collection.length() > 0`. `dropIndex(id)` (id = nome do índice).
|
|
438
|
+
|
|
439
|
+
### `GET /v3/database/{collection}/index` — listar índices
|
|
440
|
+
|
|
441
|
+
Scope `database`, `collection.length() > 0`. Retorna `getIndexInfo()`.
|
|
442
|
+
|
|
443
|
+
### `GET /v3/database/queue/info` — métricas da fila assíncrona
|
|
444
|
+
|
|
445
|
+
| Aspecto | Detalhe |
|
|
446
|
+
|---|---|
|
|
447
|
+
| Scope | **NENHUM** |
|
|
448
|
+
|
|
449
|
+
```json
|
|
450
|
+
{ "size": 0, "remaining_capacity": 2147483647, "last_process_duration_ms": 145, "max_process_duration_ms": 890 }
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### `DELETE /v3/database/queue/clear` — esvaziar a fila
|
|
454
|
+
|
|
455
|
+
| Aspecto | Detalhe |
|
|
456
|
+
|---|---|
|
|
457
|
+
| Scope | **NENHUM** |
|
|
458
|
+
|
|
459
|
+
`queue.clear()` — descarta tasks de bulk pendentes (perda de dados não processados).
|
|
460
|
+
|
|
461
|
+
### `DELETE /v3/database/queue/info` — zerar métricas da fila
|
|
462
|
+
|
|
463
|
+
| Aspecto | Detalhe |
|
|
464
|
+
|---|---|
|
|
465
|
+
| Scope | **NENHUM** |
|
|
466
|
+
|
|
467
|
+
Reseta `last_process_duration_ms` e `max_process_duration_ms` para 0 (`clearQueueInfo`). **Endpoint distinto de `/queue/clear`** — não esvazia a fila, apenas zera os contadores.
|
|
468
|
+
|
|
469
|
+
### Endpoints do Sistema — `v3/system/database`
|
|
470
|
+
|
|
471
|
+
Operam sobre o **banco de sistema** (`SystemFactory.getInstance()`), não o do tenant. Cada um chama `checkSystemPermission("api", "/system/database", <op>)` e lança **HTTP 401** se negado.
|
|
472
|
+
|
|
473
|
+
| Método | Path | Permissão | Restrição |
|
|
474
|
+
|---|---|---|---|
|
|
475
|
+
| `POST` | `/{collection}/index` | `create` | `collection.length() > 3` |
|
|
476
|
+
| `DELETE` | `/{collection}/index/{id}` | `delete` | `collection.length() > 3` |
|
|
477
|
+
| `GET` | `/{collection}/index` | `read` | `collection.length() > 3` |
|
|
478
|
+
| `GET` | `/collections` | `read` | — |
|
|
479
|
+
|
|
480
|
+
> Diferença relevante: no banco de sistema o `GET /collections` **exige** permissão; no banco do tenant (`/v3/database/collections`) **não exige** scope.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## 5. Regras de Negócio (existem no código, não no schema)
|
|
485
|
+
|
|
486
|
+
### 5.1 Limiares de validação do nome da coleção (por endpoint)
|
|
487
|
+
|
|
488
|
+
| Endpoint | Mínimo |
|
|
489
|
+
|---|---|
|
|
490
|
+
| `GET /collections`, `GET/DELETE /queue/*` | não usa collection |
|
|
491
|
+
| `GET /{collection}`, `DELETE /{collection}`, `GET /count`, `POST/DELETE/GET /index` | `> 0` |
|
|
492
|
+
| `GET /{collection}/{id}`, `DELETE /{collection}/drop` | `> 0` (drop) / `> 3` (find) |
|
|
493
|
+
| `POST /aggregate` | `> 2` |
|
|
494
|
+
| `POST /{collection}`, `PUT /{collection}`, `POST /bulk`, `POST /aggregate_test` | `> 3` |
|
|
495
|
+
|
|
496
|
+
> Os limiares são **inconsistentes entre endpoints** — isso é comportamento real, não erro de documentação. `find` (GET by id) exige `> 3`, mas `delete` exige apenas `> 0`.
|
|
497
|
+
|
|
498
|
+
### 5.2 Auto-geração de `_id`
|
|
499
|
+
`Guid.newShortGuid()` quando ausente/`null` em POST, PUT e cada item do bulk. Em PUT, isso transforma a operação em criação de documento novo.
|
|
500
|
+
|
|
501
|
+
### 5.3 PUT é replace completo
|
|
502
|
+
`save()` substitui o documento inteiro. Não há endpoint de patch parcial (o que existia, `/operations`, está comentado — ver §7).
|
|
503
|
+
|
|
504
|
+
### 5.4 Player password protection
|
|
505
|
+
`collection == "player"` e token sem `read_encrypted_player_password`: em PUT e no bulk (`readPlayerPassword=true`), a senha existente é preservada quando ausente/`null` no objeto enviado. Comentário "SEGURANCA BRADESCO" no código.
|
|
506
|
+
|
|
507
|
+
### 5.5 Convenção `__c`
|
|
508
|
+
Não é validada no CRUD. Única exigência: `out` do `aggregate` deve terminar em `__c`. O framework usa internamente `csv_config__c`, `csv_log__c`, `csv_format__c`, `find_cache_<id>__c`, `email_code_verifier_log__c`.
|
|
509
|
+
|
|
510
|
+
### 5.6 Limite de delete (20.000)
|
|
511
|
+
Filtro que casa > 20.000 docs → no-op silencioso com 200. (Comentário do código diz "1000", mas o valor real é 20.000.)
|
|
512
|
+
|
|
513
|
+
### 5.7 Assimetria de encriptação — risco silencioso
|
|
514
|
+
Encriptação na escrita só ocorre se `checkReadEncryptedValues()` for verdadeiro. Esse método retorna `true` por **dois caminhos** (`AuthBean.java:221-226`): scope `read_encrypted_field_values` **ou** permissão de sistema `api/encrypted_field_values/read`. Se **nenhum** estiver presente, o dado é salvo em **plaintext** na coleção configurada, **sem aviso**. Resultado: a mesma coleção pode acumular documentos encriptados e em plaintext, dependendo de qual token escreveu. Além disso, falhas internas de `AesCrypt.encryptFields` (ex.: JSONPath inválido) são **engolidas** (`printStackTrace`) → o campo permanece plaintext silenciosamente.
|
|
515
|
+
|
|
516
|
+
### 5.8 Remoção de campos `null` no retorno
|
|
517
|
+
Respostas não-strict passam por `Include.NON_NULL` → campos `null` desaparecem. Não é perda de dados (persistem no Mongo), mas surpreende clientes que esperam o campo.
|
|
518
|
+
|
|
519
|
+
### 5.9 Isolamento multi-tenant
|
|
520
|
+
Toda operação resolve `FrontController.getInstance(authBean.getApiKey()).getManagerFactory().getJongoConnection()` — conexão isolada por apiKey. Sem acesso cross-tenant acidental.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 6. Comportamentos Automáticos
|
|
525
|
+
|
|
526
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
527
|
+
|---|---|---|---|
|
|
528
|
+
| Auto-`_id` | `_id` ausente em POST/PUT/bulk | Gera `Guid.newShortGuid()` | Sim |
|
|
529
|
+
| Trigger `before_create` | POST e cada item do bulk | Roda Groovy; pode **mutar** o objeto (passado por referência) | Mutação persiste |
|
|
530
|
+
| Trigger `after_create` | POST e cada item do bulk | Efeitos colaterais | — |
|
|
531
|
+
| Trigger `before_update` / `after_update` | PUT | Idem | Mutação em `before_update` persiste |
|
|
532
|
+
| Trigger `before_delete` / `after_delete` | DELETE (apenas se `total <= 20.000` **e** `ids > 0`) | Recebe lista de `_id`s | — |
|
|
533
|
+
| Trigger `before_bulk` / `after_bulk` | POST `/bulk` | Recebe a lista inteira | — |
|
|
534
|
+
| Auditoria | POST/PUT/DELETE | Grava em `audit_log` **somente sob condições** (ver abaixo) | Condicional |
|
|
535
|
+
| Encriptação de campos | POST/PUT **com** capacidade de cripto + config + secret | Encripta JSONPaths configurados | Encriptado no Mongo |
|
|
536
|
+
| Plaintext silencioso | POST/PUT **sem** capacidade de cripto em coleção configurada | Salva sem encriptar, sem aviso | Plaintext no Mongo |
|
|
537
|
+
| Preservação de password | PUT/bulk em `player` sem scope de password | Reinsere a senha atual | Mantida |
|
|
538
|
+
| Ocultar password no aggregate | `/aggregate` em `player` sem scope de password | Adiciona `{$project:{password:0}}` | Só filtra retorno |
|
|
539
|
+
|
|
540
|
+
> **Trigger NÃO aborta a operação (correção):** em `TriggerManager.execute` (`:48-68`), cada `runner.run(...)` está dentro de `try/catch` que apenas faz `printStackTrace`. Uma exceção no script **não** interrompe o CRUD. Além disso, há **limite diário de execuções** por trigger (`newTriggerExecution`) — ao exceder, o trigger é **silenciosamente pulado** (apenas log em stdout).
|
|
541
|
+
|
|
542
|
+
> **Auditoria é triplamente condicional (correção):** `AuditManager.log` (`:81-129`) só grava em `audit_log` se **(1)** o token carrega identidade de `studio` (user) **ou** `player`; **(2)** existe um documento `Audit` **ativo** na coleção `audit` casando `{active:true, entity:<collection>, event:<create|update|delete>}`; **(3)** o `type` desse `Audit` (`all`/`studio`/`player`) é compatível com o tipo do chamador. Tokens puramente de aplicação (Basic/api_key, sem user/player) **não geram auditoria**. O `bulk` **nunca** audita.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## 7. Suportado vs NÃO Suportado
|
|
547
|
+
|
|
548
|
+
### ✅ Suportado
|
|
549
|
+
- CRUD genérico em qualquer coleção do tenant.
|
|
550
|
+
- Filtro inline (`?q=field:'value'`) e paginação (`Range` + `Content-Range`, `max_results`).
|
|
551
|
+
- Pipeline de agregação completo com `$facet`, `allowDiskUse`, múltiplos estágios.
|
|
552
|
+
- Materialização de agregação em coleção `__c` (`?out=`, `out_operation=replace`).
|
|
553
|
+
- Bulk insert síncrono e assíncrono (via fila por tenant).
|
|
554
|
+
- Encriptação AES de campos por coleção (JSONPaths), com chave por gamification.
|
|
555
|
+
- Triggers `before`/`after` em todos os eventos CRUD + `before_bulk`/`after_bulk`.
|
|
556
|
+
- Gerenciamento de índices (criar/remover/listar) no banco do tenant e do sistema.
|
|
557
|
+
- BSON extended JSON via `strict=true`; atalho `id=me`.
|
|
558
|
+
- Monitoramento/controle da fila assíncrona.
|
|
559
|
+
|
|
560
|
+
### ❌ NÃO Suportado / Limitações (confirmadas no código)
|
|
561
|
+
- **Schema/validação:** nenhuma — qualquer JSON é aceito.
|
|
562
|
+
- **PATCH parcial:** PUT é sempre replace via `save()`.
|
|
563
|
+
- **`PUT /{collection}/operations`:** **código morto** comentado em `DatabaseRest.java:393-447` (operações `$inc`/`$set`/`upsert`/`multi`). Implementado, porém **nunca exposto**.
|
|
564
|
+
- **Delete > 20.000 docs:** no-op silencioso (200).
|
|
565
|
+
- **Auditoria no bulk:** inexistente.
|
|
566
|
+
- **Encriptação no bulk e no aggregate:** inexistente (campos não são encriptados/decriptados nesses caminhos).
|
|
567
|
+
- **`aggregate_test` não oculta `password`** da coleção `player` (diferente de `/aggregate`).
|
|
568
|
+
- **Transações ACID** entre operações: inexistentes.
|
|
569
|
+
- **Proteção de coleções internas:** com scope `database` é possível ler/escrever `player`, `action_log`, `trigger`, `security`, etc.; e `drop`/`collections`/`queue/*` **não exigem scope algum**.
|
|
570
|
+
- **Persistência da fila assíncrona:** inexistente — restart perde tasks pendentes; sem confirmação de processamento ao chamador (`async=true`).
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## 8. Segurança e Permissões
|
|
575
|
+
|
|
576
|
+
### 8.1 Autenticação (`AuthBean`)
|
|
577
|
+
Aceita `Authorization: Bearer <JWT>` (claims `apiKey`, `scope`, `player`, `user`), `Basic <base64(apiKey:appSecret)>` (scope calculado a partir da `Application`/role `public`), `Account <...>` e `Studio <token>`. O scope para Bearer vem do claim `scope` do JWT.
|
|
578
|
+
|
|
579
|
+
### 8.2 Scope `database` e capacidades
|
|
580
|
+
- `SCOPE_DATABASE = "database"` — exigido pela maioria das operações CRUD.
|
|
581
|
+
- `SCOPE_READ_CRYPTED = "read_encrypted_field_values"` — habilita encriptar/decriptar campos (ou perm. de sistema `api/encrypted_field_values/read`).
|
|
582
|
+
- `SCOPE_READ_PLAYER_PASSWORD = "read_encrypted_player_password"` — habilita ver/preservar password do player.
|
|
583
|
+
- Falha de scope → **falha silenciosa** (`null`/200 ou 201), sem mensagem de erro.
|
|
584
|
+
|
|
585
|
+
### 8.3 Superfícies de risco (verificadas no código)
|
|
586
|
+
|
|
587
|
+
**DROP sem scope (CRÍTICO):** `DatabaseRest.java:590-603` — `drop()` não verifica `SCOPE_DATABASE`. Qualquer token autenticado dropa qualquer coleção.
|
|
588
|
+
|
|
589
|
+
**`collections` e `queue/*` sem scope:** `findCollectionNames`, `findQueueInfo`, `deleteQueueClear`, `deleteQueueInfo` não verificam scope — expõem nomes de coleções internas e permitem esvaziar a fila/contadores.
|
|
590
|
+
|
|
591
|
+
**Injeção de query MongoDB:** `q`, `out` e os estágios do pipeline são concatenados/passados **crus** ao Mongo (`s.append(q)` em `findAll`/`count`; `out` em `aggregate`). Qualquer expressão válida (`$where`, `$regex`, etc.) pode ser injetada.
|
|
592
|
+
```java
|
|
593
|
+
// findAll (:143-145)
|
|
594
|
+
s.append("{ $match : {");
|
|
595
|
+
if(q != null && q.trim().length() > 0) { s.append(q); }
|
|
596
|
+
s.append("}}");
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**`aggregate_test` expõe password de `player`** (sem o `$project:{password:0}` que `/aggregate` aplica).
|
|
600
|
+
|
|
601
|
+
**Criptografia AES em modo ECB:** `AesCrypt` usa `AES/ECB/PKCS5Padding` (`AesCrypt.java:49,65`) — modo **determinístico**: plaintexts iguais geram ciphertexts iguais, vazando padrões (não recomendado para dados sensíveis estruturados). Falhas de cripto são engolidas, podendo deixar campos em plaintext.
|
|
602
|
+
|
|
603
|
+
**Acesso a coleções internas:** o scope `database` não é restrito a coleções `__c`; cobre todas as coleções do tenant.
|
|
604
|
+
|
|
605
|
+
### 8.4 Isolamento por tenant
|
|
606
|
+
Garantido por `getJongoConnection()` resolvido a partir do `apiKey`. Operações de sistema usam `SystemFactory` + `checkSystemPermission`.
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## 9. Observabilidade e Troubleshooting
|
|
611
|
+
|
|
612
|
+
### 9.1 Verificações rápidas
|
|
613
|
+
```
|
|
614
|
+
GET /v3/database/collections # banco acessível? lista coleções
|
|
615
|
+
GET /v3/database/count?collection=cars__c&q= # total de docs
|
|
616
|
+
GET /v3/database/cars__c # lista (Range: items=0-10)
|
|
617
|
+
GET /v3/database/queue/info # estado da fila assíncrona
|
|
618
|
+
GET /v3/trigger?q=entity:'cars__c' # triggers configuradas p/ a coleção
|
|
619
|
+
GET /v3/database/cars__c/index # índices da coleção
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### 9.2 Erros comuns
|
|
623
|
+
|
|
624
|
+
| Sintoma | Causa provável | Diagnóstico |
|
|
625
|
+
|---|---|---|
|
|
626
|
+
| Resposta vazia com 200/201 | Token sem scope `database` ou nome de coleção curto demais | Conferir `scope` e o limiar de chars do endpoint (§5.1) |
|
|
627
|
+
| DELETE retorna 200 mas nada some | Filtro casa > 20.000 docs | `GET /count?q=...` antes; paginar o filtro |
|
|
628
|
+
| PUT criou doc novo em vez de atualizar | `_id` ausente no body | Incluir `_id` |
|
|
629
|
+
| Campo volta como string ilegível | Token de leitura sem capacidade de cripto → ciphertext retornado | Usar token com `read_encrypted_field_values` |
|
|
630
|
+
| Coleção com mix encriptado/plaintext | Algum writer escreveu sem capacidade de cripto | Garantir capacidade em **todos** os writers (§5.7) |
|
|
631
|
+
| Password do player virou `null` | Leu sem password e regravou com permissão de password | Reler com mesmo nível de permissão e incluir `password` no PUT |
|
|
632
|
+
| Bulk `async=true` "não gravou" | Sem confirmação/persistência; pode ter falhado no worker | Usar `async=false` para garantir; checar `queue/info` |
|
|
633
|
+
| Auditoria não aparece | Sem `Audit` ativo p/ `{entity,event}`, ou token sem user/player, ou bulk | Conferir coleção `audit` e o tipo do token (§6) |
|
|
634
|
+
| Aggregate lento/timeout | Sem `$match` inicial em coleção grande | Adicionar `?q=...`; `allowDiskUse` já está ligado |
|
|
635
|
+
|
|
636
|
+
### 9.3 Validar um pipeline antes de produção
|
|
637
|
+
```json
|
|
638
|
+
POST /v3/database/action_log/aggregate_test
|
|
639
|
+
[ {"$group":{"_id":"$extra.product_id","total":{"$sum":1}}}, {"$sort":{"total":-1}}, {"$limit":5} ]
|
|
640
|
+
```
|
|
641
|
+
Retorna o array completo (sem paginação). **Nunca** rode `aggregate_test` sobre `player` sem cuidar do `password` no pipeline.
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## 10. Exemplos Práticos
|
|
646
|
+
|
|
647
|
+
### 10.1 Mínimo funcional
|
|
26
648
|
```json
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"price": 50000
|
|
32
|
-
}
|
|
649
|
+
POST /v3/database/products__c
|
|
650
|
+
Authorization: Bearer <token-com-scope-database>
|
|
651
|
+
|
|
652
|
+
{ "_id": "prod001", "name": "Notebook Dell", "price": 3500.00, "category": "electronics" }
|
|
33
653
|
```
|
|
654
|
+
Resposta **201** (campos `null` removidos no modo não-strict).
|
|
34
655
|
|
|
35
|
-
###
|
|
36
|
-
|
|
37
|
-
|
|
656
|
+
### 10.2 Avançado — aggregate com materialização
|
|
657
|
+
```json
|
|
658
|
+
POST /v3/database/action_log/aggregate?out=report_sales_by_product__c&out_operation=replace
|
|
659
|
+
Range: items=0-1000
|
|
660
|
+
Authorization: Bearer <token>
|
|
38
661
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
662
|
+
[
|
|
663
|
+
{"$match": {"actionId": "purchase", "time": {"$gte": 1700000000000}}},
|
|
664
|
+
{"$group": {"_id": "$extra.product_id", "total_revenue": {"$sum": "$extra.value"}, "total_units": {"$sum": 1}}},
|
|
665
|
+
{"$sort": {"total_revenue": -1}}
|
|
666
|
+
]
|
|
667
|
+
```
|
|
668
|
+
Resposta:
|
|
669
|
+
```json
|
|
670
|
+
{ "total": 42, "out": "report_sales_by_product__c" }
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### 10.3 Bulk síncrono com triggers
|
|
674
|
+
```json
|
|
675
|
+
POST /v3/database/locations__c/bulk?async=false
|
|
676
|
+
[
|
|
677
|
+
{"_id": "loc001", "name": "São Paulo"},
|
|
678
|
+
{"name": "Belo Horizonte"}
|
|
679
|
+
]
|
|
680
|
+
```
|
|
681
|
+
O segundo objeto recebe `_id` automático. Cada item dispara `before_create`/`after_create`; a lista dispara `before_bulk`/`after_bulk`. **Sem auditoria e sem encriptação** mesmo se `locations__c` estiver em `crypt_object_field`.
|
|
682
|
+
|
|
683
|
+
### 10.4 Anti-pattern — PUT sem `_id`
|
|
684
|
+
```json
|
|
685
|
+
PUT /v3/database/products__c
|
|
686
|
+
{ "name": "Notebook Atualizado", "price": 3200.00 }
|
|
687
|
+
```
|
|
688
|
+
**Resultado:** cria documento NOVO (`_id` gerado). O original não é atualizado. **Sempre** envie `_id` no PUT — e lembre que campos omitidos serão removidos (replace, não patch).
|
|
689
|
+
|
|
690
|
+
### 10.5 Anti-pattern — DELETE em volume sem checar count
|
|
691
|
+
```
|
|
692
|
+
DELETE /v3/database/logs__c?q=year:2022
|
|
693
|
+
```
|
|
694
|
+
Se houver > 20.000 docs de 2022, retorna **200 sem deletar nada**. Cheque antes:
|
|
695
|
+
```
|
|
696
|
+
GET /v3/database/count?collection=logs__c&q=year:2022
|
|
697
|
+
```
|
|
698
|
+
Se > 20.000, fatie o filtro (ex.: `year:2022,month:{$lte:6}`) ou use `/drop` (com cautela — sem scope).
|
|
42
699
|
|
|
43
|
-
|
|
700
|
+
### 10.6 Anti-pattern — confiar em encriptação sem a capacidade
|
|
701
|
+
Escrever em coleção com `crypt_object_field` configurado usando token **sem** `read_encrypted_field_values` (nem permissão de sistema) salva os campos em **plaintext silenciosamente**. Garanta a capacidade em todos os writers.
|
|
44
702
|
|
|
45
|
-
|
|
46
|
-
- Definir `_id` único para cada objeto
|
|
47
|
-
- Criar índices para campos usados em consultas frequentes
|
|
703
|
+
---
|
|
48
704
|
|
|
49
|
-
##
|
|
705
|
+
## Checklist de Configuração
|
|
50
706
|
|
|
51
|
-
- [ ]
|
|
52
|
-
- [ ]
|
|
53
|
-
- [ ]
|
|
707
|
+
- [ ] Aplicação com scope `database` (cadastro em `/v3/security`).
|
|
708
|
+
- [ ] Nome da coleção respeita o limiar de chars do endpoint usado (§5.1): `> 3` para POST/PUT/bulk/aggregate_test, `> 2` para aggregate, `> 0` para GET-list/delete/count/index.
|
|
709
|
+
- [ ] `_id` incluído no PUT para atualizar (e ciência de que PUT é replace completo).
|
|
710
|
+
- [ ] Sufixo `__c` no destino `out` do aggregate.
|
|
711
|
+
- [ ] `GET /count` executado antes de DELETE para garantir filtro com ≤ 20.000 docs.
|
|
712
|
+
- [ ] Triggers cadastradas com `entity: '<colecao>'` se há comportamento automático esperado — e ciência de que **exceção no script não aborta** a operação.
|
|
713
|
+
- [ ] `Audit` **ativo** em `{entity, event, type}` se auditoria for necessária (e token com user/player; bulk não audita).
|
|
714
|
+
- [ ] Índices criados (`POST /{collection}/index`) para campos de filtro frequente.
|
|
715
|
+
- [ ] Todos os writers de coleção encriptada possuem a capacidade `read_encrypted_field_values` (scope) ou a permissão de sistema — caso contrário grava plaintext.
|
|
716
|
+
- [ ] `DROP`, `collections` e `queue/*` protegidos por ACL externa/gateway — **não há scope** nesses endpoints.
|
|
717
|
+
- [ ] Para dados sensíveis, ciência de que a cifra é **AES-ECB** (determinística) — avaliar mitigação na aplicação.
|
|
718
|
+
- [ ] Para bulk crítico, preferir `async=false` (sem confirmação/persistência no modo assíncrono).
|