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,390 +1,1045 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `folder`
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/folder`
|
|
4
|
-
**API
|
|
4
|
+
**API Endpoint:** `/v3/folder`
|
|
5
|
+
**Coleções MongoDB:** `folder`, `folder_content`, `folder_content_type`, `folder_log`
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Visão Geral
|
|
10
|
+
|
|
11
|
+
O módulo `folder` implementa um sistema de trilhas hierárquicas (cursos, módulos, aulas, conteúdos). Cinco artefatos formam o módulo:
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
- `folder` — nó da árvore (pasta).
|
|
14
|
+
- `folder_content` — vínculo entre uma pasta e um item de conteúdo (referência a documento em outra coleção, ou item próprio em coleção de formulário).
|
|
15
|
+
- `folder_content_type` — schema do tipo de conteúdo (define a coleção real onde os dados ficam e se é `repository` ou `formulary`).
|
|
16
|
+
- `folder_log` — registro de progresso do jogador em um `folder_content`.
|
|
17
|
+
- `Inside` — DTO transitório calculado em runtime (árvore + progresso); **não persiste em MongoDB**.
|
|
9
18
|
|
|
10
|
-
|
|
19
|
+
O módulo resolve três problemas:
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- Para organizar conteúdos em hierarquias com controle de ordem e desbloqueio
|
|
21
|
+
1. **Organização hierárquica arbitrária** — pastas aninhadas sem limite de profundidade, com ordem (`position`) e visibilidade (`active`) por pasta.
|
|
22
|
+
2. **Vinculação de conteúdo** — duas estratégias: `repository` (a pasta aponta para um item já existente em outra coleção) ou `formulary` (o item é criado e mantido junto ao vínculo, em uma coleção definida pelo `ContentType`).
|
|
23
|
+
3. **Rastreamento de progresso por jogador** — log granular por item (`folder_log`) e agregação recursiva por pasta (`Inside.percent/done/total`), com disparo de triggers `folder_log` e `folder_progress` por nível do breadcrumb.
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
**Limitações estruturais confirmadas no código:**
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
| `folder_content` | Vínculo entre pasta e item de conteúdo |
|
|
23
|
-
| `folder_content_type` | Schema de um tipo de conteúdo (video, text, quiz…) |
|
|
24
|
-
| `folder_log` | Histórico de progresso do jogador por item |
|
|
27
|
+
- Não há DAO separado: toda persistência é feita pelo próprio `FolderManager` via Jongo.
|
|
28
|
+
- Não há scheduler nem job de background — todo o processamento é síncrono dentro da requisição HTTP.
|
|
29
|
+
- O recurso `folder_content_type` **não tem endpoints próprios** em `FolderRest`. Sua manipulação é feita pela API genérica `/v3/database/folder_content_type`.
|
|
25
30
|
|
|
26
31
|
---
|
|
27
32
|
|
|
28
|
-
##
|
|
33
|
+
## 2. Arquitetura e Fluxos
|
|
34
|
+
|
|
35
|
+
### 2.1 Componentes
|
|
36
|
+
|
|
37
|
+
| Camada | Classe | Responsabilidade |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| REST | `FolderRest` (`src/main/java/com/funifier/rest/v3/rest/FolderRest.java`) | Endpoints HTTP, auth via `AuthBean`, dispatch para `FolderManager`. |
|
|
40
|
+
| Domínio | `FolderManager` (`src/main/java/com/funifier/engine/folder/FolderManager.java`) | Toda a lógica de negócio, persistência e disparo de triggers. |
|
|
41
|
+
| Modelos | `Folder`, `Content`, `ContentType`, `FolderLog`, `UnlockPolicy`, `Inside` | POJOs serializados via Jackson (`@JsonIgnoreProperties(ignoreUnknown=true)`). |
|
|
42
|
+
| Integração | `ManagerFactory.getTriggerManager()` | Disparo dos triggers `folder_log` e `folder_progress`. |
|
|
43
|
+
| Infra | `ConnectionPool.createIndexes()` | Cria índices MongoDB no startup. |
|
|
44
|
+
|
|
45
|
+
Acesso: `FrontController.getInstance(authBean.getApiKey()).getManagerFactory().getFolderManager()`. Cada `apiKey` mapeia para uma instância isolada de `ManagerFactory` (isolamento por tenant).
|
|
46
|
+
|
|
47
|
+
### 2.2 Pipeline `insertFolder` (`POST /v3/folder`)
|
|
48
|
+
|
|
49
|
+
`FolderManager.insertFolder(Folder folder)` — linhas 31–39:
|
|
50
|
+
|
|
51
|
+
1. Se `folder == null` → retorna sem fazer nada.
|
|
52
|
+
2. Obtém `MongoCollection` da coleção `folder`.
|
|
53
|
+
3. Se `_id` for `null` ou só whitespace → gera `Guid.shortTimeMillis()`.
|
|
54
|
+
4. Se `position == null` → `position = 0`.
|
|
55
|
+
5. Se `active == null` → `active = true`.
|
|
56
|
+
6. `c.save(folder)` — Jongo faz upsert por `_id`.
|
|
57
|
+
|
|
58
|
+
> Sem validação de campos obrigatórios. Sem disparo de trigger. Sem checagem de existência do `parent`. O endpoint serve tanto para criar quanto para atualizar (full-replace por `_id`).
|
|
59
|
+
|
|
60
|
+
### 2.3 Pipeline `insertLog` (`POST /v3/folder/log`) — critical path
|
|
61
|
+
|
|
62
|
+
`FolderManager.insertLog(FolderLog log)` — linhas 556–646:
|
|
63
|
+
|
|
64
|
+
1. Se `log == null` → retorna sem fazer nada.
|
|
65
|
+
2. Busca `Content content = folder_content.findOne({_id: log.item})`.
|
|
66
|
+
3. **Se `content == null` → retorna silenciosamente** (sem erro, sem save). O endpoint ainda devolve HTTP 201 com o `log` original.
|
|
67
|
+
4. Se `log._id` for `null` ou whitespace → gera `Guid.newShortGuid()`.
|
|
68
|
+
5. Procura log ativo existente: `folder_log.findOne({player: log.player, item: log.item, finished: {$exists: false}})`.
|
|
69
|
+
- Se **existe** → `log._id = current._id`; `log.started = current.started`.
|
|
70
|
+
- Se **não existe** → `log.started = now`.
|
|
71
|
+
6. Normalização status/percent (linhas 574–584):
|
|
72
|
+
- `status == "done"` → `percent = 100.0`.
|
|
73
|
+
- `percent != null && percent >= 100` → `status = "done"`; `percent = 100.0`.
|
|
74
|
+
- `status == "done" && finished == null` → `finished = now`.
|
|
75
|
+
7. Se `started == null` ainda assim → `started = now` (fallback de segurança).
|
|
76
|
+
8. **Cálculo de diff** (linha 594):
|
|
77
|
+
|
|
78
|
+
```java
|
|
79
|
+
double diff = (current == null) ? log.percent
|
|
80
|
+
: (current != null) ? log.percent - current.percent
|
|
81
|
+
: 0;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- Auto-unboxing de `Double` para `double`. Se `log.percent == null` ou `current.percent == null`, lança `NullPointerException` **antes** de salvar (ver Seção 7).
|
|
85
|
+
9. Se `diff > 0`:
|
|
86
|
+
- `path = breadcrumbFolder(content.parent)` (lista do raiz até o pai do content).
|
|
87
|
+
- `before = playerProgress(path.get(0)._id, log.player)` — snapshot do progresso da raiz **antes** do save.
|
|
88
|
+
10. `triggerManager.execute(log._id, log, "folder_log", BEFORE_CREATE, log.player, null)`.
|
|
89
|
+
11. `c.save(log)` — upsert do log.
|
|
90
|
+
12. `triggerManager.execute(log._id, log, "folder_log", AFTER_CREATE, log.player, null)`.
|
|
91
|
+
13. Se `diff > 0` (mesma condição de antes):
|
|
92
|
+
- `after = playerProgress(path.get(0)._id, log.player)` — snapshot **depois** do save.
|
|
93
|
+
- `Collections.reverse(path)` — inverte para `[content.parent, ..., raiz]`.
|
|
94
|
+
- Para cada `folder` no path: compara `folderAfter.percent` com `folderBefore.percent`. Se aumentou, dispara `triggerManager.execute(folder._id, payloadProgress, "folder_progress", AFTER_CREATE, log.player, null)`.
|
|
95
|
+
|
|
96
|
+
**Payload do trigger `folder_progress`:**
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"_id": "<folderId>",
|
|
101
|
+
"player": "<playerId>",
|
|
102
|
+
"folder": { /* Folder completo */ },
|
|
103
|
+
"progress": { /* Inside com percent/done/total */ },
|
|
104
|
+
"previous_percent": 60.0,
|
|
105
|
+
"current_percent": 75.0,
|
|
106
|
+
"time": "<Date now>"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
29
109
|
|
|
30
|
-
###
|
|
110
|
+
### 2.4 Pipeline `playerProgress` (`POST /v3/folder/progress`)
|
|
31
111
|
|
|
32
|
-
|
|
112
|
+
`FolderManager.playerProgress(folder, player)` — linhas 132–149:
|
|
33
113
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| `active` | boolean | Não (default true) | false = oculta da árvore |
|
|
42
|
-
| `unlock_policy` | UnlockPolicy | Não | Regra de desbloqueio condicional |
|
|
43
|
-
| `extra` | object | Não | Campos livres adicionais |
|
|
114
|
+
1. `Folder current = folder.findOne({_id: folder})`.
|
|
115
|
+
2. Se `current == null` → retorna `null`.
|
|
116
|
+
3. `Inside inside = insideFolder(folder)` — monta árvore estrutural (sem progresso).
|
|
117
|
+
4. `inside.player = player`.
|
|
118
|
+
5. `playerProgressCascade(inside, player, root=inside)` — popula `total`, `done`, `percent`, `time`, `is_unlocked` em toda a árvore.
|
|
119
|
+
6. Se `inside.time == null` → `inside.time = getMaxTimeInside(inside, null)` (fallback com bugs latentes — ver Seção 7).
|
|
120
|
+
7. Retorna `Inside`.
|
|
44
121
|
|
|
45
|
-
|
|
122
|
+
Após o `FolderManager.playerProgress`, **`FolderRest.progress` (linhas 119–129) sempre dispara o trigger `folder_progress`** para o folder consultado, mesmo sem mudança de percentual:
|
|
46
123
|
|
|
47
|
-
|
|
124
|
+
```java
|
|
125
|
+
manager.getTriggerManager().execute(folder, progress, "folder_progress", Trigger.EVENT_AFTER_CREATE, player, null);
|
|
126
|
+
```
|
|
48
127
|
|
|
49
|
-
|
|
50
|
-
|-------|------|---------|-----------|
|
|
51
|
-
| `type` | string | `"progress"` | Único tipo suportado |
|
|
52
|
-
| `folder_ref` | string | `"prev"` ou ID | `"prev"` = pasta irmã anterior; ID = pasta específica |
|
|
53
|
-
| `min_percent` | number | 0–100 | Percentual mínimo exigido |
|
|
128
|
+
Neste caminho `previous_percent == current_percent == inside.percent` — não há "antes/depois" no endpoint de leitura.
|
|
54
129
|
|
|
55
|
-
|
|
130
|
+
### 2.5 Pipeline `playerProgressCascade` (recursivo)
|
|
56
131
|
|
|
57
|
-
|
|
132
|
+
`FolderManager.playerProgressCascade(inside, player, root)` — linhas 151–234:
|
|
58
133
|
|
|
59
|
-
|
|
134
|
+
```
|
|
135
|
+
contentIds = inside.getContentIds(cascade=true) // todos os ids de content na sub-árvore
|
|
136
|
+
inside.total = contentIds.size()
|
|
137
|
+
inside.done = 0
|
|
138
|
+
inside.percent = 0.0
|
|
139
|
+
|
|
140
|
+
se contentIds não vazio:
|
|
141
|
+
aggregate folder_log:
|
|
142
|
+
{$match:{ item:{$in:contentIds}, player:#, status:"done" }}
|
|
143
|
+
{$group:{ _id:"$item", time:{$max:"$finished"} }}
|
|
144
|
+
{$group:{ _id:null, done:{$sum:1}, time:{$max:"$time"} }}
|
|
145
|
+
→ inside.done, inside.percent = (done/total)*100, inside.time
|
|
146
|
+
|
|
147
|
+
para cada item m em inside.items:
|
|
148
|
+
se m.folder == false (content):
|
|
149
|
+
log = folder_log.findOne({item:m._id, player:#, status:"done"})
|
|
150
|
+
se log:
|
|
151
|
+
m.percent = 100.0; m.time = log.finished
|
|
152
|
+
senão:
|
|
153
|
+
log2 = folder_log.findOne({item:m._id, player:#})
|
|
154
|
+
m.percent = (log2 != null && log2.percent != null) ? log2.percent : 0.0
|
|
155
|
+
senão se m.folder == true:
|
|
156
|
+
playerProgressCascade(m, player, root) // recursão
|
|
157
|
+
|
|
158
|
+
# pós-processamento: is_unlocked apenas para folders
|
|
159
|
+
onlyFolders = filtra items onde folder == true
|
|
160
|
+
para cada child em onlyFolders (com índice i):
|
|
161
|
+
policy = resolveUnlockPolicy(child._id)
|
|
162
|
+
se policy == null OR policy.type != "progress":
|
|
163
|
+
child.is_unlocked = true
|
|
164
|
+
continue
|
|
165
|
+
ref = policy.folder_ref ?: "prev"
|
|
166
|
+
min = policy.min_percent ?: 0
|
|
167
|
+
se ref == "prev":
|
|
168
|
+
se i == 0 → child.is_unlocked = true
|
|
169
|
+
senão → child.is_unlocked = (onlyFolders[i-1].percent ?: 0) >= min
|
|
170
|
+
senão (ref é folderId):
|
|
171
|
+
target = root.getInsideById(ref, cascade=true)
|
|
172
|
+
se target == null → child.is_unlocked = true
|
|
173
|
+
senão → child.is_unlocked = (target.percent ?: 0) >= min
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 2.6 Pipeline `deleteFolderCascade` (recursivo)
|
|
177
|
+
|
|
178
|
+
`FolderManager.deleteFolderCascade(id)` — linhas 324–364:
|
|
179
|
+
|
|
180
|
+
1. Se `id` nulo/whitespace → retorna.
|
|
181
|
+
2. `folder.remove({_id: id})`.
|
|
182
|
+
3. Para cada `Content` em `folder_content.find({parent: id})`:
|
|
183
|
+
- Resolve `ContentType` por `content.type`.
|
|
184
|
+
- Se `type != null && type.input == "formulary"` → `db[type.entity].remove({_id: content.content})` (apaga o dado real).
|
|
185
|
+
- `folder_log.remove({item: content._id})`.
|
|
186
|
+
4. `folder_content.remove({parent: id})` (após o loop).
|
|
187
|
+
5. Para cada `Folder` em `folder.find({parent: id})` → recursão `deleteFolderCascade(child._id)`.
|
|
188
|
+
|
|
189
|
+
> Sem transação. Sem trigger. Sem checagem de `active` — sub-folders inativos também caem na cascata.
|
|
190
|
+
|
|
191
|
+
### 2.7 Diagrama — fluxo de `insertLog`
|
|
192
|
+
|
|
193
|
+
```mermaid
|
|
194
|
+
flowchart LR
|
|
195
|
+
A[POST /v3/folder/log] --> B{content existe?}
|
|
196
|
+
B -- Não --> Z[retorno silencioso<br/>HTTP 201 sem save]
|
|
197
|
+
B -- Sim --> C{log ativo<br/>player+item+!finished?}
|
|
198
|
+
C -- Sim --> D[reutiliza _id e started]
|
|
199
|
+
C -- Não --> E[started = now]
|
|
200
|
+
D --> F[normaliza<br/>status/percent]
|
|
201
|
+
E --> F
|
|
202
|
+
F --> G[diff = log.percent - currentPercent]
|
|
203
|
+
G --> H{diff > 0?}
|
|
204
|
+
H -- Sim --> I[breadcrumb +<br/>playerProgress before]
|
|
205
|
+
H -- Não --> J[BEFORE_CREATE folder_log]
|
|
206
|
+
I --> J
|
|
207
|
+
J --> K[c.save log]
|
|
208
|
+
K --> L[AFTER_CREATE folder_log]
|
|
209
|
+
L --> M{diff > 0?}
|
|
210
|
+
M -- Não --> END[fim]
|
|
211
|
+
M -- Sim --> N[playerProgress after<br/>+ reverse path]
|
|
212
|
+
N --> O[para cada folder no path:<br/>se after.percent > before.percent<br/>→ folder_progress AFTER_CREATE]
|
|
213
|
+
O --> END
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 2.8 Diagrama — interação entre módulos no `insertLog`
|
|
217
|
+
|
|
218
|
+
```mermaid
|
|
219
|
+
sequenceDiagram
|
|
220
|
+
participant C as Cliente
|
|
221
|
+
participant FR as FolderRest
|
|
222
|
+
participant FM as FolderManager
|
|
223
|
+
participant DB as MongoDB
|
|
224
|
+
participant TM as TriggerManager
|
|
225
|
+
|
|
226
|
+
C->>FR: POST /v3/folder/log
|
|
227
|
+
FR->>FM: insertLog(log)
|
|
228
|
+
FM->>DB: findOne folder_content {_id: item}
|
|
229
|
+
alt content não existe
|
|
230
|
+
FM-->>FR: retorno silencioso
|
|
231
|
+
FR-->>C: HTTP 201 (sem save)
|
|
232
|
+
else content existe
|
|
233
|
+
FM->>DB: findOne folder_log {player, item, !finished}
|
|
234
|
+
FM->>FM: normaliza status/percent
|
|
235
|
+
opt diff > 0
|
|
236
|
+
FM->>FM: breadcrumbFolder(content.parent)
|
|
237
|
+
FM->>FM: playerProgress(root, player) → before
|
|
238
|
+
end
|
|
239
|
+
FM->>TM: execute folder_log BEFORE_CREATE
|
|
240
|
+
FM->>DB: save(log)
|
|
241
|
+
FM->>TM: execute folder_log AFTER_CREATE
|
|
242
|
+
opt diff > 0
|
|
243
|
+
FM->>FM: playerProgress(root, player) → after
|
|
244
|
+
loop cada folder no path (do mais próximo ao raiz)
|
|
245
|
+
FM->>TM: execute folder_progress AFTER_CREATE
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
FM-->>FR: retorno
|
|
249
|
+
FR-->>C: HTTP 201 + log JSON
|
|
250
|
+
end
|
|
251
|
+
```
|
|
60
252
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
253
|
+
### 2.9 Diagrama — ciclo de vida de `FolderLog`
|
|
254
|
+
|
|
255
|
+
```mermaid
|
|
256
|
+
stateDiagram-v2
|
|
257
|
+
[*] --> Iniciado: insertLog (content existe)<br/>started = now
|
|
258
|
+
Iniciado --> EmAndamento: insertLog com percent < 100<br/>(reutiliza _id; finished não setado)
|
|
259
|
+
EmAndamento --> EmAndamento: insertLog com percent < 100
|
|
260
|
+
Iniciado --> Concluido: insertLog com status="done"<br/>ou percent ≥ 100
|
|
261
|
+
EmAndamento --> Concluido: insertLog com status="done"<br/>ou percent ≥ 100
|
|
262
|
+
Concluido --> [*]: deleteLog<br/>(sem recálculo de progresso pai)
|
|
263
|
+
Iniciado --> [*]: deleteLog
|
|
264
|
+
EmAndamento --> [*]: deleteLog
|
|
265
|
+
|
|
266
|
+
note right of Iniciado: percent pode ser null<br/>finished == null<br/>idempotência ativa
|
|
267
|
+
note right of Concluido: percent = 100.0<br/>finished = now<br/>idempotência se quebra<br/>(novo log começa do zero)
|
|
268
|
+
```
|
|
70
269
|
|
|
71
|
-
|
|
270
|
+
> **Idempotência:** "ativa" enquanto `finished` é nulo. Após `status="done"`, novos `insertLog` para o mesmo `(player, item)` criam um log paralelo (a query `{finished: {$exists:false}}` deixa de bater).
|
|
72
271
|
|
|
73
|
-
|
|
272
|
+
---
|
|
74
273
|
|
|
75
|
-
|
|
76
|
-
|-------|------|-------------|-----------|
|
|
77
|
-
| `_id` | string | **Sim** | Identificador (ex: `"video"`, `"text"`, `"quiz"`) |
|
|
78
|
-
| `entity` | string | **Sim** | Nome da coleção MongoDB onde os dados reais ficam |
|
|
79
|
-
| `input` | string | **Sim** | `"repository"` ou `"formulary"` (ver abaixo) |
|
|
80
|
-
| `title` | string | Não | Label de exibição |
|
|
81
|
-
| `image` | string | Não | URL de ícone |
|
|
82
|
-
| `slug` | string | Não | Slug para URL |
|
|
83
|
-
| `attributes` | array | Não | Campos do formulário (quando `input=formulary`) |
|
|
274
|
+
## 3. Estrutura dos Objetos
|
|
84
275
|
|
|
85
|
-
|
|
86
|
-
**`input=formulary`:** o conteúdo cria um novo item inline na coleção `entity`. ⚠️ Ao deletar o `FolderContent`, o dado real também é deletado.
|
|
276
|
+
### 3.1 `Folder` — coleção `folder`
|
|
87
277
|
|
|
88
|
-
|
|
278
|
+
Definido em `src/main/java/com/funifier/engine/folder/Folder.java`.
|
|
89
279
|
|
|
90
|
-
|
|
280
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
281
|
+
|---|---|---|---|---|
|
|
282
|
+
| `_id` | `String` | `Guid.shortTimeMillis()` | — | Auto-gerado se nulo/whitespace no insert. |
|
|
283
|
+
| `type` | `String` | — | Não | Categoria livre (ex: `"course"`, `"module"`, `"lesson"`). Usado também como `Inside.type` no retorno. |
|
|
284
|
+
| `parent` | `String` | — | Não | `_id` do folder pai. Ausência ou string vazia = nó raiz. |
|
|
285
|
+
| `title` | `String` | — | Não (sem validação) | Título exibível. |
|
|
286
|
+
| `position` | `Integer` (wrapper) | `0` no insert | — | Ordenação entre irmãos. Default `0` se nulo no insert. |
|
|
287
|
+
| `active` | `Boolean` (wrapper) | `true` no insert | — | Default `true` se nulo. `false` esconde da árvore (`insideFolder`/`moveFolder`); **não bloqueia `deleteFolderCascade`**. |
|
|
288
|
+
| `unlock_policy` | `UnlockPolicy` | — | Não | Política de desbloqueio condicional (ver 3.2). Herdada via `parent` quando ausente. |
|
|
289
|
+
| `extra` | `Map<String,Object>` | `new HashMap<>()` | Não | Campos livres. |
|
|
91
290
|
|
|
92
|
-
|
|
93
|
-
|-------|------|-------------|-----------|
|
|
94
|
-
| `_id` | string | Não (auto) | ID único |
|
|
95
|
-
| `item` | string | **Sim** | `_id` do `FolderContent` |
|
|
96
|
-
| `player` | string | **Sim** | ID do jogador |
|
|
97
|
-
| `status` | string | Não | `"done"` para concluído |
|
|
98
|
-
| `percent` | number | Não | 0.0–100.0 |
|
|
99
|
-
| `started` | Date | Não (auto) | Preenchido automaticamente |
|
|
100
|
-
| `finished` | Date | Não (auto) | Preenchido quando concluído |
|
|
101
|
-
| `extra` | object | Não | Campos livres (ex: `date`) |
|
|
291
|
+
**Campos desconhecidos no JSON de entrada são silenciosamente descartados** (`@JsonIgnoreProperties(ignoreUnknown=true)`). Não existem campos deprecated ou comentados nesta classe.
|
|
102
292
|
|
|
103
|
-
|
|
293
|
+
**Comportamento de `active` por operação:**
|
|
104
294
|
|
|
105
|
-
|
|
295
|
+
| Operação | Considera `active`? |
|
|
296
|
+
|---|---|
|
|
297
|
+
| `insideFolder` / `insideFolderCascade` | `{$or:[{active:true},{active:{$exists:false}}]}` — esconde apenas `active:false` explícito. |
|
|
298
|
+
| `moveFolder` (irmãos) | Mesmo filtro acima — `active:false` não é renumerado nem movido. |
|
|
299
|
+
| `deleteFolderCascade` | **Ignora** `active`. Sub-folders inativos também são removidos. |
|
|
300
|
+
| `breadcrumbFolder` | Ignora — busca puramente por `_id`/`parent`. |
|
|
301
|
+
| `playerProgress` | Usa `insideFolder`, portanto herda o filtro. |
|
|
106
302
|
|
|
107
|
-
|
|
108
|
-
|-------|------|-----------|
|
|
109
|
-
| `_id` | string | ID do nó |
|
|
110
|
-
| `folder` | boolean | `true`=pasta, `false`=conteúdo |
|
|
111
|
-
| `title` | string | Título |
|
|
112
|
-
| `type` | string | Tipo |
|
|
113
|
-
| `content` | string | ID do item real (apenas em conteúdos) |
|
|
114
|
-
| `items` | Inside[] | Filhos (sub-pastas e conteúdos), ordenados por `position` |
|
|
115
|
-
| `player` | string | ID do jogador (apenas em `progress`) |
|
|
116
|
-
| `total` | number | Total de conteúdos na sub-árvore (apenas em `progress`) |
|
|
117
|
-
| `done` | number | Conteúdos concluídos (apenas em `progress`) |
|
|
118
|
-
| `percent` | number | % de conclusão (apenas em `progress`) |
|
|
119
|
-
| `time` | Date | Data da última conclusão na sub-árvore |
|
|
120
|
-
| `is_unlocked` | boolean | Se a pasta está desbloqueada (null para conteúdos) |
|
|
303
|
+
### 3.2 `UnlockPolicy` — embutido em `Folder.unlock_policy`
|
|
121
304
|
|
|
122
|
-
|
|
305
|
+
Definido em `src/main/java/com/funifier/engine/folder/UnlockPolicy.java`.
|
|
123
306
|
|
|
124
|
-
|
|
307
|
+
| Campo | Tipo | Valores | Descrição |
|
|
308
|
+
|---|---|---|---|
|
|
309
|
+
| `type` | `String` | apenas `"progress"` é interpretado | Qualquer outro valor (ou `null`) → `is_unlocked = true`. |
|
|
310
|
+
| `folder_ref` | `String` | `"prev"` ou ID de folder | `"prev"` = folder irmão anterior na lista ordenada. Outro valor é tratado como ID de folder e procurado em toda a árvore via `root.getInsideById(ref, cascade=true)`. |
|
|
311
|
+
| `min_percent` | `Integer` (wrapper) | `0`–`100` | Percentual mínimo do alvo. Default `0` se nulo. Comparação é `target.percent >= min_percent` (inclusivo). |
|
|
125
312
|
|
|
126
|
-
|
|
313
|
+
**Resolução via `resolveUnlockPolicy(folderId)`** — linhas 236–248:
|
|
127
314
|
|
|
128
|
-
#### Criar Pasta
|
|
129
|
-
**POST** `/v3/folder`
|
|
130
|
-
```json
|
|
131
|
-
{ "title": "Módulo 1", "parent": "raiz_id", "position": 0 }
|
|
132
315
|
```
|
|
133
|
-
|
|
316
|
+
f = folder.findOne({_id: folderId})
|
|
317
|
+
enquanto f != null:
|
|
318
|
+
se f.unlock_policy != null → return f.unlock_policy
|
|
319
|
+
se f.parent é nulo/whitespace → break
|
|
320
|
+
f = folder.findOne({_id: f.parent})
|
|
321
|
+
return null
|
|
322
|
+
```
|
|
134
323
|
|
|
135
|
-
|
|
136
|
-
**GET** `/v3/folder/:id`
|
|
324
|
+
Sobe a árvore até achar o primeiro ancestral com política. Sem política em nenhum ancestral → `is_unlocked = true`.
|
|
137
325
|
|
|
138
|
-
|
|
139
|
-
**GET** `/v3/database/folder`
|
|
326
|
+
### 3.3 `Content` — coleção `folder_content`
|
|
140
327
|
|
|
141
|
-
|
|
142
|
-
**DELETE** `/v3/folder/:id`
|
|
328
|
+
Definido em `src/main/java/com/funifier/engine/folder/Content.java`.
|
|
143
329
|
|
|
144
|
-
|
|
330
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
331
|
+
|---|---|---|---|---|
|
|
332
|
+
| `_id` | `String` | `Guid.shortTimeMillis()` | — | Auto-gerado se nulo/whitespace no insert. |
|
|
333
|
+
| `type` | `String` | — | Sim (runtime) | `_id` do `ContentType`. Sem isso, `findContentData` retorna `null` e `title` automático não funciona. |
|
|
334
|
+
| `content` | `String` | — | Condicional | `_id` do item real em `ContentType.entity`. Obrigatório quando `input=repository`. Em `formulary` aponta para o documento criado por outro fluxo. |
|
|
335
|
+
| `parent` | `String` | — | Sim (runtime) | `_id` do `Folder` pai. Sem validação de existência. |
|
|
336
|
+
| `title` | `String` | — | Não | Calculado automaticamente em `insertContent` quando nulo, OU sempre quando `ContentType.input == "formulary"`. |
|
|
337
|
+
| `extra` | `Map<String,Object>` | `new HashMap<>()` | Não | Campos livres. |
|
|
338
|
+
| `position` | `int` (primitivo) | `0` | — | Sempre presente (primitivo). Inválido enviar `null`. |
|
|
145
339
|
|
|
146
|
-
|
|
147
|
-
**DELETE** `/v3/folder?q=parent:"<id>"`
|
|
340
|
+
> **Diferença em relação a `Folder`:** `Content` **não tem campo `active`** — não pode ser escondido sem ser deletado. `moveContent` consequentemente **não filtra por `active`** (e nem poderia).
|
|
148
341
|
|
|
149
|
-
|
|
150
|
-
**GET** `/v3/folder/:id/move?direction=up` ou `direction=down`
|
|
342
|
+
**Auto-cálculo de `title` em `insertContent`** (linhas 399–415):
|
|
151
343
|
|
|
152
|
-
#### Árvore Completa
|
|
153
|
-
**POST** `/v3/folder/inside`
|
|
154
|
-
```json
|
|
155
|
-
{ "folder": "<id>" }
|
|
156
344
|
```
|
|
157
|
-
|
|
345
|
+
ContentType ct = folder_content_type.findOne({_id: content.type})
|
|
346
|
+
se content.title == null OU (ct != null && ct.input == "formulary"):
|
|
347
|
+
raw = findContentData(content, jongo) // busca em ct.entity
|
|
348
|
+
se raw != null:
|
|
349
|
+
title = raw["title"] ?: raw["name"] ?: String.valueOf(raw["_id"])
|
|
350
|
+
content.title = title
|
|
351
|
+
```
|
|
158
352
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
353
|
+
> Em `formulary`, o título do `Content` é **sempre sobrescrito** pelo título do item real — qualquer `title` enviado pelo cliente é descartado.
|
|
354
|
+
|
|
355
|
+
### 3.4 `ContentType` — coleção `folder_content_type`
|
|
356
|
+
|
|
357
|
+
Definido em `src/main/java/com/funifier/engine/folder/ContentType.java`.
|
|
358
|
+
|
|
359
|
+
| Campo | Tipo | Descrição |
|
|
360
|
+
|---|---|---|
|
|
361
|
+
| `_id` | `String` | Identificador (ex: `"video"`, `"quiz"`). |
|
|
362
|
+
| `entity` | `String` | Nome da coleção MongoDB que armazena o item real (ex: `"video__c"`). |
|
|
363
|
+
| `title` | `String` | Label de exibição (não é usado nas decisões internas). |
|
|
364
|
+
| `image` | `String` | URL de ícone. |
|
|
365
|
+
| `input` | `String` | `"repository"` ou `"formulary"`. Apenas essas duas constantes são interpretadas pelo código. |
|
|
366
|
+
| `slug` | `String` | Slug para URL. |
|
|
367
|
+
| `attributes` | `List<HashMap>` | Definição do formulário para `input=formulary`. Não é validada pelo backend de folder. |
|
|
368
|
+
|
|
369
|
+
**Constantes no código:**
|
|
370
|
+
|
|
371
|
+
```java
|
|
372
|
+
ContentType.INPUT_REPOSITORY = "repository"
|
|
373
|
+
ContentType.INPUT_FORMULARY = "formulary"
|
|
163
374
|
```
|
|
164
|
-
Retorna árvore `Inside` com `total`, `done`, `percent`, `time`, `is_unlocked` por nó. Também **dispara o trigger `folder_progress`**.
|
|
165
375
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
376
|
+
**Impacto no delete:** quando `input == "formulary"`, `deleteContent`/`deleteFolderCascade` removem **também** o documento real em `ContentType.entity` (`{_id: content.content}`). Em `repository`, apenas o vínculo é removido.
|
|
377
|
+
|
|
378
|
+
> **Não há endpoints REST para `ContentType` em `FolderRest`.** Use a API genérica `/v3/database/folder_content_type` para CRUD.
|
|
379
|
+
|
|
380
|
+
### 3.5 `FolderLog` — coleção `folder_log`
|
|
381
|
+
|
|
382
|
+
Definido em `src/main/java/com/funifier/engine/folder/FolderLog.java`.
|
|
383
|
+
|
|
384
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
385
|
+
|---|---|---|---|---|
|
|
386
|
+
| `_id` | `String` | `Guid.newShortGuid()` | — | Auto-gerado se nulo/whitespace. Sobrescrito pelo `_id` do log ativo existente quando há idempotência. |
|
|
387
|
+
| `item` | `String` | — | Sim | `_id` do `Content`. Sem isso, `insertLog` aborta silenciosamente. |
|
|
388
|
+
| `player` | `String` | — | Sim | ID do jogador. |
|
|
389
|
+
| `status` | `String` | — | Não | Única constante interpretada: `STATUS_DONE = "done"`. Outros valores são preservados mas ignorados pela lógica. |
|
|
390
|
+
| `started` | `Date` | `now` no insert | — | Preenchido automaticamente; preservado de log ativo existente. |
|
|
391
|
+
| `finished` | `Date` | `now` quando `status="done"` | — | Preenchido apenas quando o log fica `done`. Logs em andamento têm `finished == null` — esta é a chave operacional de idempotência. |
|
|
392
|
+
| `percent` | `Double` (wrapper) | — | Não | `0.0`–`100.0`. **Pode ser `null`** — gatilho de NPE em `insertLog` (Seção 7). |
|
|
393
|
+
| `extra` | `HashMap<String,Object>` | — | Não | Campos livres. Não é inicializado (pode ser `null`). |
|
|
394
|
+
|
|
395
|
+
**Constantes:**
|
|
396
|
+
|
|
397
|
+
```java
|
|
398
|
+
FolderLog.STATUS_DONE = "done"
|
|
170
399
|
```
|
|
171
|
-
|
|
400
|
+
|
|
401
|
+
**Normalização aplicada pelo servidor em `insertLog`:**
|
|
402
|
+
|
|
403
|
+
| Entrada | Resultado |
|
|
404
|
+
|---|---|
|
|
405
|
+
| `status == "done"` | `percent = 100.0` (qualquer valor enviado é sobrescrito) |
|
|
406
|
+
| `percent != null && percent >= 100` | `status = "done"`, `percent = 100.0` |
|
|
407
|
+
| `status == "done" && finished == null` | `finished = now` |
|
|
408
|
+
| `started == null` no final do pipeline | `started = now` |
|
|
409
|
+
|
|
410
|
+
**Chave operacional de idempotência:** `(player, item, finished:{$exists:false})`. Existe no máximo um log "ativo" por par `(player, item)`. Após `status="done"` (i.e., `finished` setado), o próximo `insertLog` criará um log novo.
|
|
411
|
+
|
|
412
|
+
### 3.6 `Inside` — DTO de resposta (não persistido)
|
|
413
|
+
|
|
414
|
+
Definido em `src/main/java/com/funifier/engine/folder/Inside.java`.
|
|
415
|
+
|
|
416
|
+
| Campo | Tipo | Descrição |
|
|
417
|
+
|---|---|---|
|
|
418
|
+
| `_id` | `String` | `_id` do nó (folder ou content). |
|
|
419
|
+
| `folder` | `boolean` (primitivo) | `true` = pasta; `false` = conteúdo. |
|
|
420
|
+
| `title` | `String` | Vem de `Folder.title` ou `Content.title`. |
|
|
421
|
+
| `type` | `String` | Vem de `Folder.type` ou `Content.type` (ContentType id no caso de conteúdo). |
|
|
422
|
+
| `content` | `String` | Apenas em conteúdos — copia `Content.content` (id do item real). |
|
|
423
|
+
| `items` | `List<Inside>` | Filhos ordenados: **folders primeiro** (todos), **depois conteúdos** (todos). Cada grupo ordenado por `position`. |
|
|
424
|
+
| `player` | `String` | Setado apenas no `playerProgress`. |
|
|
425
|
+
| `total` | `Integer` | Total de conteúdos na sub-árvore (contagem recursiva). |
|
|
426
|
+
| `done` | `Integer` | Conteúdos com pelo menos um `folder_log` `status="done"` para o player. |
|
|
427
|
+
| `percent` | `Double` | Folder: `(done/total)*100`. Content: `100.0` se há log `done`, senão `log.percent ?: 0.0`. |
|
|
428
|
+
| `time` | `Date` | Maior `finished` entre os logs `done` da sub-árvore. Em folders sem nenhum log `done`, cai no fallback `getMaxTimeInside` (com bugs — ver Seção 7). |
|
|
429
|
+
| `is_unlocked` | `Boolean` | `true`/`false` para folders; **`null` para conteúdos**. |
|
|
430
|
+
|
|
431
|
+
**Métodos utilitários internos (package-private):**
|
|
432
|
+
|
|
433
|
+
- `getContentIds(boolean cascade)` — coleta `_id` de conteúdos descendentes; se `cascade=false`, apenas os conteúdos diretos do `inside`.
|
|
434
|
+
- `getInsideById(String id, boolean cascade)` — busca o próprio `Inside` ou um filho/descendente por `_id`. Retorna o **primeiro match** (DFS, conteúdo antes de descer em folder filho que casa pelo id — ver código linhas 70–90).
|
|
172
435
|
|
|
173
436
|
---
|
|
174
437
|
|
|
175
|
-
|
|
438
|
+
## 4. Endpoints
|
|
176
439
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
440
|
+
Todos os endpoints exigem `Bearer token` (via `AuthBean`). O `apiKey` extraído do token resolve o `FrontController` (tenant) e o `ManagerFactory` correspondente.
|
|
441
|
+
|
|
442
|
+
### 4.1 `POST /v3/folder`
|
|
443
|
+
|
|
444
|
+
| Aspecto | Detalhe |
|
|
445
|
+
|---|---|
|
|
446
|
+
| Finalidade | Criar ou atualizar pasta (upsert por `_id`). |
|
|
447
|
+
| Body | `Folder` JSON. |
|
|
448
|
+
| Comportamento | Full replace via `MongoCollection.save()`. Defaults aplicados quando `position` ou `active` forem `null`. |
|
|
449
|
+
| Status | `201 Created`. |
|
|
450
|
+
| Triggers | Nenhum. |
|
|
451
|
+
|
|
452
|
+
**Exemplo:**
|
|
453
|
+
|
|
454
|
+
```http
|
|
455
|
+
POST /v3/folder
|
|
456
|
+
Authorization: Bearer <token>
|
|
457
|
+
Content-Type: application/json
|
|
458
|
+
|
|
459
|
+
{ "title": "Módulo 1 — Introdução", "parent": "trail001", "position": 0 }
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
```http
|
|
463
|
+
HTTP/1.1 201 Created
|
|
464
|
+
{ "_id": "ABC123xyz", "title": "Módulo 1 — Introdução", "parent": "trail001", "position": 0, "active": true }
|
|
181
465
|
```
|
|
182
466
|
|
|
183
|
-
|
|
184
|
-
**GET** `/v3/folder/content/:id`
|
|
467
|
+
### 4.2 `GET /v3/folder/{id}`
|
|
185
468
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
469
|
+
| Aspecto | Detalhe |
|
|
470
|
+
|---|---|
|
|
471
|
+
| Finalidade | Buscar pasta por `_id`. |
|
|
472
|
+
| Resposta | `200 OK` com `Folder` ou body vazio se `findOne` retornar `null`. |
|
|
189
473
|
|
|
190
|
-
|
|
191
|
-
**POST** `/v3/folder/content/aggregate?folder=<id>&cascade=true&append_data=true`
|
|
192
|
-
Body: pipeline MongoDB opcional (array).
|
|
193
|
-
- `cascade=true`: inclui conteúdos de sub-pastas
|
|
194
|
-
- `append_data=true`: adiciona campo `data` com o objeto real de cada conteúdo
|
|
474
|
+
### 4.3 `GET /v3/folder/{id}/move?direction=up|down`
|
|
195
475
|
|
|
196
|
-
|
|
197
|
-
|
|
476
|
+
| Aspecto | Detalhe |
|
|
477
|
+
|---|---|
|
|
478
|
+
| Finalidade | Mover folder uma posição para cima/baixo entre seus irmãos `active=true`. |
|
|
479
|
+
| Status | `204 No Content`. |
|
|
198
480
|
|
|
199
|
-
|
|
200
|
-
**GET** `/v3/folder/content/:id/move?direction=up` ou `direction=down`
|
|
481
|
+
**Algoritmo (`moveFolder` linhas 251–295):**
|
|
201
482
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
483
|
+
1. `findOne({_id: id})` — se `null`, no-op.
|
|
484
|
+
2. Lista todos os irmãos `{parent: o.parent, active:true|inexistente}` ordenados por `position`.
|
|
485
|
+
3. **Renumera todos os irmãos** sequencialmente `0, 1, 2…` e salva cada um (mesmo os que não mudam).
|
|
486
|
+
4. Localiza índice do alvo; identifica `antes` (`index-1`) e `depois` (`index+1`).
|
|
487
|
+
5. Se `direction == "up"` e `index > 0`: troca `position` do alvo com o de `antes`, salva ambos.
|
|
488
|
+
6. Se `direction == "down"` e `index < size-1`: troca com `depois`.
|
|
489
|
+
7. Qualquer outro valor de `direction` (ou `null`): apenas a renumeração (passo 3) é aplicada.
|
|
205
490
|
|
|
206
|
-
|
|
207
|
-
**DELETE** `/v3/folder/content?q=parent:"<id>"`
|
|
491
|
+
> Efeito colateral: o endpoint **sempre re-grava todos os irmãos** mesmo quando o alvo já está nas pontas. Não é idempotente em termos de I/O.
|
|
208
492
|
|
|
209
|
-
|
|
493
|
+
### 4.4 `DELETE /v3/folder/{id}`
|
|
210
494
|
|
|
211
|
-
|
|
495
|
+
| Aspecto | Detalhe |
|
|
496
|
+
|---|---|
|
|
497
|
+
| Finalidade | Deletar pasta com cascata total (ver 2.6). |
|
|
498
|
+
| Status | `204 No Content`. |
|
|
499
|
+
| Triggers | Nenhum em `folder` / `folder_content`; **`folder_log` também não** (cascata deleta logs por `remove`, sem passar pelo `deleteLog`). |
|
|
212
500
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
501
|
+
> Irreversível. Sem transação — uma falha no meio deixa estado inconsistente.
|
|
502
|
+
|
|
503
|
+
### 4.5 `DELETE /v3/folder?q=<query>`
|
|
504
|
+
|
|
505
|
+
`FolderRest.deleteAll` linhas 71–80. Executa:
|
|
506
|
+
|
|
507
|
+
```java
|
|
508
|
+
distinct("_id").query("{" + q + "}")
|
|
222
509
|
```
|
|
223
510
|
|
|
224
|
-
|
|
225
|
-
**GET** `/v3/database/folder_content_type`
|
|
511
|
+
…concatenando o parâmetro `q` diretamente. Depois itera os IDs e chama `deleteFolder` para cada. Risco de injeção MongoDB descrito na Seção 8.
|
|
226
512
|
|
|
227
|
-
|
|
513
|
+
### 4.6 `POST /v3/folder/inside`
|
|
228
514
|
|
|
229
|
-
|
|
515
|
+
| Aspecto | Detalhe |
|
|
516
|
+
|---|---|
|
|
517
|
+
| Finalidade | Retornar árvore estrutural (sem progresso). |
|
|
518
|
+
| Body | `{ "folder": "<id ou vazio>" }` |
|
|
519
|
+
| Resposta | `Inside` recursivo. |
|
|
520
|
+
|
|
521
|
+
**Comportamento por valor de `folder`:**
|
|
522
|
+
|
|
523
|
+
| Input | Comportamento |
|
|
524
|
+
|---|---|
|
|
525
|
+
| ID existente | Retorna `Inside` cuja raiz corresponde ao folder; `items` contém sub-folders (filtrados por `active`) e contents. |
|
|
526
|
+
| ID inexistente (não-vazio) | Retorna `Inside` raiz "vazio" (apenas `folder=true`, `items=[]`) — a query por `parent: <id>` não acha nada. |
|
|
527
|
+
| `null` ou `""` | Retorna **todos os folders raiz** (`{parent: {$exists: false}}`) **e** os `folder_content` raiz (registros de `folder_content` sem `parent`, se existirem). |
|
|
528
|
+
|
|
529
|
+
**Ordem dos `items`:** sempre `[folder_0, folder_1, …, content_0, content_1, …]`. Pastas e conteúdos **não são intercalados** por `position`.
|
|
530
|
+
|
|
531
|
+
### 4.7 `POST /v3/folder/breadcrumb`
|
|
532
|
+
|
|
533
|
+
| Aspecto | Detalhe |
|
|
534
|
+
|---|---|
|
|
535
|
+
| Finalidade | Retornar caminho do raiz até a pasta informada. |
|
|
536
|
+
| Body | `{ "folder": "<id>" }` |
|
|
537
|
+
| Resposta | `List<Folder>` ordenada `[raiz, ..., alvo]`. |
|
|
538
|
+
|
|
539
|
+
**Algoritmo (`breadcrumbFolder` linhas 46–57):**
|
|
230
540
|
|
|
231
|
-
#### Registrar Progresso ⚠️ Via endpoint REST (não via database direto)
|
|
232
|
-
**POST** `/v3/folder/log`
|
|
233
|
-
```json
|
|
234
|
-
{ "item": "<content_id>", "player": "player123", "status": "done" }
|
|
235
541
|
```
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
542
|
+
list = []
|
|
543
|
+
f = folder.findOne({_id: id})
|
|
544
|
+
list.add(f)
|
|
545
|
+
enquanto f.parent != null && !f.parent.trim().isEmpty():
|
|
546
|
+
f = folder.findOne({_id: f.parent})
|
|
547
|
+
list.add(f)
|
|
548
|
+
Collections.reverse(list)
|
|
549
|
+
return list
|
|
239
550
|
```
|
|
240
551
|
|
|
241
|
-
> **
|
|
552
|
+
> **NPE garantida** se `id` não existir (`f` será `null`, e `f.parent` rompe na primeira iteração). Mesmo problema se algum `parent` no caminho não existir no banco.
|
|
242
553
|
|
|
243
|
-
|
|
244
|
-
**GET** `/v3/folder/log/:id`
|
|
554
|
+
### 4.8 `POST /v3/folder/progress`
|
|
245
555
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
556
|
+
| Aspecto | Detalhe |
|
|
557
|
+
|---|---|
|
|
558
|
+
| Finalidade | Calcular o progresso do jogador na árvore + disparar trigger. |
|
|
559
|
+
| Body | `{ "folder": "<id>", "player": "<playerId>" }` |
|
|
560
|
+
| Resposta | `Inside` com `percent/done/total/time/is_unlocked` preenchidos. |
|
|
249
561
|
|
|
250
|
-
|
|
251
|
-
**GET** `/v3/database/folder_log`
|
|
562
|
+
`FolderRest.progress` (linhas 104–133) sempre dispara `folder_progress` (`EVENT_AFTER_CREATE`) ao final, com `previous_percent == current_percent == inside.percent`. Use o trigger via `insertLog` quando precisar de "antes/depois" reais.
|
|
252
563
|
|
|
253
|
-
|
|
564
|
+
> **NPE confirmada** quando `folder` não existe: `FolderManager.playerProgress` retorna `null`, mas `FolderRest.progress` linhas 125–126 fazem `progress.put("previous_percent", inside.percent)` — quebra antes de devolver resposta.
|
|
565
|
+
|
|
566
|
+
### 4.9 `POST /v3/folder/log`
|
|
567
|
+
|
|
568
|
+
| Aspecto | Detalhe |
|
|
569
|
+
|---|---|
|
|
570
|
+
| Finalidade | Registrar/atualizar progresso de um conteúdo (ver pipeline 2.3). |
|
|
571
|
+
| Body | `FolderLog` JSON. |
|
|
572
|
+
| Status | `201 Created` (mesmo quando o log **não** é salvo por falta de content). |
|
|
573
|
+
| Triggers | `folder_log` BEFORE/AFTER `CREATE`; `folder_progress` AFTER `CREATE` por cada folder cujo `percent` aumentou. |
|
|
574
|
+
|
|
575
|
+
**Falha silenciosa:** se `log.item` não existir em `folder_content`, retorna 201 com o body original, sem persistir nada (`FolderManager.insertLog` linhas 561–562: o `if(content != null)` engole tudo).
|
|
576
|
+
|
|
577
|
+
**Query de idempotência:**
|
|
578
|
+
|
|
579
|
+
```javascript
|
|
580
|
+
db.folder_log.findOne({ player: "<player>", item: "<item>", finished: {$exists: false} })
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### 4.10 `GET /v3/folder/log/{id}`
|
|
584
|
+
|
|
585
|
+
`findLog`. Retorna `FolderLog` ou body vazio se não encontrado.
|
|
586
|
+
|
|
587
|
+
### 4.11 `DELETE /v3/folder/log/{id}`
|
|
588
|
+
|
|
589
|
+
`FolderManager.deleteLog` linhas 653–660:
|
|
590
|
+
|
|
591
|
+
1. `findOne({_id: id})` → `log`.
|
|
592
|
+
2. `triggerManager.execute(log._id, log, "folder_log", BEFORE_DELETE, log.player, null)`.
|
|
593
|
+
3. `c.remove({_id: id})`.
|
|
594
|
+
4. `triggerManager.execute(..., AFTER_DELETE, ...)`.
|
|
595
|
+
|
|
596
|
+
> **NPE confirmada** quando `id` não existe: `log` será `null` e o passo 2 quebra em `log._id`. **Não recalcula** progresso das pastas pai.
|
|
597
|
+
|
|
598
|
+
### 4.12 `POST /v3/folder/content`
|
|
599
|
+
|
|
600
|
+
| Aspecto | Detalhe |
|
|
601
|
+
|---|---|
|
|
602
|
+
| Finalidade | Criar vínculo `Content` entre pasta e item. |
|
|
603
|
+
| Body | `Content` JSON. |
|
|
604
|
+
| Status | `201 Created`. |
|
|
605
|
+
| Triggers | Nenhum. |
|
|
606
|
+
|
|
607
|
+
Auto-cálculo de `title` ver 3.3. Sem validação de existência de `parent` (folder) nem do item real apontado por `content`.
|
|
608
|
+
|
|
609
|
+
### 4.13 `POST /v3/folder/content/aggregate`
|
|
610
|
+
|
|
611
|
+
| Aspecto | Detalhe |
|
|
612
|
+
|---|---|
|
|
613
|
+
| Finalidade | Listar `folder_content` via pipeline MongoDB customizado, com paginação e enriquecimento opcional do dado real. |
|
|
614
|
+
| Body | `List<Object>` — estágios adicionais do aggregate (já vêm depois do `$match` interno). |
|
|
615
|
+
| Headers | `Range: items=0-N` (paginação via `PaginationUtil`, default `0-100`). |
|
|
616
|
+
|
|
617
|
+
**Query params:**
|
|
618
|
+
|
|
619
|
+
| Param | Tipo | Descrição |
|
|
620
|
+
|---|---|---|
|
|
621
|
+
| `folder` | `String` | Se não vazio: filtro de escopo. |
|
|
622
|
+
| `cascade` | `boolean` | `true` → expande para todos os contents na sub-árvore (`insideFolder(folder).getContentIds(true)`); `false` → apenas contents diretos. |
|
|
623
|
+
| `append_data` | `boolean` | `true` → para cada item retornado, adiciona campo `data` com o documento real da coleção `ContentType.entity` (via `findContentData(id)`). |
|
|
624
|
+
|
|
625
|
+
**Pipeline construído (`findAllContentAggregate` linhas 493–544):**
|
|
626
|
+
|
|
627
|
+
```
|
|
628
|
+
estágio 0 (sempre):
|
|
629
|
+
{$match: { ... } }
|
|
630
|
+
├── se folder não vazio && cascade → {_id: {$in: [ids]}}
|
|
631
|
+
├── se folder não vazio && !cascade → {parent: folder}
|
|
632
|
+
└── se folder vazio → {} (match-tudo)
|
|
633
|
+
estágios 1+ (do body) → adicionados via Aggregate.and(FunifierMarshaller.toString(stage))
|
|
634
|
+
```
|
|
254
635
|
|
|
255
|
-
|
|
636
|
+
Resultado é paginado por `PaginationUtil.getPageResultThrowsException(a, range, 0, 100)`.
|
|
256
637
|
|
|
257
|
-
|
|
258
|
-
`DELETE /v3/folder/:id` é uma operação destrutiva em cascata:
|
|
259
|
-
1. Deleta todos os sub-folders recursivamente
|
|
260
|
-
2. Deleta todos os `folder_content` da pasta
|
|
261
|
-
3. Deleta todos os `folder_log` dos conteúdos
|
|
262
|
-
4. Se `FolderContentType.input = "formulary"`: deleta o dado real na coleção `entity`
|
|
638
|
+
> Estágios injetados pelo cliente (`$lookup`, `$out`, `$merge`, etc.) são executados sem validação. Ver Seção 8.
|
|
263
639
|
|
|
264
|
-
|
|
640
|
+
### 4.14 `GET /v3/folder/content/{id}`
|
|
265
641
|
|
|
266
|
-
|
|
267
|
-
O servidor busca log existente por `(player, item)` sem `finished`. Se encontrado, atualiza em vez de criar novo. Nunca gera duplicata para um conteúdo em andamento.
|
|
642
|
+
Retorna `Content` ou body vazio.
|
|
268
643
|
|
|
269
|
-
###
|
|
270
|
-
- `status: "done"` → `percent = 100.0` + `finished = now` (automático)
|
|
271
|
-
- `percent >= 100` → `status = "done"` + `finished = now` (automático)
|
|
644
|
+
### 4.15 `GET /v3/folder/content/{id}/data`
|
|
272
645
|
|
|
273
|
-
|
|
274
|
-
|
|
646
|
+
Retorna o documento real em `ContentType.entity` por `{_id: content.content}`. Retorna body vazio se:
|
|
647
|
+
|
|
648
|
+
- `content == null` (ID inexistente), ou
|
|
649
|
+
- `content.type` ou `content.content` `null`, ou
|
|
650
|
+
- `ContentType` ou item real não existirem.
|
|
651
|
+
|
|
652
|
+
`findContentData` engole essas condições e retorna `null`.
|
|
653
|
+
|
|
654
|
+
### 4.16 `GET /v3/folder/content/{id}/move?direction=up|down`
|
|
655
|
+
|
|
656
|
+
Mesmo algoritmo de `moveFolder`, na coleção `folder_content`, **sem filtro por `active`** (`Content` não tem o campo). Status `204`.
|
|
657
|
+
|
|
658
|
+
> Diferença importante em relação a `moveFolder`: `Content.position` é `int` primitivo, não há null-guards (linhas 460–471) — direto `atual.position - 1`.
|
|
659
|
+
|
|
660
|
+
### 4.17 `DELETE /v3/folder/content/{id}`
|
|
661
|
+
|
|
662
|
+
`FolderManager.deleteContent` linhas 477–490:
|
|
663
|
+
|
|
664
|
+
1. `Content content = folder_content.findOne({_id: id})`.
|
|
665
|
+
2. `ContentType type = folder_content_type.findOne({_id: content.type})`.
|
|
666
|
+
3. Se `type != null && type.input == "formulary"` → `db[type.entity].remove({_id: content.content})`.
|
|
667
|
+
4. `folder_content.remove({_id: id})`.
|
|
668
|
+
5. `folder_log.remove({item: id})`.
|
|
669
|
+
|
|
670
|
+
> **NPE confirmada** quando `id` não existe (`content == null`) — passo 2 quebra em `content.type`. **Sem trigger**. **Sem recálculo** de progresso.
|
|
671
|
+
|
|
672
|
+
### 4.18 `DELETE /v3/folder/content?q=<query>`
|
|
673
|
+
|
|
674
|
+
Mesmo padrão de `4.5` na coleção `folder_content`. `q` interpolado sem sanitização. Itera os IDs e chama `deleteContent` para cada.
|
|
275
675
|
|
|
276
676
|
---
|
|
277
677
|
|
|
278
|
-
##
|
|
678
|
+
## 5. Regras de Negócio
|
|
279
679
|
|
|
280
|
-
###
|
|
281
|
-
| Evento | Quando |
|
|
282
|
-
|--------|--------|
|
|
283
|
-
| `BEFORE_CREATE` | Antes de inserir/atualizar log |
|
|
284
|
-
| `AFTER_CREATE` | Após inserir/atualizar log |
|
|
285
|
-
| `BEFORE_DELETE` | Antes de deletar log |
|
|
286
|
-
| `AFTER_DELETE` | Após deletar log |
|
|
680
|
+
### 5.1 Idempotência ativa do log de progresso
|
|
287
681
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
682
|
+
Enquanto `finished == null`, qualquer `insertLog` com mesmo `(player, item)` atualiza o log existente preservando `_id` e `started`. Quando o log é fechado (`status="done"` → `finished` setado), a chave de idempotência deixa de bater — chamadas posteriores criam **um novo** log.
|
|
683
|
+
|
|
684
|
+
### 5.2 Normalização cruzada de status e percent
|
|
685
|
+
|
|
686
|
+
Servidor força a consistência:
|
|
687
|
+
|
|
688
|
+
- `status="done"` ⇒ `percent=100.0` (sobrescreve qualquer valor).
|
|
689
|
+
- `percent>=100` ⇒ `status="done"`, `percent=100.0`.
|
|
690
|
+
- Outros valores de `status` não são interpretados — apenas armazenados.
|
|
691
|
+
|
|
692
|
+
### 5.3 Herança de `unlock_policy`
|
|
693
|
+
|
|
694
|
+
Sobe a árvore (`f.parent`) até achar o primeiro ancestral com `unlock_policy != null`. Sem nenhum ancestral com política → `is_unlocked = true`.
|
|
695
|
+
|
|
696
|
+
### 5.4 Tipos não reconhecidos de política desbloqueiam
|
|
697
|
+
|
|
698
|
+
Em `playerProgressCascade`: se `policy.type == null` ou `policy.type != "progress"` → `is_unlocked = true; continue`. Não há outros tipos suportados.
|
|
699
|
+
|
|
700
|
+
### 5.5 Primeiro folder em `prev` é sempre desbloqueado
|
|
701
|
+
|
|
702
|
+
Quando `folder_ref == "prev"` e o índice do folder na lista `onlyFolders` é `0`, `is_unlocked = true` independentemente de `min_percent` — não há "irmão anterior".
|
|
703
|
+
|
|
704
|
+
### 5.6 `min_percent` é comparação inclusiva (`>=`)
|
|
705
|
+
|
|
706
|
+
`is_unlocked = (target.percent ?: 0) >= min` — `target.percent` `null` é tratado como `0.0`.
|
|
707
|
+
|
|
708
|
+
### 5.7 Visibilidade vs deleção (`active`)
|
|
709
|
+
|
|
710
|
+
`active=false` esconde o folder da árvore de leitura e do `moveFolder`, mas **não impede a remoção em cascata** quando um ancestral é deletado.
|
|
711
|
+
|
|
712
|
+
### 5.8 Progresso conta contents distintos com `status="done"`
|
|
713
|
+
|
|
714
|
+
O aggregate em `playerProgressCascade` agrupa por `item` antes de somar — mesmo que existam N logs com `status="done"` para o mesmo content (logs históricos após reset/recriação), eles contam **1 vez**.
|
|
715
|
+
|
|
716
|
+
### 5.9 Triggers `folder_progress` disparam do mais próximo ao raiz, só quando o percent aumenta
|
|
717
|
+
|
|
718
|
+
Ordem após `Collections.reverse(path)`: `[content.parent, ..., raiz]`. O disparo é condicional a `folderAfter.percent > folderBefore.percent`. Logs `done` que **não mudam** o percent do folder (por exemplo, reapertar `done` no mesmo content) **não disparam** o trigger.
|
|
719
|
+
|
|
720
|
+
### 5.10 Trigger `folder_progress` do endpoint `/progress` é incondicional
|
|
721
|
+
|
|
722
|
+
Independente de mudança, `POST /v3/folder/progress` dispara o trigger uma vez por chamada com `previous_percent == current_percent`. Útil para "ping" do progresso, ruim para handlers que assumem mudança.
|
|
723
|
+
|
|
724
|
+
### 5.11 Multi-tenant
|
|
725
|
+
|
|
726
|
+
Cada `apiKey` resolve um `FrontController` próprio, com `ManagerFactory` e `JongoConnection` próprios. Não há cross-tenant query no módulo.
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## 6. Comportamentos Automáticos
|
|
731
|
+
|
|
732
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
733
|
+
|---|---|---|---|
|
|
734
|
+
| Auto-`_id` em `Folder` | `insertFolder` com `_id` nulo/vazio | `Guid.shortTimeMillis()` | Sim |
|
|
735
|
+
| Auto-`_id` em `Content` | `insertContent` com `_id` nulo/vazio | `Guid.shortTimeMillis()` | Sim |
|
|
736
|
+
| Auto-`_id` em `FolderLog` | `insertLog` com `_id` nulo/vazio (e sem log ativo) | `Guid.newShortGuid()` | Sim |
|
|
737
|
+
| Default `position=0` | `insertFolder` com `position` nulo | Pasta na posição 0 | Sim |
|
|
738
|
+
| Default `active=true` | `insertFolder` com `active` nulo | Pasta visível | Sim |
|
|
739
|
+
| Auto-`title` de `Content` | `insertContent` com `title==null` OU `ContentType.input=formulary` | Resolve `title`/`name`/`_id` do item real | Sim |
|
|
740
|
+
| Reutilização de `_id` do log ativo | `insertLog` com log existente `(player,item,!finished)` | Mantém histórico contínuo | Sim |
|
|
741
|
+
| Auto-`started` | `insertLog` sem log ativo | `started = now` | Sim |
|
|
742
|
+
| Auto-`finished` | `insertLog` com `status="done"` e `finished==null` | `finished = now` | Sim |
|
|
743
|
+
| Normalização `status`↔`percent` | `insertLog` | Coerência entre os dois campos | Sim |
|
|
744
|
+
| Trigger `folder_log` BEFORE/AFTER CREATE | `insertLog` (apenas se content existe) | Executa regras externas | Não (somente externo) |
|
|
745
|
+
| Trigger `folder_log` BEFORE/AFTER DELETE | `deleteLog` (apenas se id existe) | Executa regras externas | Não |
|
|
746
|
+
| Trigger `folder_progress` por breadcrumb | `insertLog` quando `diff>0` e `folderAfter.percent > folderBefore.percent` | Um trigger por folder no path, do mais próximo ao raiz | Não |
|
|
747
|
+
| Trigger `folder_progress` do endpoint | `POST /v3/folder/progress` | Disparo único, incondicional, no folder consultado | Não |
|
|
748
|
+
| Cascata de `formulary` | `deleteContent` ou `deleteFolderCascade` | Remove dado real em `ContentType.entity` | Sim (destrutivo) |
|
|
749
|
+
| Renumeração total de irmãos | `moveFolder` / `moveContent` | Salva **todos** os irmãos com posições sequenciais | Sim |
|
|
750
|
+
|
|
751
|
+
### 6.1 Diagrama — encadeamento automático em `insertLog`
|
|
752
|
+
|
|
753
|
+
```mermaid
|
|
754
|
+
flowchart LR
|
|
755
|
+
A[insertLog] --> B[normalização status/percent]
|
|
756
|
+
B --> C{content<br/>existe?}
|
|
757
|
+
C -- Não --> X[abort silencioso]
|
|
758
|
+
C -- Sim --> D[trigger folder_log<br/>BEFORE_CREATE]
|
|
759
|
+
D --> E[save]
|
|
760
|
+
E --> F[trigger folder_log<br/>AFTER_CREATE]
|
|
761
|
+
F --> G{diff > 0?}
|
|
762
|
+
G -- Não --> Z[fim]
|
|
763
|
+
G -- Sim --> H[breadcrumb invertido]
|
|
764
|
+
H --> I[para cada folder do path]
|
|
765
|
+
I --> J{folderAfter.percent<br/>> folderBefore.percent?}
|
|
766
|
+
J -- Não --> I
|
|
767
|
+
J -- Sim --> K[trigger folder_progress<br/>AFTER_CREATE]
|
|
768
|
+
K --> I
|
|
769
|
+
I --> Z
|
|
300
770
|
```
|
|
301
771
|
|
|
302
772
|
---
|
|
303
773
|
|
|
304
|
-
##
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
774
|
+
## 7. Suportado vs NÃO Suportado
|
|
775
|
+
|
|
776
|
+
### ✅ Suportado
|
|
777
|
+
|
|
778
|
+
- Hierarquia arbitrária de pastas com aninhamento ilimitado.
|
|
779
|
+
- Duas estratégias de vínculo de conteúdo (`repository`, `formulary`).
|
|
780
|
+
- Reordenação de folders e contents via `move?direction=up|down`.
|
|
781
|
+
- Cálculo recursivo de progresso por jogador com aggregate MongoDB.
|
|
782
|
+
- Desbloqueio condicional de pastas por percent (`unlock_policy.type="progress"`), com referência a folder específico ou `prev`.
|
|
783
|
+
- Herança de `unlock_policy` via ancestrais (`resolveUnlockPolicy`).
|
|
784
|
+
- Idempotência operacional de logs em andamento via `(player, item, !finished)`.
|
|
785
|
+
- Triggers `folder_log` BEFORE/AFTER CREATE e BEFORE/AFTER DELETE.
|
|
786
|
+
- Triggers `folder_progress` por folder no breadcrumb (apenas quando o percent realmente aumenta).
|
|
787
|
+
- Trigger `folder_progress` incondicional ao consultar progresso (`POST /v3/folder/progress`).
|
|
788
|
+
- Listagem de conteúdos via aggregate customizado com `cascade` e `append_data`.
|
|
789
|
+
- Deleção em cascata de pastas (folders, contents, logs, dados `formulary`).
|
|
790
|
+
- Auto-cálculo de `title` de `Content` baseado no item real.
|
|
791
|
+
|
|
792
|
+
### ❌ NÃO Suportado
|
|
793
|
+
|
|
794
|
+
- **`GET /v3/folder` (listagem geral)** — não existe rota em `FolderRest`. Use `/v3/database/folder?q=...`.
|
|
795
|
+
- **`PUT /v3/folder/{id}`** — não existe. `POST /v3/folder` cobre create+update (upsert).
|
|
796
|
+
- **Endpoints CRUD para `folder_content_type`** — gestão é via `/v3/database/folder_content_type`.
|
|
797
|
+
- **Intercalação de folders e contents por `position`** — `insideFolder` sempre retorna **folders antes de contents**, ambos ordenados por `position` dentro do próprio grupo.
|
|
798
|
+
- **Filtragem de contents por `active`** — `Content` não tem `active`; um content só some quando deletado.
|
|
799
|
+
- **Transações atômicas** — nenhum dos pipelines (insert, delete, cascata) usa transação MongoDB. Falha parcial deixa estado inconsistente.
|
|
800
|
+
- **Triggers para `folder` e `folder_content`** — `insertFolder`/`deleteFolder`/`insertContent`/`deleteContent` não disparam triggers.
|
|
801
|
+
- **Recálculo de progresso ao deletar log** — `deleteLog` é cirúrgico; nenhum folder pai é re-agregado.
|
|
802
|
+
- **Validação de referências** — nenhum método valida que `parent`, `type` ou `content` apontam para documentos existentes (com exceção de `insertLog` que requer `content` existir).
|
|
803
|
+
- **Scheduler/job de background** — não há `Runner`/`Scheduler`/job que processe folder.
|
|
804
|
+
- **`type != "progress"` em `unlock_policy`** — outros valores resultam em `is_unlocked=true` (passa direto).
|
|
805
|
+
- **Recurso `folder_log` pago/`paginado` via `/v3/folder`** — listagem só por `/v3/database/folder_log`.
|
|
806
|
+
|
|
807
|
+
### 7.1 Bugs e edge cases confirmados
|
|
808
|
+
|
|
809
|
+
| Local | Comportamento |
|
|
810
|
+
|---|---|
|
|
811
|
+
| `FolderManager.insertLog` linha 594 | `double diff = log.percent` (auto-unbox). Se `log.percent == null` (sem `status="done"` e sem `percent`), lança `NullPointerException` **antes** de salvar o log. Mesmo problema com `current.percent` em logs antigos. |
|
|
812
|
+
| `FolderManager.breadcrumbFolder` linhas 49–53 | `id` inexistente → `findOne` retorna `null` → `folder.parent` NPEs. Mesmo erro se algum `parent` referenciado não existir. |
|
|
813
|
+
| `FolderManager.deleteLog` linhas 655–657 | `id` inexistente → `log == null` → `log._id` NPEs antes do trigger. |
|
|
814
|
+
| `FolderManager.deleteContent` linhas 481–482 | `id` inexistente → `content == null` → `content.type` NPEs. |
|
|
815
|
+
| `FolderRest.progress` linhas 125–126 | `folder` inexistente → `FolderManager.playerProgress` retorna `null` → `inside.percent` NPEs. |
|
|
816
|
+
| `FolderManager.getMaxTimeInside` linhas 297–315 | (1) Quando `m.time == null` no primeiro item content, `MAX = m.time = null` (porque a condição `max==null` curto-circuita o `m.time.after(max)`), e o resto da iteração herda `null`. (2) Em recursão de folder, `MAX = getMaxTimeInside(m, max)` usa o **parâmetro** `max`, não o `MAX` acumulado — descarta valores anteriores. |
|
|
817
|
+
| `FolderManager.findFolder` (e similares) | Quando `_id` não existe, retorna `null`. `FolderRest.find` serializa via `JsonUtil.toJsonRemoveNullFields(null)` → resposta com body vazio mas `200 OK`. Não há `404` no módulo. |
|
|
818
|
+
| `FolderManager.insertLog` | Quando `log.item` aponta para um content inexistente, retorna sem salvar e sem erro — **HTTP 201 silencioso** (Seção 2.3). |
|
|
819
|
+
| `DELETE /v3/folder?q=` / `/content?q=` | Concatenação direta de `q` no `query("{" + q + "}")` (Seção 8). |
|
|
319
820
|
|
|
320
821
|
---
|
|
321
822
|
|
|
322
|
-
##
|
|
823
|
+
## 8. Segurança e Permissões
|
|
323
824
|
|
|
324
|
-
|
|
325
|
-
```json
|
|
326
|
-
funifier_save type=folder
|
|
327
|
-
{ "title": "Trilha de Vendas" }
|
|
328
|
-
```
|
|
825
|
+
**Autenticação:** Bearer token via `@BeanParam AuthBean`. O `apiKey` (extraído do token) é o seletor de tenant em `FrontController.getInstance(authBean.getApiKey())`.
|
|
329
826
|
|
|
330
|
-
|
|
331
|
-
```json
|
|
332
|
-
funifier_save type=folder
|
|
333
|
-
{ "title": "Módulo 1 - Prospecção", "parent": "<trilha_id>", "position": 0 }
|
|
334
|
-
```
|
|
827
|
+
**Isolamento por tenant:** cada `FrontController` mantém seu próprio `ManagerFactory` e `Jongo`/`MongoCollection`. Não há query cross-tenant em `FolderManager`.
|
|
335
828
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
829
|
+
**Superfícies de injeção MongoDB confirmadas no código:**
|
|
830
|
+
|
|
831
|
+
1. **`DELETE /v3/folder?q=<query>`** — `FolderRest.java:75`:
|
|
832
|
+
|
|
833
|
+
```java
|
|
834
|
+
jongo.getCollection(Entity.FOLDER.collection)
|
|
835
|
+
.distinct("_id").query("{" + q + "}").as(String.class);
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
`q` é concatenado bruto. Um cliente autenticado pode injetar operadores `$where`, `$or`, `$regex` etc. e disparar `deleteFolder` (cascata destrutiva) em todos os IDs retornados.
|
|
839
|
+
|
|
840
|
+
2. **`DELETE /v3/folder/content?q=<query>`** — `FolderRest.java:230`: mesmo padrão na coleção `folder_content`.
|
|
841
|
+
|
|
842
|
+
3. **`POST /v3/folder/content/aggregate` (body)** — `FolderManager.java:517`:
|
|
843
|
+
|
|
844
|
+
```java
|
|
845
|
+
a.and(FunifierMarshaller.toString(aggregations.get(i)));
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Cada item do body é serializado e empurrado como estágio adicional do pipeline. Não há validação ou whitelist — `$lookup`, `$out`, `$merge`, `$function` (em Mongo recente) podem ser executados.
|
|
849
|
+
|
|
850
|
+
**Recomendações operacionais:**
|
|
851
|
+
|
|
852
|
+
- Restringir o uso desses endpoints a chaves administrativas; nunca expor diretamente ao cliente final.
|
|
853
|
+
- Considerar um proxy / WAF que filtre `q` por regex de operadores perigosos.
|
|
854
|
+
- Logs de auditoria devem capturar `q` integral e payload do aggregate.
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
## 9. Observabilidade e Troubleshooting
|
|
859
|
+
|
|
860
|
+
### 9.1 Índices MongoDB
|
|
861
|
+
|
|
862
|
+
Criados em `ConnectionPool.createIndexes()` (linhas 184–197):
|
|
863
|
+
|
|
864
|
+
| Coleção | Campo(s) | Direção |
|
|
865
|
+
|---|---|---|
|
|
866
|
+
| `folder` | `parent` | `1` |
|
|
867
|
+
| `folder_content` | `parent` | `1` |
|
|
868
|
+
| `folder_content` | `position` | `1` |
|
|
869
|
+
| `folder_log` | `item` | `1` |
|
|
870
|
+
| `folder_log` | `player` | `1` |
|
|
871
|
+
| `folder_log` | `status` | `1` |
|
|
872
|
+
| `folder_log` | `time` | `1` |
|
|
873
|
+
|
|
874
|
+
> **Faltam índices úteis:** `folder_log` não tem índice composto `(player, item)` nem `(player, item, finished)` — a query de idempotência (executada em todo `insertLog`) faz scan parcial.
|
|
875
|
+
|
|
876
|
+
### 9.2 Comandos de diagnóstico
|
|
877
|
+
|
|
878
|
+
```http
|
|
879
|
+
# Existe a pasta?
|
|
880
|
+
GET /v3/folder/<id>
|
|
881
|
+
|
|
882
|
+
# Listar pastas raiz
|
|
883
|
+
GET /v3/database/folder?q={"parent":{"$exists":false}}
|
|
884
|
+
|
|
885
|
+
# Listar conteúdos diretos de uma pasta
|
|
886
|
+
GET /v3/database/folder_content?q={"parent":"<folder_id>"}
|
|
887
|
+
|
|
888
|
+
# Logs de progresso de um jogador
|
|
889
|
+
GET /v3/database/folder_log?q={"player":"<player_id>"}
|
|
890
|
+
|
|
891
|
+
# Log ativo (em andamento)
|
|
892
|
+
GET /v3/database/folder_log?q={"player":"<p>","item":"<i>","finished":{"$exists":false}}
|
|
893
|
+
|
|
894
|
+
# Verificar tipo de conteúdo
|
|
895
|
+
GET /v3/database/folder_content_type?q={"_id":"<type_id>"}
|
|
896
|
+
|
|
897
|
+
# Calcular progresso completo do jogador
|
|
898
|
+
POST /v3/folder/progress
|
|
899
|
+
{"folder":"<root_folder_id>","player":"<player_id>"}
|
|
900
|
+
|
|
901
|
+
# Árvore estrutural (sem progresso)
|
|
902
|
+
POST /v3/folder/inside
|
|
903
|
+
{"folder":"<root_folder_id>"}
|
|
345
904
|
```
|
|
346
905
|
|
|
347
|
-
###
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
906
|
+
### 9.3 Sintomas vs causas
|
|
907
|
+
|
|
908
|
+
| Sintoma | Causa provável |
|
|
909
|
+
|---|---|
|
|
910
|
+
| `POST /v3/folder/log` retorna `201` mas log não aparece em `folder_log` | `log.item` não existe em `folder_content`; pipeline aborta silenciosamente. |
|
|
911
|
+
| `POST /v3/folder/log` lança `500` (NPE) | `log.percent == null` e `status != "done"`. Ou `current.percent` de um log antigo é `null`. |
|
|
912
|
+
| `POST /v3/folder/breadcrumb` lança `500` (NPE) | `folder` inexistente, ou algum `parent` referenciado não existe no banco. |
|
|
913
|
+
| `POST /v3/folder/progress` lança `500` (NPE) | `folder` inexistente — `FolderRest` não trata `inside == null`. |
|
|
914
|
+
| `DELETE /v3/folder/log/{id}` lança `500` (NPE) | `id` inexistente. |
|
|
915
|
+
| `DELETE /v3/folder/content/{id}` lança `500` (NPE) | `id` inexistente. |
|
|
916
|
+
| `inside.percent` em folder folha está sempre `0.0` | Não há `folder_log` com `status="done"` para o player nesses contents; ou logs foram inseridos com `item` apontando para `Content._id` errado. |
|
|
917
|
+
| `inside.time` zerado mas há `done` | Provavelmente caiu no fallback `getMaxTimeInside` que tem dois bugs latentes (Seção 7). |
|
|
918
|
+
| Folder não aparece em `/inside` | `active == false` explícito. |
|
|
919
|
+
| `is_unlocked == true` em folder bloqueado | Ancestor sem `unlock_policy`, ou `policy.type` diferente de `"progress"`. |
|
|
920
|
+
| Trigger `folder_progress` não dispara após `insertLog` | (a) `content.parent` resolve em breadcrumb inválido; (b) o percent do folder **não aumentou** (mudança apenas no content individual sem cruzar threshold). |
|
|
921
|
+
| Após `insertLog`, `started` aparece muito antigo | Há log ativo `!finished` para o mesmo `(player,item)`. Esperado. |
|
|
922
|
+
| Após `deleteLog`, progresso da pasta não muda | Por design: `deleteLog` não recalcula folders pai. Re-consulte `POST /v3/folder/progress`. |
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## 10. Exemplos Práticos
|
|
927
|
+
|
|
928
|
+
### 10.1 Exemplo mínimo — trilha com um módulo e um conteúdo
|
|
929
|
+
|
|
930
|
+
```http
|
|
931
|
+
# 1. Criar pasta raiz
|
|
932
|
+
POST /v3/folder
|
|
933
|
+
{ "title": "Trilha de Onboarding" }
|
|
934
|
+
# → 201 { "_id":"trail001","title":"Trilha de Onboarding","position":0,"active":true }
|
|
935
|
+
|
|
936
|
+
# 2. Criar módulo filho
|
|
937
|
+
POST /v3/folder
|
|
938
|
+
{ "title": "Módulo 1 — Boas-vindas", "parent":"trail001", "position":0 }
|
|
939
|
+
# → 201 { "_id":"mod001","parent":"trail001","position":0,"active":true }
|
|
940
|
+
|
|
941
|
+
# 3. Definir tipo de conteúdo (via API de database — não há endpoint próprio)
|
|
942
|
+
POST /v3/database/folder_content_type
|
|
943
|
+
{ "_id":"video", "entity":"video__c", "input":"repository", "title":"Vídeo" }
|
|
944
|
+
|
|
945
|
+
# 4. Vincular conteúdo ao módulo
|
|
946
|
+
POST /v3/folder/content
|
|
947
|
+
{ "type":"video", "content":"vid001", "parent":"mod001", "position":0 }
|
|
948
|
+
# → 201 { "_id":"fc001","type":"video","content":"vid001","parent":"mod001","title":"<auto>","position":0 }
|
|
949
|
+
|
|
950
|
+
# 5. Registrar conclusão pelo jogador
|
|
951
|
+
POST /v3/folder/log
|
|
952
|
+
{ "item":"fc001", "player":"player123", "status":"done" }
|
|
953
|
+
# → 201 { "_id":"log001","item":"fc001","player":"player123","status":"done","percent":100.0,"started":"...","finished":"..." }
|
|
954
|
+
|
|
955
|
+
# 6. Consultar progresso completo
|
|
956
|
+
POST /v3/folder/progress
|
|
957
|
+
{ "folder":"trail001", "player":"player123" }
|
|
958
|
+
# → 200 { "_id":"trail001","folder":true,"player":"player123","total":1,"done":1,"percent":100.0,"is_unlocked":true,"items":[ ... ] }
|
|
351
959
|
```
|
|
352
960
|
|
|
353
|
-
###
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
961
|
+
### 10.2 Exemplo avançado — desbloqueio condicional por progresso de irmão anterior
|
|
962
|
+
|
|
963
|
+
```http
|
|
964
|
+
# Mod 1 (sem policy — sempre desbloqueado por ser primeiro irmão)
|
|
965
|
+
POST /v3/folder
|
|
966
|
+
{ "_id":"mod001", "title":"Módulo 1", "parent":"trail001", "position":0 }
|
|
967
|
+
|
|
968
|
+
# Mod 2 — bloqueado até Mod 1 atingir 80%
|
|
969
|
+
POST /v3/folder
|
|
970
|
+
{ "_id":"mod002", "title":"Módulo 2", "parent":"trail001", "position":1,
|
|
971
|
+
"unlock_policy": { "type":"progress", "folder_ref":"prev", "min_percent":80 } }
|
|
972
|
+
|
|
973
|
+
# Mod 3 — bloqueado até Mod 1 (específico) atingir 100%
|
|
974
|
+
POST /v3/folder
|
|
975
|
+
{ "_id":"mod003", "title":"Módulo 3", "parent":"trail001", "position":2,
|
|
976
|
+
"unlock_policy": { "type":"progress", "folder_ref":"mod001", "min_percent":100 } }
|
|
977
|
+
|
|
978
|
+
# Filhos de Mod 3 herdam essa policy via resolveUnlockPolicy se não tiverem uma própria.
|
|
357
979
|
```
|
|
358
980
|
|
|
359
|
-
###
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
981
|
+
### 10.3 Exemplo — aggregate de conteúdos com dado real
|
|
982
|
+
|
|
983
|
+
```http
|
|
984
|
+
# Todos os contents da trilha (cascade), enriquecidos com `data`, primeiros 50
|
|
985
|
+
POST /v3/folder/content/aggregate?folder=trail001&cascade=true&append_data=true
|
|
986
|
+
Range: items=0-49
|
|
987
|
+
[]
|
|
988
|
+
# → { "results":[ { "_id":"fc001","type":"video","content":"vid001","parent":"mod001","data":{ ... item real ... } }, ... ], "pagination":{...} }
|
|
989
|
+
|
|
990
|
+
# Mesma chamada com pipeline extra (ex: limitar a contents de tipo "video")
|
|
991
|
+
POST /v3/folder/content/aggregate?folder=trail001&cascade=true
|
|
992
|
+
Range: items=0-49
|
|
993
|
+
[ { "$match": { "type": "video" } } ]
|
|
363
994
|
```
|
|
364
995
|
|
|
365
|
-
###
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
996
|
+
### 10.4 Anti-patterns — o que NÃO fazer
|
|
997
|
+
|
|
998
|
+
```http
|
|
999
|
+
# ❌ Inserir log direto na coleção via /v3/database — bypassa
|
|
1000
|
+
# idempotência, normalização status/percent, breadcrumb, triggers folder_progress.
|
|
1001
|
+
POST /v3/database/folder_log
|
|
1002
|
+
{ "item":"fc001", "player":"p1", "status":"done" }
|
|
1003
|
+
|
|
1004
|
+
# Use sempre:
|
|
1005
|
+
POST /v3/folder/log
|
|
1006
|
+
{ "item":"fc001", "player":"p1", "status":"done" }
|
|
1007
|
+
|
|
1008
|
+
# ❌ DELETE em massa sem revisar `q`
|
|
1009
|
+
DELETE /v3/folder?q={"$where":"true"} # remove TODAS as pastas
|
|
1010
|
+
|
|
1011
|
+
# ❌ Enviar log sem `percent` e sem `status="done"` quando não há log ativo
|
|
1012
|
+
POST /v3/folder/log
|
|
1013
|
+
{ "item":"fc001", "player":"p1" }
|
|
1014
|
+
# Resultado: NullPointerException no servidor (Seção 7). Sempre envie ou
|
|
1015
|
+
# `status="done"` ou um `percent` numérico.
|
|
1016
|
+
|
|
1017
|
+
# ❌ Renomear/mover folder por update direto da coleção `folder`
|
|
1018
|
+
POST /v3/database/folder
|
|
1019
|
+
{ "_id":"mod002", "parent":"<novo>", "position":3 }
|
|
1020
|
+
# Posições de irmãos ficam furadas porque `moveFolder` é quem normaliza —
|
|
1021
|
+
# Use POST /v3/folder/{id}/move?direction=up|down repetidamente.
|
|
1022
|
+
|
|
1023
|
+
# ❌ Confiar em `POST /v3/folder/progress` como gatilho de mudança
|
|
1024
|
+
# Ele dispara `folder_progress` mesmo sem mudança (previous_percent ==
|
|
1025
|
+
# current_percent). Handlers que reagem a esse evento precisam comparar
|
|
1026
|
+
# valores ou só agir em payloads vindos do `insertLog`.
|
|
369
1027
|
```
|
|
370
1028
|
|
|
371
1029
|
---
|
|
372
1030
|
|
|
373
|
-
## Integração com Outras Técnicas
|
|
374
|
-
|
|
375
|
-
| Técnica | Integração |
|
|
376
|
-
|---------|-----------|
|
|
377
|
-
| **Challenge** | Criar desafio vinculado à completion do folder (rule: `folder_progress` com `percent >= 100`) |
|
|
378
|
-
| **Point** | Conceder pontos via trigger `folder_progress` quando `current_percent >= 100` |
|
|
379
|
-
| **Achievement** | Conquista ao completar 100% de um folder |
|
|
380
|
-
| **Quiz** | Quiz como `FolderContent` dentro de um folder (via `folder-content-type` com `input=repository`) |
|
|
381
|
-
|
|
382
1031
|
## Checklist de Configuração
|
|
383
1032
|
|
|
384
|
-
- [ ]
|
|
385
|
-
- [ ]
|
|
386
|
-
- [ ]
|
|
387
|
-
- [ ]
|
|
388
|
-
- [ ]
|
|
389
|
-
- [ ]
|
|
390
|
-
- [ ]
|
|
1033
|
+
- [ ] Definir `FolderContentType` para cada tipo de conteúdo via `POST /v3/database/folder_content_type` antes de criar `folder_content`.
|
|
1034
|
+
- [ ] Escolher `input`: `repository` (item já existe em outra coleção) **ou** `formulary` (gerenciado junto ao vínculo).
|
|
1035
|
+
- [ ] Criar estrutura de pastas raiz → módulos → sub-pastas com `position` sequencial (`0,1,2…`).
|
|
1036
|
+
- [ ] Em `folder_content`, garantir que `parent` (folder) e `content` (item real) já existam — o servidor não valida.
|
|
1037
|
+
- [ ] Para desbloqueio condicional, definir `unlock_policy` apenas no nível mais alto necessário; descendentes herdam via `resolveUnlockPolicy`.
|
|
1038
|
+
- [ ] Usar **sempre** `POST /v3/folder/log` para registrar progresso; nunca `POST /v3/database/folder_log`.
|
|
1039
|
+
- [ ] Ao enviar `folder_log`, garantir `status="done"` **ou** `percent` numérico — `null` em ambos lança NPE.
|
|
1040
|
+
- [ ] Verificar a árvore com `POST /v3/folder/inside` antes de publicar; com `POST /v3/folder/progress` para validar `is_unlocked`.
|
|
1041
|
+
- [ ] Antes de `DELETE /v3/folder/{id}` ou `/content/{id}`: revisar a sub-árvore — operação é cascata, irreversível, e em `formulary` deleta o dado real também.
|
|
1042
|
+
- [ ] Nunca expor `DELETE /v3/folder?q=` e `DELETE /v3/folder/content?q=` ao cliente final — `q` é interpolado bruto no MongoDB.
|
|
1043
|
+
- [ ] Logar e auditar uso de `POST /v3/folder/content/aggregate` — estágios são passados sem validação.
|
|
1044
|
+
- [ ] Se progresso parecer "travado" após `deleteLog`, lembre: não há recálculo automático; chame `POST /v3/folder/progress` para atualizar `is_unlocked` no front.
|
|
1045
|
+
- [ ] Para handlers de trigger `folder_progress`, distinguir origem: payload de `insertLog` tem `previous_percent != current_percent`; payload de `POST /v3/folder/progress` tem ambos iguais.
|