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.
Files changed (181) 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/check-update.d.ts +5 -0
  67. package/dist/mcp/check-update.d.ts.map +1 -1
  68. package/dist/mcp/check-update.js +21 -10
  69. package/dist/mcp/check-update.js.map +1 -1
  70. package/dist/mcp/check-update.test.d.ts +2 -0
  71. package/dist/mcp/check-update.test.d.ts.map +1 -0
  72. package/dist/mcp/check-update.test.js +33 -0
  73. package/dist/mcp/check-update.test.js.map +1 -0
  74. package/dist/mcp/index.js +2 -2
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/prompts/templates.d.ts.map +1 -1
  77. package/dist/mcp/prompts/templates.js +35 -0
  78. package/dist/mcp/prompts/templates.js.map +1 -1
  79. package/dist/mcp/resources/documentation.d.ts +1 -1
  80. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  81. package/dist/mcp/resources/documentation.js +39 -3
  82. package/dist/mcp/resources/documentation.js.map +1 -1
  83. package/dist/mcp/tools/connect.d.ts.map +1 -1
  84. package/dist/mcp/tools/connect.js +18 -8
  85. package/dist/mcp/tools/connect.js.map +1 -1
  86. package/dist/mcp/tools/database.d.ts.map +1 -1
  87. package/dist/mcp/tools/database.js +59 -47
  88. package/dist/mcp/tools/database.js.map +1 -1
  89. package/dist/mcp/tools/database.test.js +2 -2
  90. package/dist/mcp/tools/database.test.js.map +1 -1
  91. package/dist/mcp/tools/delete.d.ts.map +1 -1
  92. package/dist/mcp/tools/delete.js +13 -3
  93. package/dist/mcp/tools/delete.js.map +1 -1
  94. package/dist/mcp/tools/execute.d.ts.map +1 -1
  95. package/dist/mcp/tools/execute.js +20 -9
  96. package/dist/mcp/tools/execute.js.map +1 -1
  97. package/dist/mcp/tools/folder.d.ts.map +1 -1
  98. package/dist/mcp/tools/folder.js +22 -12
  99. package/dist/mcp/tools/folder.js.map +1 -1
  100. package/dist/mcp/tools/get.d.ts.map +1 -1
  101. package/dist/mcp/tools/get.js +16 -6
  102. package/dist/mcp/tools/get.js.map +1 -1
  103. package/dist/mcp/tools/index.d.ts +1 -1
  104. package/dist/mcp/tools/index.d.ts.map +1 -1
  105. package/dist/mcp/tools/index.js +28 -1
  106. package/dist/mcp/tools/index.js.map +1 -1
  107. package/dist/mcp/tools/list.d.ts.map +1 -1
  108. package/dist/mcp/tools/list.js +38 -14
  109. package/dist/mcp/tools/list.js.map +1 -1
  110. package/dist/mcp/tools/logs.d.ts.map +1 -1
  111. package/dist/mcp/tools/logs.js +15 -5
  112. package/dist/mcp/tools/logs.js.map +1 -1
  113. package/dist/mcp/tools/save.d.ts.map +1 -1
  114. package/dist/mcp/tools/save.js +14 -4
  115. package/dist/mcp/tools/save.js.map +1 -1
  116. package/dist/mcp/tools/save.test.js +3 -3
  117. package/dist/mcp/tools/save.test.js.map +1 -1
  118. package/dist/mcp/tools/search-docs.d.ts +3 -0
  119. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  120. package/dist/mcp/tools/search-docs.js +102 -0
  121. package/dist/mcp/tools/search-docs.js.map +1 -0
  122. package/package.json +6 -2
  123. package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
  124. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  125. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  126. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  127. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  128. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  129. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  130. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
  131. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  132. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  133. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  134. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  135. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  136. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  137. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  138. package/skills/funifier/SKILL.md +88 -0
  139. package/skills/funifier/references/configure-security.md +96 -0
  140. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  141. package/skills/funifier/references/create-aggregate.md +144 -0
  142. package/skills/funifier/references/create-challenge.md +116 -0
  143. package/skills/funifier/references/create-competition.md +98 -0
  144. package/skills/funifier/references/create-crossword.md +574 -0
  145. package/skills/funifier/references/create-custom-object.md +91 -0
  146. package/skills/funifier/references/create-custom-page.md +135 -0
  147. package/skills/funifier/references/create-folder.md +104 -0
  148. package/skills/funifier/references/create-lastmile.md +643 -0
  149. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  150. package/skills/funifier/references/create-level.md +94 -0
  151. package/skills/funifier/references/create-lottery.md +913 -0
  152. package/skills/funifier/references/create-mystery.md +769 -0
  153. package/skills/funifier/references/create-notification.md +75 -0
  154. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  155. package/skills/funifier/references/create-quiz.md +98 -0
  156. package/skills/funifier/references/create-scheduler.md +141 -0
  157. package/skills/funifier/references/create-story.md +636 -0
  158. package/skills/funifier/references/create-swap.md +95 -0
  159. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  160. package/skills/funifier/references/create-virtual-good.md +96 -0
  161. package/skills/funifier/references/create-webhook.md +72 -0
  162. package/skills/funifier/references/create-websocket.md +71 -0
  163. package/skills/funifier/references/create-widget.md +76 -0
  164. package/skills/funifier/references/debug.md +87 -0
  165. package/skills/funifier/references/help.md +81 -0
  166. package/skills/funifier/references/implement-frontend.md +106 -0
  167. package/skills/funifier/references/import-csv.md +75 -0
  168. package/skills/funifier/references/manage-player.md +82 -0
  169. package/skills/funifier/references/manage-team.md +76 -0
  170. package/skills/funifier/references/upload-file.md +91 -0
  171. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  172. package/skills/funifier-create-challenge/SKILL.md +0 -88
  173. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  174. package/skills/funifier-create-level/SKILL.md +0 -87
  175. package/skills/funifier-create-quiz/SKILL.md +0 -87
  176. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  177. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  178. package/skills/funifier-debug/SKILL.md +0 -92
  179. package/skills/funifier-help/SKILL.md +0 -86
  180. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  181. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,241 +1,807 @@
1
- # Database (Database)
1
+ # `database`
2
2
 
3
- **API Endpoint:** `/v3/database`
3
+ **Acesso Studio:** `/studio/database`
4
+ **API Endpoint (tenant):** `/v3/database`
5
+ **API Endpoint (system):** `/v3/system/database`
6
+ **Coleção MongoDB:** não possui coleção própria — opera diretamente sobre **qualquer** coleção do banco da gamificação (tenant) ou do banco de sistema.
4
7
 
5
- ## O que é
8
+ ---
6
9
 
7
- Acesso direto à base de dados para operações avançadas. Oferece ferramentas para criar índices, executar comandos aggregate (MongoDB), importar dados em lote, e muito mais. Permite extrair relatórios personalizados e realizar manipulações complexas diretamente no banco de dados da gamificação. Cada gamificação tem seu próprio banco de dados.
10
+ ## 1. Visão Geral
8
11
 
9
- ## ⚠️ Requisito de Permissão Especial
12
+ O módulo `database` é uma API de acesso direto ao MongoDB. Diferente dos demais módulos da plataforma, ele **não tem schema, Entity, Repository nem DAO próprios**: os documentos trafegam como `HashMap<String, Object>` (entrada) e `BasicDBObject` (persistência), e qualquer JSON é aceito e gravado sem validação.
10
13
 
11
- O endpoint `/v3/database` exige que a palavra **`database`** esteja presente no scope da Role do token, **além** das permissões padrão (`read_all`, `write_all`, `delete_all`). Sem ela:
14
+ Papel arquitetural:
12
15
 
13
- - **POST** retorna `201` mas **NÃO persiste os dados**
14
- - **GET** retorna `200` com **corpo vazio**
15
- - **Nenhuma mensagem de erro** é exibida
16
+ - Expõe operações CRUD, MongoDB Aggregation Framework, gestão de índices, contagem, drop de coleção e bulk insert sobre coleções nativas (ex.: `player`, `action_log`, `achievement`) e coleções customizadas (convenção de sufixo `__c`).
17
+ - É o mecanismo de **bulk insert assíncrono** usado por pipelines de importação, com uma thread dedicada por tenant.
18
+ - Permite gravar o resultado de uma aggregation em outra coleção (`out`), funcionando como motor de materialização de relatórios.
16
19
 
17
- **Scope mínimo para usar database:** `read_all, write_all, delete_all, database`
20
+ Relação com outros módulos:
18
21
 
19
- Configurar em: `Studio > Security > Roles`
22
+ - **Triggers** (`com.funifier.engine.integration.trigger.TriggerManager`) são disparados em todas as operações de escrita (`before_create`, `after_create`, `before_update`, `after_update`, `before_delete`, `after_delete`, e os literais `before_bulk` / `after_bulk`).
23
+ - **Audit log** (`com.funifier.engine.audit.AuditManager`) registra POST, PUT e DELETE — mas **não** registra bulk insert.
24
+ - **Criptografia de campos AES v4.0** (`com.funifier.engine.crypt.CryptManager` + `com.funifier.engine.util.AesCrypt`) atua em POST, PUT e GET por `_id` quando o token está autorizado.
25
+ - Usa a mesma conexão `Jongo`/`MongoClient` compartilhada pela plataforma (`ManagerFactory.getJongoConnection()` no tenant; `SystemFactory.getInstance().getJongoConnection()` no sistema).
20
26
 
21
- > **Nota:** O jogador precisa fazer login novamente após alteração da Role para obter um token com as novas permissões.
27
+ Problema que resolve: dar a aplicações externas e a integrações um canal direto de leitura/escrita sobre o banco da gamificação, sem precisar de um endpoint REST dedicado por tipo de dado, ao custo de transferir para o cliente a responsabilidade por consistência, validação e segurança de query.
22
28
 
23
- ## Quando usar
29
+ ---
24
30
 
25
- - Para consultas avançadas com aggregates MongoDB
26
- - Para criar/gerenciar índices de performance
27
- - Para operações CRUD em qualquer coleção
28
- - Para inserção em massa (bulk)
29
- - Para acessar coleções customizadas (sufixo __c)
31
+ ## 2. Arquitetura e Fluxos
30
32
 
31
- ## API Endpoints
33
+ ### 2.1 Classes envolvidas
32
34
 
33
- ### Listar Coleções
34
- **Método:** GET
35
- **Endpoint:** `/v3/database/collections`
36
- **Descrição:** Lista todas as coleções existentes no banco.
35
+ | Classe | Papel |
36
+ |---|---|
37
+ | `com.funifier.rest.v3.rest.DatabaseRest` | Controller REST tenant (`/v3/database`) — todos os endpoints CRUD/aggregate/index/queue |
38
+ | `com.funifier.rest.v3.rest.system.DatabaseRest` | Controller REST sistema (`/v3/system/database`) — só índices e listagem de coleções |
39
+ | `com.funifier.engine.database.DatabaseManager` | Orquestra bulk insert (`bulkInsert`) e gerencia o processador assíncrono |
40
+ | `com.funifier.engine.database.DatabaseAsyncProcessor` | `Runnable` — thread dedicada por tenant que consome a fila de bulk |
41
+ | `com.funifier.engine.database.DatabaseAsyncTask` | DTO de task assíncrona (`task`, `collection`, `list`, `readPlayerPassword`) |
42
+ | `com.funifier.engine.util.PaginationUtil` | Paginação via estágio `$facet`; `getPageResult` (silencioso) vs `getPageResultThrowsException` |
43
+ | `com.funifier.engine.util.AesCrypt` | Criptografia AES de campos por JSONPath |
44
+ | `com.funifier.engine.crypt.CryptManager` | Resolve `CryptObjectField` (config de campos) e `CryptTenantSecret` (segredo) |
45
+ | `com.funifier.controller.database.ConnectionPool` | Pool `MongoClient`; cria índices nativos no startup do tenant (`createIndexes`) |
46
+ | `com.funifier.controller.database.ConnectionContext` | Parâmetros da conexão (URI, host, user, password, pool) |
37
47
 
38
- ### Buscar Registro por ID
39
- **Método:** GET
40
- **Endpoint:** `/v3/database/:collection/:id`
41
- **Query Params:**
42
- - `strict=true` — Preserva tipos BSON (datas como `{ $date }`, etc.)
43
- - Use `me` como ID para buscar dados do player autenticado
48
+ > **Atenção (legado comentado):** existe um endpoint `PUT /{collection}/operations` (método `updateOperations`) que aplicaria operadores MongoDB (`$inc`, `$set`, upsert, multi) a vários documentos. Ele está **inteiramente comentado** em `DatabaseRest` (linhas ~393–447) e **não existe em runtime**. Ver seção 7.
44
49
 
45
- ### Listar Dados de uma Coleção (com filtro)
46
- **Método:** GET
47
- **Endpoint:** `/v3/database/:collection`
48
- **Query Params:**
49
- - `q` — Filtro MongoDB (sintaxe Mongo, NÃO JSON). Inserido dentro de `{ $match: { <q> } }`
50
- - `strict=true` — Preserva tipos BSON
51
- - `max_results` — Máximo de resultados (default: 100)
50
+ ### 2.2 Pipeline `insert()` (POST, insert puro)
52
51
 
53
- **Header:**
54
- - `Range: items=0-19` — Paginação (Range-based)
52
+ Síncrono. Citando os métodos reais de `DatabaseRest.insert`:
55
53
 
56
- **⚠️ IMPORTANTE:** O parâmetro correto de filtro é `q`, **NÃO** `_filter`. Parâmetros como `_filter`, `_sort`, `_limit` **NÃO existem** neste endpoint e são silenciosamente ignorados.
54
+ ```
55
+ [1] gson = "true".equalsIgnoreCase(strict)
56
+ [2] Guarda: getScope().indexOf("database") != -1 E collection.trim().length() > 3
57
+ → se falso: NÃO entra no bloco; retorna 201 com body null (sem erro, sem log)
58
+ [3] Se _id ausente ou null → object.put("_id", Guid.newShortGuid()) // ObjectId.toString(), 24 hex
59
+ [4] player = authBean.getPlayerFromTokenIfExist() // claim "player" do JWT; null em Basic
60
+ [5] triggerManager.execute(null, object, collection, "before_create", player, null)
61
+ [6] CRIPTOGRAFIA (só se authBean.checkReadEncryptedValues() == true):
62
+ field = cryptManager.findObjectField(collection) // coleção crypt_object_field (tenant)
63
+ secret = cryptManager.findTenantSecretActive() // coleção crypt_tenant_secret (system)
64
+ se field.fields não vazio E secret ativa:
65
+ object = JsonUtil.fromJson(JsonUtil.toBsonStrictMode(object)) // preserva marcações BSON
66
+ object = AesCrypt.encryptFields(object, field.fields, secret.secret)
67
+ encrypted = true
68
+ [7] o = BasicDBObject.parse( marshaller.marshall(object).toString() ) // JacksonMapper → tipo BSON
69
+ [8] jongo.getCollection(collection).insert(o) // INSERT puro — falha se _id já existe
70
+ [9] triggerManager.execute(null, o, collection, "after_create", player, null)
71
+ [10] auditManager.log(collection, "create", authBean, o)
72
+ [11] Se encrypted → AesCrypt.decryptFields(o, ...) para a resposta sair em claro
73
+ [12] Retorna 201 com o documento (decriptado se aplicável)
74
+ ```
75
+
76
+ > **Comportamento silencioso crítico:** a criptografia de escrita é condicionada a `checkReadEncryptedValues()`. Um token **sem** o scope `read_encrypted_field_values` que grava em uma coleção com campos configurados em `crypt_object_field` armazena o valor **em texto puro**, lado a lado com documentos criptografados. Uma leitura posterior com a permissão tentará `AesCrypt.decrypt` sobre texto puro → exceção interna → o campo retorna **`null`**. Isso corrompe dados silenciosamente.
77
+
78
+ ### 2.3 Pipeline — `update()` (PUT, full replace / upsert)
79
+
80
+ Síncrono. Idêntico ao POST exceto:
57
81
 
58
- **Exemplos de `q`:**
59
82
  ```
60
- ?q=userId:"ricardo@funifier.com"
61
- ?q=userId:"ricardo@funifier.com", type:"daily"
62
- ?q=status:"active", price:{$gte:100}
63
- ?q=_id:"abc123"
83
+ [5'] eventos before_update / after_update (em vez de create)
84
+ [6'] mesmo bloco de criptografia
85
+ [7'] o = BasicDBObject.parse(...)
86
+ [SEGURANÇA "BRADESCO" — só coleção player E !authBean.checkReadPlayerPassword()]:
87
+ current = jongo.getCollection("player").findOne("{_id:#}", object.get("_id")).as(Player.class)
88
+ se current != null E (object.get("password") == null OU !object.containsKey("password")):
89
+ o.put("password", current.password) // restaura senha existente
90
+ [8'] jongo.getCollection(collection).save(o) // UPSERT por _id — substitui o documento inteiro
91
+ [10'] auditManager.log(collection, "update", authBean, o)
64
92
  ```
65
93
 
66
- **⚠️ Limitação:** O `GET` não suporta `$sort`. Internamente ele executa `{ $match: { <q> } }` via aggregate, sem estágios adicionais. Para queries com ordenação, use o endpoint `aggregate`.
94
+ **PUT é full replace, não PATCH:** campos ausentes no corpo são removidos do documento. Como `_id` é gerado quando ausente (passo [3]), um PUT sem `_id` **cria** um documento novo a cada chamada.
67
95
 
68
- ### Contar Registros
69
- **Método:** GET
70
- **Endpoint:** `/v3/database/count?collection=:collection&q=:query`
71
- **Descrição:** Retorna `{ total: N }`.
96
+ ### 2.4 Pipeline — `delete()` (DELETE por filtro)
72
97
 
73
- ### Criar Registro (Upsert)
74
- **Método:** POST
75
- **Endpoint:** `/v3/database/:collection`
76
- **Query Params:**
77
- - `strict=true` Para preservar tipos BSON na resposta
98
+ ```
99
+ [1] Guarda: scope database E collection.trim().length() > 0 E q != null E q.trim().length() >= 3
100
+ [2] total = jongo.getCollection(collection).count("{" + q + "}")
101
+ [3] Se total <= 20.000:
102
+ ids = jongo.getCollection(collection).distinct("_id").query("{" + q + "}").as(String.class)
103
+ Senão: ids permanece lista vazia (não é populada)
104
+ [4] Se ids != null E ids.size() > 0:
105
+ triggerManager.execute(null, ids, collection, "before_delete", null, null)
106
+ jongo.getCollection(collection).remove("{" + q + "}")
107
+ triggerManager.execute(null, ids, collection, "after_delete", null, null)
108
+ auditManager.log(collection, "delete", authBean, ids)
109
+ [5] Retorna sempre 200 com body null
110
+ ```
78
111
 
79
- **Descrição:** Se `_id` não for fornecido, gera automaticamente um GUID curto. Executa triggers `before_create` e `after_create`.
112
+ > **CORREÇÃO DE COMPORTAMENTO (contra-intuitivo e não documentado antes):** o `remove()` está **dentro** do `if(ids.size() > 0)`. Quando `total > 20.000`, `ids` nunca é populada → o bloco inteiro é pulado → **nada é excluído**. Ou seja, **um DELETE cujo filtro casa mais de 20.000 documentos é um no-op silencioso**: retorna 200, não dispara triggers, não audita e não apaga nada. Só há exclusão quando `1 ≤ casados ≤ 20.000`.
113
+ >
114
+ > O comentário no código diz "menor que 1000", mas o limite real codificado é `<= 20_000`.
115
+
116
+ #### Fluxo de exclusão — `delete()`
117
+
118
+ ```mermaid
119
+ flowchart TD
120
+ A[DELETE /v3/database/collection?q=...] --> B{"scope, collection>0 e q>=3 chars?"}
121
+ B -- nao --> Z[200 body null - nada acontece]
122
+ B -- sim --> C["total = count({q})"]
123
+ C --> D{"total <= 20.000?"}
124
+ D -- nao --> E[ids fica vazia]
125
+ E --> F{"ids.size > 0?"}
126
+ D -- sim --> G["ids = distinct _id where {q}"]
127
+ G --> F
128
+ F -- nao --> Z2[200 body null - NO-OP SILENCIOSO]
129
+ F -- sim --> H[before_delete trigger]
130
+ H --> I["remove({q})"]
131
+ I --> J[after_delete trigger]
132
+ J --> K[auditManager.log delete]
133
+ K --> L[200 body null]
134
+ ```
80
135
 
81
- **Exemplo de Body:**
82
- ```json
83
- {
84
- "year": 2010,
85
- "fuel": "gasoline",
86
- "price": 50000,
87
- "name": "Civic",
88
- "description": "Honda Civic",
89
- "brand": "honda"
90
- }
136
+ ### 2.5 Pipeline — Bulk insert (`insertBulk` → `DatabaseManager.bulkInsert`)
137
+
138
+ ```
139
+ insertBulk (controller):
140
+ [1] Guarda: scope database E collection.trim().length() > 3 E list != null E list.size() > 0
141
+ [2] result.put("total", list.size())
142
+ [3] readPlayerPassword = (collection == "player" E !authBean.checkReadPlayerPassword())
143
+ [4] async == "true" manager.getDatabaseManager().addAsyncTask(task) // enfileira e responde já
144
+ senão → manager.getDatabaseManager().bulkInsert(collection, list, readPlayerPassword)
145
+ [5] Retorna 201 com {"total": N} (se a guarda falhar → 201 com {})
146
+
147
+ DatabaseManager.bulkInsert (síncrono, item a item):
148
+ [a] triggerManager.execute(null, list, collection, "before_bulk", null, null) // player SEMPRE null
149
+ [b] para cada objeto:
150
+ object = BasicDBObject.parse( marshaller.marshall(o).toString() )
151
+ se _id ausente/null → Guid.newShortGuid()
152
+ se readPlayerPassword E player atual existe E object.password null → object.put("password", current.password)
153
+ triggerManager.execute(null, object, collection, "before_create", null, null)
154
+ c.save(object) // UPSERT, não insert
155
+ triggerManager.execute(null, object, collection, "after_create", null, null)
156
+ [c] triggerManager.execute(null, list, collection, "after_bulk", null, null)
91
157
  ```
92
158
 
93
- ### Atualizar Registro (Save/Upsert)
94
- **Método:** PUT
95
- **Endpoint:** `/v3/database/:collection`
96
- **Query Params:**
97
- - `strict=true` Para preservar tipos BSON
159
+ Observações reais:
160
+
161
+ - **Bulk usa `save()` (upsert), não `insert()`** — um `_id` repetido sobrescreve em vez de gerar erro de chave duplicada. Difere do POST simples (`insert()`).
162
+ - **Bulk não gera audit log** (nem síncrono nem assíncrono). Apenas POST/PUT/DELETE auditam.
163
+ - O `player` passado às triggers é **sempre `null`** no bulk (mesmo na variante síncrona), pois `bulkInsert` não recebe o `AuthBean`.
164
+ - `before_bulk` / `after_bulk` são **strings literais**, não constantes em `Trigger` (que só define os eventos create/update/delete).
165
+
166
+ ### 2.6 Processador assíncrono — `DatabaseAsyncProcessor`
167
+
168
+ Uma thread por tenant, criada no construtor de `DatabaseManager` e nomeada `Funifier-Database-Async-<apiKey>`. Ela é iniciada na primeira vez que o `DatabaseManager` daquele tenant é instanciado.
169
+
170
+ ```mermaid
171
+ sequenceDiagram
172
+ participant C as Cliente
173
+ participant R as DatabaseRest.insertBulk
174
+ participant M as DatabaseManager
175
+ participant Q as LinkedBlockingQueue
176
+ participant T as Thread Funifier-Database-Async
177
+ C->>R: POST /collection/bulk?async=true
178
+ R->>M: addAsyncTask(task)
179
+ M->>Q: queue.offer(task)
180
+ R-->>C: 201 {"total": N} (imediato)
181
+ loop run() enquanto thread viva
182
+ T->>Q: queue.poll()
183
+ alt task de bulk válida
184
+ T->>M: bulkInsert(collection, list, readPlayerPassword)
185
+ T->>T: lastDuration / maxDuration
186
+ else fila vazia
187
+ T->>T: Thread.sleep(100ms)
188
+ end
189
+ end
190
+ ```
191
+
192
+ A fila é uma `LinkedBlockingQueue` **sem capacidade definida** → ilimitada. Logo `remaining_capacity` em `/queue/info` é sempre `Integer.MAX_VALUE` (2147483647) — campo essencialmente decorativo. Não há persistência: tasks pendentes são perdidas em restart do processo.
193
+
194
+ #### Decisão de criptografia na escrita (POST/PUT)
195
+
196
+ ```mermaid
197
+ flowchart TD
198
+ A[POST/PUT documento] --> B{checkReadEncryptedValues?}
199
+ B -- nao --> P[Grava EM TEXTO PURO mesmo se campo for criptografado - corrompe leitura futura]
200
+ B -- sim --> C{crypt_object_field tem campos p/ a colecao?}
201
+ C -- nao --> N[Grava sem criptografia]
202
+ C -- sim --> D{crypt_tenant_secret ativa existe?}
203
+ D -- nao --> N
204
+ D -- sim --> E[AesCrypt.encryptFields nos campos]
205
+ E --> F[Grava criptografado]
206
+ F --> G[Resposta decriptada em claro]
207
+ ```
98
208
 
99
- **Descrição:** Usa `save()` do MongoDB (upsert por `_id`). Executa triggers `before_update` e `after_update`. Se `_id` não existir, gera um novo.
209
+ ### 2.7 Paginação interna (GET list e POST aggregate)
100
210
 
101
- **⚠️ Player password safety:** Se a coleção for `player` e o token não tem permissão de ler senha, o PUT automaticamente preserva a senha existente (não sobrescreve com null).
211
+ `PaginationUtil.getPageResult` / `getPageResultThrowsException` anexam ao pipeline:
102
212
 
103
- **Exemplo de Body:**
104
213
  ```json
105
- {
106
- "_id": "6553e49815865a7a732f5fc7",
107
- "year": 2010,
108
- "fuel": "gasoline",
109
- "price": 50000,
110
- "name": "Civic",
111
- "description": "Honda Civic",
112
- "brand": "honda"
113
- }
214
+ {"$facet": {
215
+ "result": [{"$skip": skip}, {"$limit": limit}],
216
+ "pagination": [{"$group": {"_id": null, "count": {"$sum": 1}}}]
217
+ }}
114
218
  ```
115
219
 
116
- ### Excluir Registro(s)
117
- **Método:** DELETE
118
- **Endpoint:** `/v3/database/:collection?q=:query`
220
+ Toda listagem executa, na mesma passagem, o slice de dados **e** a contagem total. O parse do header `Range` é feito por `getRange`:
221
+
222
+ - `Range: items=<a>-<b>` → `skip = a`, `limit = b`. **O segundo número é o tamanho da página (`$limit`), não o índice final.**
223
+ - Sem header → `skip = 0`, `limit = 100` (GET list usa `max_results`, padrão 100; aggregate usa 100 fixo).
224
+
225
+ O `Content-Range` é montado por `getRangeResponse` como `"<chave> <skip>-<limit>/<count>"`.
226
+
227
+ > **CORREÇÃO:** `Range: items=0-19` retorna **19** documentos (`$skip:0, $limit:19`) e devolve `Content-Range: items 0-19/<total>` — **não** `0-20`. O formato não segue o padrão HTTP `start-end/total`; o segundo número é a quantidade pedida.
228
+
229
+ **Diferença silenciosa importante:** `getPageResult` (usado no GET list) **engole exceções** e retorna lista vazia em caso de erro de query. `getPageResultThrowsException` (usado no aggregate) **propaga** a exceção → erro 500 para sintaxe inválida de pipeline.
230
+
231
+ ---
232
+
233
+ ## 3. Estrutura dos Objetos
234
+
235
+ ### 3.1 Documentos — schema-less
236
+
237
+ O módulo não define schema. Qualquer JSON é aceito. Comportamentos implícitos sobre `_id`:
238
+
239
+ | Situação | Comportamento |
240
+ |---|---|
241
+ | `_id` ausente/`null` no POST | Gerado via `Guid.newShortGuid()` = `new ObjectId().toString()` (24 hex chars) |
242
+ | `_id` ausente/`null` no PUT | Gerado automaticamente → o documento é **inserido**, não atualizado |
243
+ | PUT com `_id` existente | Full replace via `save()` — campos não enviados são removidos |
244
+ | Bulk com `_id` repetido | `save()` (upsert) sobrescreve; não há erro de duplicidade |
245
+ | POST com `_id` já existente | `insert()` → falha (erro Mongo de chave duplicada propaga como 500) |
246
+
247
+ **Campos com tratamento especial:**
248
+
249
+ - `password` (coleção `player`): preservado em PUT e bulk quando o token não tem `read_encrypted_player_password` e o campo veio `null`/ausente. Enviar `"password": ""` **sobrescreve** com string vazia (a proteção só age sobre `null`/ausente).
250
+ - Campos listados em `crypt_object_field`: criptografados/decriptados conforme seção 5.4.
251
+
252
+ Não há campos computados nem campos removidos silenciosamente no save além do comportamento de full-replace do PUT.
253
+
254
+ ### 3.2 `DatabaseAsyncTask`
255
+
256
+ | Campo | Tipo | Padrão | Descrição |
257
+ |---|---|---|---|
258
+ | `task` | String | — | Tipo da task; único valor processado: `"bulk"` (`TASK_BULK_INSERT`) |
259
+ | `collection` | String | — | Coleção alvo |
260
+ | `list` | `List<Object>` | — | Documentos a inserir/upsertar |
261
+ | `readPlayerPassword` | boolean | `false` | Se `true`, preserva `password` do player existente |
262
+
263
+ O `run()` só processa a task se `task=="bulk"`, `collection` não vazia e `list` não vazia; caso contrário dorme 100ms.
264
+
265
+ ### 3.3 `CryptObjectField` (config de criptografia de campos)
266
+
267
+ Armazenado na coleção **`crypt_object_field`** do banco **tenant**. (`@JsonIgnoreProperties(ignoreUnknown=true)`.)
268
+
269
+ | Campo | Tipo | Descrição |
270
+ |---|---|---|
271
+ | `_id` (mapeado para `entity`) | String | Nome da coleção à qual a criptografia se aplica |
272
+ | `fields` | `List<String>` | Caminhos JSONPath dot-notation dos campos a criptografar |
273
+
274
+ O segredo (`CryptTenantSecret`, campos `secret`/`status`/`gamification`) fica na coleção **`crypt_tenant_secret`** do banco **sistema**, resolvido por `findTenantSecretActive()` filtrando `status = "active"`.
275
+
276
+ ### 3.4 Resposta de `/queue/info`
277
+
278
+ | Campo | Tipo | Descrição |
279
+ |---|---|---|
280
+ | `size` | int | Tasks pendentes na fila |
281
+ | `remaining_capacity` | int | Capacidade restante — sempre `2147483647` (fila ilimitada) |
282
+ | `last_process_duration_ms` | long | Duração do último bulk processado |
283
+ | `max_process_duration_ms` | long | Maior duração registrada |
284
+
285
+ ---
286
+
287
+ ## 4. Endpoints
288
+
289
+ Salvo indicação contrária, autenticação é **Bearer token (ou Basic apiKey:secretKey)** e a guarda de scope é `getScope().indexOf("database") != -1`. Quando a guarda de scope/validação falha, a resposta é vazia e **sem erro** (GET → `200 null`; POST/PUT → `201 null`/`{}`; DELETE → `200 null`).
290
+
291
+ ### 4.1 `GET /v3/database/{collection}/{id}`
292
+
293
+ | Aspecto | Detalhe |
294
+ |---|---|
295
+ | Finalidade | Buscar documento por `_id` (`find()`) |
296
+ | `id = me` | Substituído por `getPlayerFromTokenIfExist()` — só funciona em token **Bearer** com claim `player`; em Basic vira `null` e cai na guarda |
297
+ | Validação | `collection.trim().length() > 3` E `id.trim().length() > 0` |
298
+ | Decriptação | Sim, se `checkReadEncryptedValues()` e campo configurado e secret ativa |
299
+
300
+ **Query params:** `strict=true` → tipos BSON preservados (ex.: datas como `{$date:...}`). Por padrão, campos `null` são removidos da resposta (`toJsonRemoveNullFields`).
301
+
302
+ ### 4.2 `GET /v3/database/{collection}`
303
+
304
+ | Aspecto | Detalhe |
305
+ |---|---|
306
+ | Finalidade | Listar com filtro opcional (`findAll()`) |
307
+ | Validação | `collection.trim().length() > 0` |
308
+
309
+ **Query params:**
119
310
 
120
- **Descrição:** O `q` precisa ter pelo menos 3 caracteres. Executa triggers `before_delete` e `after_delete` se total ≤ 20.000 registros. IDs são identificados antes da exclusão para passar aos triggers.
311
+ | Param | Tipo | Padrão | Descrição |
312
+ |---|---|---|---|
313
+ | `q` | String | — | Inserido cru em `{ $match : { <q> }}` — sintaxe Mongo direta, não JSON strict |
314
+ | `strict` | String | — | `true` → tipos BSON |
315
+ | `max_results` | String | `100` | Limite quando não há header `Range`; parse inválido → 100 silencioso |
121
316
 
122
- **Exemplos:**
317
+ **Header:** `Range: items=<skip>-<limit>` (ver 2.7). Comportamento real:
318
+
319
+ - **Não ordena** (`$sort`) — use `POST /aggregate`.
320
+ - **Não decripta** campos criptografados (só o GET por `_id` decripta).
321
+ - Erros de query são engolidos → retorna lista vazia (não 500).
322
+ - `Content-Range` no formato não-padrão `items <skip>-<limit>/<total>`.
323
+
324
+ ### 4.3 `POST /v3/database/{collection}`
325
+
326
+ | Aspecto | Detalhe |
327
+ |---|---|
328
+ | Finalidade | Inserir documento novo (`insert()`) |
329
+ | Operação Mongo | `insert()` — falha se `_id` já existe |
330
+ | Validação | `collection.trim().length() > 3` |
331
+ | Retorno | `201` com o documento (decriptado se aplicável) |
332
+
333
+ **Exemplo:**
334
+
335
+ ```http
336
+ POST /v3/database/car__c
337
+ Authorization: Bearer <token>
338
+
339
+ { "brand": "honda", "model": "civic", "year": 2023 }
123
340
  ```
124
- DELETE /v3/database/checkin__c?q=_id:"abc123"
125
- DELETE /v3/database/signup__c?q=userId:"test@test.com"
341
+
342
+ ```json
343
+ { "_id": "507f1f77bcf86cd799439011", "brand": "honda", "model": "civic", "year": 2023 }
344
+ ```
345
+
346
+ ### 4.4 `PUT /v3/database/{collection}`
347
+
348
+ | Aspecto | Detalhe |
349
+ |---|---|
350
+ | Finalidade | Full replace / upsert por `_id` (`update()`) |
351
+ | Operação Mongo | `save()` — substitui o documento inteiro |
352
+ | Validação | `collection.trim().length() > 3` |
353
+ | Retorno | `201` |
354
+
355
+ Diferenças do REST padrão: é **full replace** (campos ausentes somem); sem `_id` cria documento novo; campos `null` sobrescrevem (exceto `password` em `player`, ver 5.3).
356
+
357
+ ### 4.5 `DELETE /v3/database/{collection}?q={query}`
358
+
359
+ | Aspecto | Detalhe |
360
+ |---|---|
361
+ | Finalidade | Excluir documentos que casam o filtro (`delete()`) |
362
+ | Operação Mongo | `remove("{" + q + "}")` |
363
+ | Validação | `collection > 0` E `q.trim().length() >= 3` |
364
+ | Retorno | `200` body null (sempre) |
365
+
366
+ Comportamento real (ver 2.4): `q` < 3 chars → no-op silencioso; **`total > 20.000` → no-op silencioso** (não apaga, não dispara trigger, não audita). A validação de 3 chars é só de comprimento — `q=a:1` (3 chars) apaga tudo onde `a==1`.
367
+
368
+ ### 4.6 `POST /v3/database/{collection}/bulk`
369
+
370
+ | Aspecto | Detalhe |
371
+ |---|---|
372
+ | Finalidade | Inserir/upsertar lista (`insertBulk()`) |
373
+ | Validação | `collection > 3` E `list.size() > 0` |
374
+ | Retorno | `201` com `{"total": N}` |
375
+
376
+ **Query param:** `async=true` → enfileira no `DatabaseAsyncProcessor` e responde imediatamente. **Body:** array JSON. Comportamento real: upsert por item (`save`), triggers `before_create`/`after_create` por documento + `before_bulk`/`after_bulk` na lista, **sem audit log**, `player` sempre `null` nas triggers.
377
+
378
+ ### 4.7 `POST /v3/database/{collection}/aggregate`
379
+
380
+ | Aspecto | Detalhe |
381
+ |---|---|
382
+ | Finalidade | Executar pipeline de Aggregation (`aggregate()`) |
383
+ | Validação | `collection.trim().length() > 2` |
384
+ | Retorno | `200` paginado, ou `{"total": N, "out": "..."}` se `out` informado |
385
+
386
+ **Query params:**
387
+
388
+ | Param | Tipo | Descrição |
389
+ |---|---|---|
390
+ | `q` | String | Vira `{ $match : { <q> }}` como **primeiro** estágio, antes do body |
391
+ | `strict` | String | `true` → tipos BSON |
392
+ | `out` | String | Coleção destino — só efetivada se terminar em `__c` |
393
+ | `out_operation` | String | `replace` → `drop()` na coleção destino antes de gravar |
394
+
395
+ **Header:** `Range` para paginação (padrão `$limit:100`). Comportamento real:
396
+
397
+ - `allowDiskUse(true)` sempre ativo.
398
+ - Body é `List<Object>`; cada elemento é um estágio.
399
+ - Coleção `player` + token sem `read_encrypted_player_password` → adiciona `{$project:{password:0}}` ao **final** do pipeline.
400
+ - Com `out` válido: aplica `$skip`/`$limit` (do `Range`) e itera salvando cada doc na coleção destino via `save()`.
401
+ - Erro de pipeline → **500** (usa `getPageResultThrowsException`).
402
+ - **Não decripta** campos criptografados.
403
+
404
+ ### 4.8 `POST /v3/database/{collection}/aggregate_test`
405
+
406
+ | Aspecto | Detalhe |
407
+ |---|---|
408
+ | Finalidade | Rodar pipeline **sem paginação**, retornando a lista completa (`aggregate_test()`) |
409
+ | Validação | `collection.trim().length() > 3` |
410
+ | Diferença vs `/aggregate` | Sem `$facet`, sem `out`, sem param `q`; sem `Content-Range` |
411
+ | Retorno | `200` com array JSON cru |
412
+
413
+ > **Inconsistência de segurança:** `aggregate_test` **não** adiciona `{$project:{password:0}}`. Um token apenas com scope `database` pode ler o campo `password` da coleção `player` por este endpoint, contornando a proteção que existe em `/aggregate`. Ver seções 7 e 8.
414
+
415
+ Sem estágios no body, executa `{$match:{}}` (retorna tudo).
416
+
417
+ ### 4.9 `DELETE /v3/database/{collection}/drop`
418
+
419
+ | Aspecto | Detalhe |
420
+ |---|---|
421
+ | Finalidade | Dropar a coleção inteira (`drop()`) |
422
+ | Operação Mongo | `jongo.getCollection(collection).drop()` |
423
+ | Validação | apenas `collection.trim().length() > 0` |
424
+ | ⚠️ Scope | **NÃO verifica o scope `database`** — qualquer token cujo `apiKey` resolva a conexão pode dropar uma coleção |
425
+ | Retorno | `200` body null |
426
+
427
+ ### 4.10 `GET /v3/database/collections`
428
+
429
+ | Aspecto | Detalhe |
430
+ |---|---|
431
+ | Finalidade | Listar nomes de coleções do banco da gamificação (`findCollectionNames()`) |
432
+ | ⚠️ Scope | **NÃO verifica o scope `database`** |
433
+ | Retorno | `200` com array de strings |
434
+
435
+ ### 4.11 `GET /v3/database/count`
436
+
437
+ | Aspecto | Detalhe |
438
+ |---|---|
439
+ | Finalidade | Contar documentos (`count()`) |
440
+ | Validação | scope `database` E `collection > 0` |
441
+ | Retorno | `200` com `{"total": N}` |
442
+
443
+ **Query params:** `collection` (nome) e `q` (inserido cru em `{ <q> }`).
444
+
445
+ ### 4.12 Índices (tenant)
446
+
447
+ - `GET /v3/database/{collection}/index` — `getDBCollection().getIndexInfo()`.
448
+ - `POST /v3/database/{collection}/index` — body `Map<campo, direção>` (`{"fuel": 1}`); cria um índice por chave; retorna os índices.
449
+ - `DELETE /v3/database/{collection}/index/{id}` — `dropIndex(id)` pelo nome do índice.
450
+
451
+ Todos exigem scope `database` e `collection > 0`.
452
+
453
+ ### 4.13 Fila assíncrona
454
+
455
+ - `GET /v3/database/queue/info` — `getQueueInfo()`.
456
+ - `DELETE /v3/database/queue/clear` — `clearQueue()` (descarta tasks pendentes).
457
+ - `DELETE /v3/database/queue/info` — `clearQueueInfo()` (zera contadores de duração).
458
+
459
+ > ⚠️ Os três endpoints de fila **não verificam scope** — apenas resolvem o `ManagerFactory` pelo `apiKey`.
460
+
461
+ ### 4.14 System API (`/v3/system/database`)
462
+
463
+ Opera sobre o **banco de sistema** (`SystemFactory.getInstance().getJongoConnection()`), não o tenant. Autorização por **permissão de sistema**: `checkPermission` → `authBean.checkSystemPermission("api", "/system/database", <operation>)`; se falhar, lança `FunifierRuntimeException` → **401 UNAUTHORIZED** (diferente do silêncio do tenant).
464
+
465
+ | Endpoint | Método | Operation | Validação |
466
+ |---|---|---|---|
467
+ | `/v3/system/database/{collection}/index` | GET | `read` | `collection > 3` |
468
+ | `/v3/system/database/{collection}/index` | POST | `create` | `collection > 3` |
469
+ | `/v3/system/database/{collection}/index/{id}` | DELETE | `delete` | `collection > 3` |
470
+ | `/v3/system/database/collections` | GET | `read` | — (sem checagem de tamanho) |
471
+
472
+ > **Assimetria:** o `/v3/system/database/collections` exige permissão de sistema `read`; já o `/v3/database/collections` (tenant) **não exige nada**.
473
+
474
+ ---
475
+
476
+ ## 5. Regras de Negócio
477
+
478
+ ### 5.1 Guarda de scope por substring
479
+
480
+ A verificação é `getScope().indexOf("database") != -1` — **match de substring**, não de token. Um scope literal como `my_database_tool` satisfaz a guarda de `database`. O scope vem de: app config (Basic), role `public` (Basic só com apiKey), ou claim `scope` do JWT (Bearer). Ausência do scope **não gera erro** — gera resposta vazia (exceto onde não há checagem: `drop`, `collections`, `queue/*`).
481
+
482
+ ### 5.2 Validação de tamanho mínimo da coleção (por endpoint)
483
+
484
+ | Endpoint | Mínimo de chars |
485
+ |---|---|
486
+ | `GET /{collection}/{id}` | `> 3` |
487
+ | `GET /{collection}` | `> 0` |
488
+ | `POST /{collection}` | `> 3` |
489
+ | `PUT /{collection}` | `> 3` |
490
+ | `POST /{collection}/bulk` | `> 3` |
491
+ | `DELETE /{collection}` | `> 0` (e `q >= 3`) |
492
+ | `POST /{collection}/aggregate` | `> 2` |
493
+ | `POST /{collection}/aggregate_test` | `> 3` |
494
+ | `DELETE /{collection}/drop` | `> 0` |
495
+ | `POST/GET/DELETE .../index` | `> 0` |
496
+ | System `.../index` | `> 3` |
497
+
498
+ ### 5.3 Proteção de senha do player ("SEGURANÇA BRADESCO")
499
+
500
+ Em PUT e bulk, quando a coleção é `player` e o token **não** tem `read_encrypted_player_password`: o sistema lê o player atual e restaura `password` **se ele veio `null`/ausente** no objeto. Previne apagar a senha em updates parciais (o cliente normalmente lê o player sem ver a senha → vem `null`).
501
+
502
+ **Armadilhas:**
503
+ - Enviar `"password": ""` sobrescreve com vazio (a proteção só cobre `null`/ausente).
504
+ - A proteção **não existe** no POST simples nem no GET — só PUT e bulk.
505
+
506
+ ### 5.4 Criptografia de campos (v4.0)
507
+
508
+ Ativa quando **todas** as condições valem:
509
+ 1. `checkReadEncryptedValues()` → scope `read_encrypted_field_values` **ou** permissão de sistema `api/encrypted_field_values/read`.
510
+ 2. Existe `CryptObjectField` para a coleção (em `crypt_object_field`, banco tenant) com `fields` não vazio.
511
+ 3. Existe `CryptTenantSecret` com `status = "active"` (em `crypt_tenant_secret`, banco sistema).
512
+
513
+ Aplica-se em **POST, PUT e GET por `_id`**. **Não** se aplica em GET list, aggregate nem aggregate_test.
514
+
515
+ Algoritmo (`AesCrypt`): `AES/ECB/PKCS5Padding`, chave = `SHA-1(secret)` truncado a 16 bytes (**AES-128**), saída Base64. Campos endereçados por **JSONPath dot-notation**; cada path é lido como `String` — apenas valores string são efetivamente criptografados (numéricos/aninhados falham silenciosamente no read do JSONPath).
516
+
517
+ > Mudanças em `CryptObjectField`/`CryptTenantSecret` (via `CryptManager`) **re-processam todos os documentos** das coleções afetadas de forma síncrona: adicionar campo → criptografa em todos; remover campo/entity → decripta; trocar secret → decripta com a antiga e re-criptografa com a nova. Operação potencialmente longa e sem transação.
518
+
519
+ ### 5.5 Propagação de triggers
520
+
521
+ | Operação | Eventos | `player` repassado |
522
+ |---|---|---|
523
+ | POST | `before_create`, `after_create` | claim do token |
524
+ | PUT | `before_update`, `after_update` | claim do token |
525
+ | DELETE (1..20.000) | `before_delete`, `after_delete` (payload = lista de `_id`s) | `null` |
526
+ | Bulk (por item) | `before_create`, `after_create` | `null` |
527
+ | Bulk (lista) | `before_bulk`, `after_bulk` | `null` |
528
+
529
+ ### 5.6 Multi-tenant
530
+
531
+ Cada gamificação tem seu próprio banco MongoDB. A conexão é resolvida por `FrontController.getInstance(authBean.getApiKey()).getManagerFactory().getJongoConnection()` — o `apiKey` define o banco. Não há acesso cross-tenant por esta API. O banco de sistema é separado e só acessível via `/v3/system/database`.
532
+
533
+ ### 5.7 Consistência
534
+
535
+ Não há transação multi-documento. Bulk e aggregate-`out` gravam item a item; uma falha no meio deixa estado parcial. Triggers rodam de forma síncrona dentro do request (exceto bulk async).
536
+
537
+ ---
538
+
539
+ ## 6. Comportamentos Automáticos
540
+
541
+ | Comportamento | Trigger | Impacto | Persistência |
542
+ |---|---|---|---|
543
+ | Geração de `_id` | POST/PUT/bulk sem `_id` | `_id` = `ObjectId().toString()` (24 hex) | Persiste |
544
+ | Preservação de `password` | PUT/bulk em `player` sem scope de senha, campo `null`/ausente | Restaura senha atual do banco | Persiste |
545
+ | Full replace | PUT | Campos ausentes no corpo são removidos | Persiste |
546
+ | `$facet` de paginação | GET list e aggregate | Conta total na mesma query | Não persiste |
547
+ | `{$project:{password:0}}` | aggregate em `player` sem scope de senha | Oculta senha (só em `/aggregate`, **não** em `aggregate_test`) | Não persiste |
548
+ | `allowDiskUse(true)` | aggregate / aggregate_test | Permite passar do limite de RAM | Não persiste |
549
+ | Drop de destino | aggregate com `out_operation=replace` | `drop()` da coleção `out` antes de gravar | Persiste |
550
+ | Criação de índices nativos | startup do tenant (`ConnectionPool.createIndexes`) | Índices em ~20 coleções (player, action_log, achievement, etc.) | Persiste |
551
+ | Sleep de 100ms | fila vazia no `DatabaseAsyncProcessor` | Latência até 100ms para bulk async | Não persiste |
552
+ | Texto puro por falta de scope | POST/PUT sem `read_encrypted_field_values` em coleção criptografada | Grava em claro; leitura futura retorna `null` | Persiste (corrompido) |
553
+
554
+ ---
555
+
556
+ ## 7. Suportado vs NÃO Suportado
557
+
558
+ ### ✅ Suportado
559
+
560
+ - CRUD por `_id` em qualquer coleção (tenant).
561
+ - Listagem com filtro `q` (sintaxe Mongo) e paginação via header `Range`.
562
+ - Aggregation Framework completo com `allowDiskUse(true)`.
563
+ - Aggregation com saída em coleção customizada (`out` terminando em `__c`, `out_operation=replace`).
564
+ - Bulk insert/upsert síncrono e assíncrono.
565
+ - Gestão de índices (criar, listar, remover) no tenant e no sistema.
566
+ - Drop de coleção; contagem; listagem de coleções.
567
+ - Strict mode (tipos BSON preservados).
568
+ - Criptografia AES-128 de campos por coleção (POST/PUT/GET-by-id).
569
+ - Triggers em todas as escritas; audit em POST/PUT/DELETE.
570
+ - `id=me` no GET-by-id (apenas Bearer com claim `player`).
571
+ - Inspeção/índices do banco sistema via `/v3/system/database`.
572
+
573
+ ### ❌ NÃO Suportado / armadilhas
574
+
575
+ - **`PUT /{collection}/operations`** (update com `$inc`/`$set`/multi/upsert) — **comentado no código**, inexistente em runtime.
576
+ - **DELETE acima de 20.000 documentos** — **no-op silencioso** (não apaga nada; ver 2.4).
577
+ - **Ordenação no GET list** — sem `$sort`; use `/aggregate`.
578
+ - **Decriptação no GET list e no aggregate** — só GET-by-id decripta.
579
+ - **Audit em bulk** — bulk não gera audit log.
580
+ - **`player` real nas triggers de bulk/delete** — sempre `null`.
581
+ - **Projeção `password:0` no `aggregate_test`** — ausente → vaza senha de `player`.
582
+ - **Scope check em `drop`, `collections` (tenant) e `queue/*`** — inexistente.
583
+ - **Validação de schema** — nenhuma; qualquer JSON é gravado.
584
+ - **Transações multi-documento** — nenhuma.
585
+ - **Capacidade de fila** — ilimitada; `remaining_capacity` é decorativo; sem persistência (perde na reinicialização).
586
+ - **Criptografia de campos não-string** — JSONPath lê como `String`; números/objetos aninhados não são criptografados.
587
+ - **Params estilo `_filter`/`_sort`/`_limit`** — não existem; ignorados se enviados.
588
+
589
+ ---
590
+
591
+ ## 8. Segurança e Permissões
592
+
593
+ ### 8.1 Autenticação e scopes
594
+
595
+ | Scope / permissão | Constante | Efeito |
596
+ |---|---|---|
597
+ | `database` | `Application.SCOPE_DATABASE` | Habilita CRUD/aggregate/index/count (match por substring) |
598
+ | `read_encrypted_field_values` | `Application.SCOPE_READ_CRYPTED` | Habilita criptografia/decriptação de campos (POST/PUT/GET-by-id) |
599
+ | `read_encrypted_player_password` | `Application.SCOPE_READ_PLAYER_PASSWORD` | Desliga a proteção de senha e a projeção `password:0` |
600
+ | `api/encrypted_field_values/read` (sistema) | — | Alternativa a `read_encrypted_field_values` |
601
+ | `api/.../system/database` (sistema) | — | Necessário para a System API |
602
+
603
+ ### 8.2 Endpoints sem checagem de scope
604
+
605
+ `DELETE /{collection}/drop`, `GET /collections` e os três `/queue/*` (tenant) **não verificam scope `database`**. Qualquer requisição cujo `apiKey` resolva a conexão pode dropar coleções, listar nomes de coleções e manipular a fila assíncrona. Mitigação atual: depende exclusivamente de o `apiKey`/conexão serem válidos.
606
+
607
+ ### 8.3 Injeção de query (NoSQL)
608
+
609
+ O parâmetro `q` é concatenado **cru** na query Mongo, sem sanitização:
610
+
611
+ ```java
612
+ // GET list: s.append("{ $match : {"); s.append(q); s.append("}}");
613
+ // aggregate: query.append("{ $match : { "); query.append(q); query.append("}}");
614
+ // DELETE: jongo.getCollection(collection).remove("{" + q + "}");
615
+ // count: total = log.count("{" + ft + "}");
126
616
  ```
127
617
 
128
- ### Executar Agregações
129
- **Método:** POST
130
- **Endpoint:** `/v3/database/:collection/aggregate`
131
- **Query Params:**
132
- - `q` Filtro `$match` inicial (opcional, aplicado antes do pipeline)
133
- - `strict=true` — Preserva tipos BSON
134
- - `out` Coleção destino para gravar resultados (deve terminar em `__c`)
135
- - `out_operation=replace` — Limpa coleção destino antes de gravar
618
+ Um token com scope `database` pode montar qualquer filtro/expressão Mongo (incluindo `$where`, varreduras totais, contorno de filtros esperados). A proteção é por autenticação/scope, **não** por sanitização. O nome da coleção também é livre (`{collection}` path param), permitindo atingir qualquer coleção do tenant.
619
+
620
+ ### 8.4 Criptografia
621
+
622
+ `AES/ECB/PKCS5Padding` é **modo ECB**: determinístico, sem IV e sem autenticação (MAC). Ciphertexts iguais revelam plaintexts iguais e os campos são maleáveis. A chave deriva de `SHA-1(secret)[0:16]` (AES-128). Avalie isso ao classificar dados sensíveis armazenados via este módulo.
623
+
624
+ ### 8.5 Vazamento de senha por `aggregate_test`
625
+
626
+ Como `aggregate_test` não aplica `{$project:{password:0}}`, um token só com `database` lê `player.password` por ele, mesmo sem `read_encrypted_player_password`. A proteção de senha do `/aggregate` é, portanto, contornável.
627
+
628
+ ---
136
629
 
137
- **Header:**
138
- - `Range: items=0-19` — Paginação
630
+ ## 9. Observabilidade e Troubleshooting
139
631
 
140
- **Descrição:** O `q` é aplicado como primeiro `$match`. Os estágios do body são adicionados em sequência após ele. Usa `allowDiskUse(true)` automaticamente.
632
+ ### 9.1 Diagnóstico básico
141
633
 
142
- **Exemplo — Query com filtro e ordenação:**
634
+ ```http
635
+ GET /v3/database/collections # lista coleções (não exige scope) — se vazio, conexão/tenant suspeitos
636
+ GET /v3/database/queue/info # estado da fila de bulk async
637
+ GET /v3/database/{collection}/index # índices de uma coleção
143
638
  ```
144
- POST /v3/database/body_checkin__c/aggregate?q=userId:"ricardo@funifier.com"&strict=true
145
- Range: items=0-19
146
639
 
147
- [
148
- { "$sort": { "created": -1 } }
149
- ]
640
+ `/queue/info` saudável:
641
+
642
+ ```json
643
+ { "size": 0, "remaining_capacity": 2147483647, "last_process_duration_ms": 1234, "max_process_duration_ms": 5678 }
150
644
  ```
151
645
 
152
- **ExemploAgregação completa:**
646
+ `size` crescente indica ingestão mais rápida que o processamento (ou thread travada). Lembre: `remaining_capacity` é sempre `2147483647` (fila ilimitada) não use como sinal.
647
+
648
+ ### 9.2 Erros comuns
649
+
650
+ | Sintoma | Causa provável |
651
+ |---|---|
652
+ | POST/PUT retorna 201 mas nada grava | Token sem scope `database` (substring) |
653
+ | GET retorna `null` | Sem scope, ou coleção/`id` inválidos, ou `id=me` em token Basic |
654
+ | GET list retorna `[]` com `q` complexo | Erro de query engolido por `getPageResult` |
655
+ | Aggregate retorna 500 | Sintaxe de pipeline inválida (`getPageResultThrowsException`) |
656
+ | **DELETE não apaga nada** | `q` < 3 chars, **ou filtro casa > 20.000 docs (no-op)**, ou nenhum match |
657
+ | Bulk async: dados somem após restart | Fila em memória, sem persistência |
658
+ | Campo criptografado vem `null` na leitura | Foi gravado em texto puro por token sem `read_encrypted_field_values`, ou secret trocada |
659
+ | Senha do player apagada após PUT | Token com `read_encrypted_player_password` e `password` não enviado |
660
+
661
+ ### 9.3 Queries úteis (mongosh / via aggregate)
662
+
663
+ ```javascript
664
+ // quantos documentos um DELETE tentaria apagar (cheque o limite de 20.000!)
665
+ db.minha__c.countDocuments({ status: "active" })
666
+
667
+ // ver índices reais de uma coleção
668
+ db.player.getIndexes()
669
+
670
+ // inspecionar se um campo está criptografado (valor Base64 vs claro)
671
+ db.player.findOne({ _id: "p123" }, { email: 1 })
153
672
  ```
154
- POST /v3/database/action_log/aggregate?strict=true
155
673
 
674
+ ```http
675
+ GET /v3/database/count?collection=minha__c&q=status:"active"
676
+ POST /v3/database/action_log/aggregate
156
677
  [
157
- { "$match": { "actionId": "sell", "userId": "jerry" } },
158
- { "$group": { "_id": "$userId", "total": { "$sum": "$attributes.price" } } },
159
- { "$sort": { "total": -1 } },
160
- { "$limit": 10 }
678
+ {"$match": {"userId": "player123", "time": {"$gte": 1700000000000}}},
679
+ {"$group": {"_id": "$actionId", "total": {"$sum": 1}}},
680
+ {"$sort": {"total": -1}}
161
681
  ]
162
682
  ```
163
683
 
164
- ### Inserção em Massa (Bulk)
165
- **Método:** POST
166
- **Endpoint:** `/v3/database/:collection/bulk`
167
- **Query Params:**
168
- - `async=true` Processamento assíncrono (para grandes volumes)
684
+ ### 9.4 O que verificar quando algo não funciona
685
+
686
+ 1. O token tem o scope correto? (`database`; e `read_encrypted_*` se aplicável)
687
+ 2. O nome da coleção respeita o tamanho mínimo do endpoint (ver 5.2)?
688
+ 3. No DELETE: o filtro casa **entre 1 e 20.000** documentos?
689
+ 4. Há `CryptObjectField` + `CryptTenantSecret` ativa coerentes com o que foi gravado?
690
+ 5. A fila async está drenando (`/queue/info`)?
691
+
692
+ ---
693
+
694
+ ## 10. Exemplos Práticos
695
+
696
+ ### 10.1 Mínimo — inserir em coleção customizada
697
+
698
+ ```http
699
+ POST /v3/database/car__c
700
+ Authorization: Bearer <token_com_scope_database>
701
+ Content-Type: application/json
702
+
703
+ { "brand": "honda", "model": "civic", "year": 2023 }
704
+ ```
169
705
 
170
- **Exemplo de Body:**
171
706
  ```json
707
+ { "_id": "507f1f77bcf86cd799439011", "brand": "honda", "model": "civic", "year": 2023 }
708
+ ```
709
+
710
+ ### 10.2 Listagem paginada com filtro
711
+
712
+ ```http
713
+ GET /v3/database/action_log?q=userId:"player123",actionId:"buy"
714
+ Range: items=0-19
715
+ Authorization: Bearer <token>
716
+ ```
717
+
718
+ Retorna **19** documentos. Header de resposta:
719
+
720
+ ```
721
+ Content-Range: items 0-19/47
722
+ ```
723
+
724
+ (Para 20 documentos peça `items=0-20`. O segundo número é o tamanho da página, não o índice final.)
725
+
726
+ ### 10.3 Avançado — aggregate materializando relatório em `__c`
727
+
728
+ ```http
729
+ POST /v3/database/action_log/aggregate?out=sales_summary__c&out_operation=replace&strict=true
730
+ Range: items=0-9999
731
+ Authorization: Bearer <token>
732
+
172
733
  [
173
- { "_id": "car001", "year": 2010, "price": 60000, "name": "Honda Civic", "brand": "honda" },
174
- { "_id": "car002", "year": 2012, "price": 70000, "name": "Toyota Corolla", "brand": "toyota" }
734
+ {"$match": {"actionId": "purchase", "time": {"$gte": 1704067200000}}},
735
+ {"$group": {"_id": {"player": "$userId"}, "total": {"$sum": "$attributes.amount"}, "count": {"$sum": 1}}},
736
+ {"$sort": {"total": -1}}
175
737
  ]
176
738
  ```
177
739
 
178
- ### Drop Collection
179
- **Método:** DELETE
180
- **Endpoint:** `/v3/database/:collection/drop`
181
- **⚠️ CUIDADO:** Remove a coleção inteira!
740
+ ```json
741
+ { "total": 128, "out": "sales_summary__c" }
742
+ ```
743
+
744
+ ### 10.4 Bulk assíncrono
745
+
746
+ ```http
747
+ POST /v3/database/product__c/bulk?async=true
748
+ Authorization: Bearer <token>
182
749
 
183
- ### Listar Índices
184
- **Método:** GET
185
- **Endpoint:** `/v3/database/:collection/index`
750
+ [
751
+ {"_id": "prod001", "name": "Widget A", "price": 29.99},
752
+ {"_id": "prod002", "name": "Widget B", "price": 49.99}
753
+ ]
754
+ ```
186
755
 
187
- ### Criar Índice
188
- **Método:** POST
189
- **Endpoint:** `/v3/database/:collection/index`
756
+ Resposta imediata:
190
757
 
191
- **Exemplo de Body:**
192
758
  ```json
193
- { "fuel": 1 }
759
+ { "total": 2 }
194
760
  ```
195
761
 
196
- ### Excluir Índice
197
- **Método:** DELETE
198
- **Endpoint:** `/v3/database/:collection/index/:index_name`
762
+ Depois acompanhe `GET /v3/database/queue/info`. Reenviar o mesmo `_id` faz **upsert** (sobrescreve), não erro.
199
763
 
200
- ## Paginação
764
+ ### 10.5 Anti-patterns
201
765
 
202
- Usa header `Range` no padrão HTTP Range:
203
- ```
204
- Range: items=0-9 (primeiros 10)
205
- Range: items=10-19 (próximos 10)
206
- Range: items=0-99 (primeiros 100)
766
+ **NÃO FAÇA — PUT sem `_id` esperando atualizar:**
767
+
768
+ ```http
769
+ PUT /v3/database/config__c
770
+ { "key": "max_players", "value": 100 }
207
771
  ```
208
772
 
209
- Resposta inclui header `Content-Range` com total de registros.
773
+ Sem `_id`, cada PUT **cria** um documento novo (ObjectId gerado). Sempre inclua `_id` para atualizar.
210
774
 
211
- Default sem Range: retorna até 100 registros.
775
+ **NÃO FAÇA DELETE contando com filtro amplo:**
212
776
 
213
- ## Strict Mode (BSON Types)
777
+ ```http
778
+ DELETE /v3/database/logs__c?q=ttl:1
779
+ ```
214
780
 
215
- Quando `strict=true`:
216
- - **Datas** são retornadas como `{ "$date": "2026-03-06T10:00:00.000Z" }` (ISO-8601)
217
- - **Sem strict**, datas podem vir como timestamps numéricos ou strings
781
+ Se o filtro casar **mais de 20.000** documentos, **nada é apagado** (no-op silencioso, 200). Apague em lotes (`q` que limite a ≤ 20.000 por chamada) ou use `drop` se a coleção inteira puder ir embora.
218
782
 
219
- Para **gravar** datas tipadas, enviar como:
220
- ```json
221
- { "created": { "$date": "2026-03-06T10:00:00.000Z" } }
783
+ **NÃO FAÇA ler dados sensíveis por GET list ou aggregate esperando decriptação:**
784
+
785
+ ```http
786
+ GET /v3/database/player?q=email:"user@example.com"
222
787
  ```
223
788
 
224
- > **Regra:** Sempre use `strict=true` em GETs para preservar tipos BSON. Sem ele, datas viram strings/números e quebram queries.
789
+ GET list e aggregate **não decriptam**. Use `GET /v3/database/player/{id}` com `read_encrypted_field_values`.
790
+
791
+ **NÃO FAÇA — gravar em coleção criptografada com token sem `read_encrypted_field_values`:**
225
792
 
226
- ## Criptografia de Campos (v4.0)
793
+ O valor vai em **texto puro** e a leitura futura (com o scope) retorna `null`. Use sempre o mesmo nível de permissão para ler e gravar a mesma coleção.
227
794
 
228
- Se habilitada (Studio > Security > Field Encryption):
229
- - Campos marcados são criptografados no POST/PUT e descriptografados no GET
230
- - Requer token com permissão `readEncryptedValues`
231
- - Usa AES com chave secreta do tenant
795
+ ---
232
796
 
233
- ## Validações e Testes
797
+ ## Checklist de Configuração
234
798
 
235
- - [ ] Listar coleções retorna resultado
236
- - [ ] CRUD funciona na coleção alvo
237
- - [ ] Filtro `q` funciona corretamente
238
- - [ ] Aggregate com `q` + pipeline retorna dados filtrados e ordenados
239
- - [ ] Bulk insert registra múltiplos itens
240
- - [ ] Índices são criados/removidos corretamente
241
- - [ ] `strict=true` preserva tipos BSON
799
+ - [ ] Token tem o scope `database` (lembre: a checagem é por **substring**).
800
+ - [ ] Coleções customizadas usam sufixo `__c`; `out` de aggregate **deve** terminar em `__c`.
801
+ - [ ] DELETE: o filtro `q` tem ≥ 3 chars **e** casa **no máximo 20.000** documentos (acima disso é no-op).
802
+ - [ ] Para preservar `password` em `player`: garanta o mesmo nível de permissão (`read_encrypted_player_password`) ao ler e gravar.
803
+ - [ ] Para campos criptografados: ler **e** gravar com `read_encrypted_field_values`; `CryptObjectField` (tenant) e `CryptTenantSecret` ativa (sistema) coerentes.
804
+ - [ ] `drop`, `collections` (tenant) e `queue/*` não checam scope — proteja esses tokens de clientes não confiáveis.
805
+ - [ ] Para bulk grande: prefira `async=true` e monitore `/queue/info` (lembrando que a fila não persiste em restart).
806
+ - [ ] Não dependa de `aggregate_test` para dados sensíveis — ele não oculta `password`.
807
+ - [ ] Paginação: o segundo número do `Range`/`Content-Range` é a **quantidade**, não o índice final.