funifier-mcp 0.2.26 → 0.2.27

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.
Files changed (170) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/index.js +2 -2
  67. package/dist/mcp/index.js.map +1 -1
  68. package/dist/mcp/resources/documentation.d.ts +1 -1
  69. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  70. package/dist/mcp/resources/documentation.js +39 -3
  71. package/dist/mcp/resources/documentation.js.map +1 -1
  72. package/dist/mcp/tools/connect.d.ts.map +1 -1
  73. package/dist/mcp/tools/connect.js +18 -8
  74. package/dist/mcp/tools/connect.js.map +1 -1
  75. package/dist/mcp/tools/database.d.ts.map +1 -1
  76. package/dist/mcp/tools/database.js +59 -47
  77. package/dist/mcp/tools/database.js.map +1 -1
  78. package/dist/mcp/tools/database.test.js +2 -2
  79. package/dist/mcp/tools/database.test.js.map +1 -1
  80. package/dist/mcp/tools/delete.d.ts.map +1 -1
  81. package/dist/mcp/tools/delete.js +13 -3
  82. package/dist/mcp/tools/delete.js.map +1 -1
  83. package/dist/mcp/tools/execute.d.ts.map +1 -1
  84. package/dist/mcp/tools/execute.js +20 -9
  85. package/dist/mcp/tools/execute.js.map +1 -1
  86. package/dist/mcp/tools/folder.d.ts.map +1 -1
  87. package/dist/mcp/tools/folder.js +22 -12
  88. package/dist/mcp/tools/folder.js.map +1 -1
  89. package/dist/mcp/tools/get.d.ts.map +1 -1
  90. package/dist/mcp/tools/get.js +16 -6
  91. package/dist/mcp/tools/get.js.map +1 -1
  92. package/dist/mcp/tools/index.d.ts +1 -1
  93. package/dist/mcp/tools/index.d.ts.map +1 -1
  94. package/dist/mcp/tools/index.js +3 -1
  95. package/dist/mcp/tools/index.js.map +1 -1
  96. package/dist/mcp/tools/list.d.ts.map +1 -1
  97. package/dist/mcp/tools/list.js +38 -14
  98. package/dist/mcp/tools/list.js.map +1 -1
  99. package/dist/mcp/tools/logs.d.ts.map +1 -1
  100. package/dist/mcp/tools/logs.js +15 -5
  101. package/dist/mcp/tools/logs.js.map +1 -1
  102. package/dist/mcp/tools/save.d.ts.map +1 -1
  103. package/dist/mcp/tools/save.js +14 -4
  104. package/dist/mcp/tools/save.js.map +1 -1
  105. package/dist/mcp/tools/save.test.js +3 -3
  106. package/dist/mcp/tools/save.test.js.map +1 -1
  107. package/dist/mcp/tools/search-docs.d.ts +3 -0
  108. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  109. package/dist/mcp/tools/search-docs.js +102 -0
  110. package/dist/mcp/tools/search-docs.js.map +1 -0
  111. package/package.json +6 -2
  112. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  113. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  114. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  115. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  116. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  117. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  118. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  119. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  120. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  121. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  122. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  123. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  124. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  125. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  126. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  127. package/skills/funifier/SKILL.md +88 -0
  128. package/skills/funifier/references/configure-security.md +96 -0
  129. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  130. package/skills/funifier/references/create-aggregate.md +144 -0
  131. package/skills/funifier/references/create-challenge.md +116 -0
  132. package/skills/funifier/references/create-competition.md +98 -0
  133. package/skills/funifier/references/create-crossword.md +574 -0
  134. package/skills/funifier/references/create-custom-object.md +91 -0
  135. package/skills/funifier/references/create-custom-page.md +135 -0
  136. package/skills/funifier/references/create-folder.md +104 -0
  137. package/skills/funifier/references/create-lastmile.md +643 -0
  138. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  139. package/skills/funifier/references/create-level.md +94 -0
  140. package/skills/funifier/references/create-lottery.md +913 -0
  141. package/skills/funifier/references/create-mystery.md +769 -0
  142. package/skills/funifier/references/create-notification.md +75 -0
  143. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  144. package/skills/funifier/references/create-quiz.md +98 -0
  145. package/skills/funifier/references/create-scheduler.md +141 -0
  146. package/skills/funifier/references/create-story.md +636 -0
  147. package/skills/funifier/references/create-swap.md +95 -0
  148. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  149. package/skills/funifier/references/create-virtual-good.md +96 -0
  150. package/skills/funifier/references/create-webhook.md +72 -0
  151. package/skills/funifier/references/create-websocket.md +71 -0
  152. package/skills/funifier/references/create-widget.md +76 -0
  153. package/skills/funifier/references/debug.md +87 -0
  154. package/skills/funifier/references/help.md +81 -0
  155. package/skills/funifier/references/implement-frontend.md +106 -0
  156. package/skills/funifier/references/import-csv.md +75 -0
  157. package/skills/funifier/references/manage-player.md +82 -0
  158. package/skills/funifier/references/manage-team.md +76 -0
  159. package/skills/funifier/references/upload-file.md +91 -0
  160. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  161. package/skills/funifier-create-challenge/SKILL.md +0 -88
  162. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  163. package/skills/funifier-create-level/SKILL.md +0 -87
  164. package/skills/funifier-create-quiz/SKILL.md +0 -87
  165. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  166. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  167. package/skills/funifier-debug/SKILL.md +0 -92
  168. package/skills/funifier-help/SKILL.md +0 -86
  169. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  170. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,53 +1,718 @@
1
- # Custom Object (Objeto Customizado)
1
+ # `custom-object` Database (Objeto Customizado)
2
2
 
3
- ## O que é
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
- Criação de objetos personalizados para necessidades específicas. Permite criar e gerenciar coleções personalizadas no banco de dados (sufixo `__c`), que podem ser acessadas via API, triggers, schedulers e páginas personalizadas. Ideal para quando os módulos padrão não atendem a todos os requisitos do projeto.
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
- ## Quando usar
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
- - Para cadastrar entidades específicas do negócio (produtos, imóveis, etc.)
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
- ## API Endpoints
20
+ ## 1. Visão Geral
14
21
 
15
- Objetos customizados são acessados via o módulo Database:
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
- ### Listar Objetos
18
- **Método:** GET
19
- **Endpoint:** `/v3/database/<nome>__c`
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
- ### Criar Objeto
22
- **Método:** POST
23
- **Endpoint:** `/v3/database/<nome>__c`
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
- **Exemplo (coleção car__c):**
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
- "_id": "car001",
29
- "name": "Civic",
30
- "brand": "Honda",
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
- ### Atualizar Objeto
36
- **Método:** PUT
37
- **Endpoint:** `/v3/database/<nome>__c`
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
- ### Excluir Objeto
40
- **Método:** DELETE
41
- **Endpoint:** `/v3/database/<nome>__c?q=_id:'car001'`
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
- ## Boas Práticas
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
- - Sempre usar sufixo `__c` no nome da coleção
46
- - Definir `_id` único para cada objeto
47
- - Criar índices para campos usados em consultas frequentes
703
+ ---
48
704
 
49
- ## Validações e Testes
705
+ ## Checklist de Configuração
50
706
 
51
- - [ ] Coleção customizada aparece em GET /v3/database/collections
52
- - [ ] CRUD funciona corretamente
53
- - [ ] Triggers com entity `<nome>__c` funcionam
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).