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.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +3 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,241 +1,807 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `database`
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
-
|
|
8
|
+
---
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
## 1. Visão Geral
|
|
8
11
|
|
|
9
|
-
|
|
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
|
-
|
|
14
|
+
Papel arquitetural:
|
|
12
15
|
|
|
13
|
-
-
|
|
14
|
-
- **
|
|
15
|
-
-
|
|
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
|
-
|
|
20
|
+
Relação com outros módulos:
|
|
18
21
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
+
---
|
|
24
30
|
|
|
25
|
-
|
|
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
|
-
|
|
33
|
+
### 2.1 Classes envolvidas
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
54
|
-
- `Range: items=0-19` — Paginação (Range-based)
|
|
52
|
+
Síncrono. Citando os métodos reais de `DatabaseRest.insert`:
|
|
55
53
|
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
**
|
|
96
|
-
**
|
|
97
|
-
- `
|
|
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
|
-
|
|
209
|
+
### 2.7 Paginação interna (GET list e POST aggregate)
|
|
100
210
|
|
|
101
|
-
|
|
211
|
+
`PaginationUtil.getPageResult` / `getPageResultThrowsException` anexam ao pipeline:
|
|
102
212
|
|
|
103
|
-
**Exemplo de Body:**
|
|
104
213
|
```json
|
|
105
|
-
{
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
- `Range: items=0-19` — Paginação
|
|
630
|
+
## 9. Observabilidade e Troubleshooting
|
|
139
631
|
|
|
140
|
-
|
|
632
|
+
### 9.1 Diagnóstico básico
|
|
141
633
|
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
158
|
-
{
|
|
159
|
-
{
|
|
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
|
-
###
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
{
|
|
174
|
-
{ "_id": "
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
750
|
+
[
|
|
751
|
+
{"_id": "prod001", "name": "Widget A", "price": 29.99},
|
|
752
|
+
{"_id": "prod002", "name": "Widget B", "price": 49.99}
|
|
753
|
+
]
|
|
754
|
+
```
|
|
186
755
|
|
|
187
|
-
|
|
188
|
-
**Método:** POST
|
|
189
|
-
**Endpoint:** `/v3/database/:collection/index`
|
|
756
|
+
Resposta imediata:
|
|
190
757
|
|
|
191
|
-
**Exemplo de Body:**
|
|
192
758
|
```json
|
|
193
|
-
{ "
|
|
759
|
+
{ "total": 2 }
|
|
194
760
|
```
|
|
195
761
|
|
|
196
|
-
|
|
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
|
-
|
|
764
|
+
### 10.5 Anti-patterns
|
|
201
765
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
773
|
+
Sem `_id`, cada PUT **cria** um documento novo (ObjectId gerado). Sempre inclua `_id` para atualizar.
|
|
210
774
|
|
|
211
|
-
|
|
775
|
+
**NÃO FAÇA — DELETE contando com filtro amplo:**
|
|
212
776
|
|
|
213
|
-
|
|
777
|
+
```http
|
|
778
|
+
DELETE /v3/database/logs__c?q=ttl:1
|
|
779
|
+
```
|
|
214
780
|
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
797
|
+
## Checklist de Configuração
|
|
234
798
|
|
|
235
|
-
- [ ]
|
|
236
|
-
- [ ]
|
|
237
|
-
- [ ]
|
|
238
|
-
- [ ]
|
|
239
|
-
- [ ]
|
|
240
|
-
- [ ]
|
|
241
|
-
- [ ] `
|
|
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.
|