funifier-mcp 0.2.26 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/index.js +2 -2
  67. package/dist/mcp/index.js.map +1 -1
  68. package/dist/mcp/resources/documentation.d.ts +1 -1
  69. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  70. package/dist/mcp/resources/documentation.js +39 -3
  71. package/dist/mcp/resources/documentation.js.map +1 -1
  72. package/dist/mcp/tools/connect.d.ts.map +1 -1
  73. package/dist/mcp/tools/connect.js +18 -8
  74. package/dist/mcp/tools/connect.js.map +1 -1
  75. package/dist/mcp/tools/database.d.ts.map +1 -1
  76. package/dist/mcp/tools/database.js +59 -47
  77. package/dist/mcp/tools/database.js.map +1 -1
  78. package/dist/mcp/tools/database.test.js +2 -2
  79. package/dist/mcp/tools/database.test.js.map +1 -1
  80. package/dist/mcp/tools/delete.d.ts.map +1 -1
  81. package/dist/mcp/tools/delete.js +13 -3
  82. package/dist/mcp/tools/delete.js.map +1 -1
  83. package/dist/mcp/tools/execute.d.ts.map +1 -1
  84. package/dist/mcp/tools/execute.js +20 -9
  85. package/dist/mcp/tools/execute.js.map +1 -1
  86. package/dist/mcp/tools/folder.d.ts.map +1 -1
  87. package/dist/mcp/tools/folder.js +22 -12
  88. package/dist/mcp/tools/folder.js.map +1 -1
  89. package/dist/mcp/tools/get.d.ts.map +1 -1
  90. package/dist/mcp/tools/get.js +16 -6
  91. package/dist/mcp/tools/get.js.map +1 -1
  92. package/dist/mcp/tools/index.d.ts +1 -1
  93. package/dist/mcp/tools/index.d.ts.map +1 -1
  94. package/dist/mcp/tools/index.js +3 -1
  95. package/dist/mcp/tools/index.js.map +1 -1
  96. package/dist/mcp/tools/list.d.ts.map +1 -1
  97. package/dist/mcp/tools/list.js +38 -14
  98. package/dist/mcp/tools/list.js.map +1 -1
  99. package/dist/mcp/tools/logs.d.ts.map +1 -1
  100. package/dist/mcp/tools/logs.js +15 -5
  101. package/dist/mcp/tools/logs.js.map +1 -1
  102. package/dist/mcp/tools/save.d.ts.map +1 -1
  103. package/dist/mcp/tools/save.js +14 -4
  104. package/dist/mcp/tools/save.js.map +1 -1
  105. package/dist/mcp/tools/save.test.js +3 -3
  106. package/dist/mcp/tools/save.test.js.map +1 -1
  107. package/dist/mcp/tools/search-docs.d.ts +3 -0
  108. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  109. package/dist/mcp/tools/search-docs.js +102 -0
  110. package/dist/mcp/tools/search-docs.js.map +1 -0
  111. package/package.json +6 -2
  112. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  113. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  114. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  115. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  116. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  117. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  118. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  119. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  120. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  121. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  122. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  123. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  124. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  125. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  126. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  127. package/skills/funifier/SKILL.md +88 -0
  128. package/skills/funifier/references/configure-security.md +96 -0
  129. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  130. package/skills/funifier/references/create-aggregate.md +144 -0
  131. package/skills/funifier/references/create-challenge.md +116 -0
  132. package/skills/funifier/references/create-competition.md +98 -0
  133. package/skills/funifier/references/create-crossword.md +574 -0
  134. package/skills/funifier/references/create-custom-object.md +91 -0
  135. package/skills/funifier/references/create-custom-page.md +135 -0
  136. package/skills/funifier/references/create-folder.md +104 -0
  137. package/skills/funifier/references/create-lastmile.md +643 -0
  138. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  139. package/skills/funifier/references/create-level.md +94 -0
  140. package/skills/funifier/references/create-lottery.md +913 -0
  141. package/skills/funifier/references/create-mystery.md +769 -0
  142. package/skills/funifier/references/create-notification.md +75 -0
  143. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  144. package/skills/funifier/references/create-quiz.md +98 -0
  145. package/skills/funifier/references/create-scheduler.md +141 -0
  146. package/skills/funifier/references/create-story.md +636 -0
  147. package/skills/funifier/references/create-swap.md +95 -0
  148. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  149. package/skills/funifier/references/create-virtual-good.md +96 -0
  150. package/skills/funifier/references/create-webhook.md +72 -0
  151. package/skills/funifier/references/create-websocket.md +71 -0
  152. package/skills/funifier/references/create-widget.md +76 -0
  153. package/skills/funifier/references/debug.md +87 -0
  154. package/skills/funifier/references/help.md +81 -0
  155. package/skills/funifier/references/implement-frontend.md +106 -0
  156. package/skills/funifier/references/import-csv.md +75 -0
  157. package/skills/funifier/references/manage-player.md +82 -0
  158. package/skills/funifier/references/manage-team.md +76 -0
  159. package/skills/funifier/references/upload-file.md +91 -0
  160. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  161. package/skills/funifier-create-challenge/SKILL.md +0 -88
  162. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  163. package/skills/funifier-create-level/SKILL.md +0 -87
  164. package/skills/funifier-create-quiz/SKILL.md +0 -87
  165. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  166. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  167. package/skills/funifier-debug/SKILL.md +0 -92
  168. package/skills/funifier-help/SKILL.md +0 -86
  169. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  170. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,155 +1,528 @@
1
- # Upload (Upload)
1
+ # `upload`
2
2
 
3
+ **Acesso Studio:** sem rota/página dedicada no backend — o módulo é consumido via API REST (ex.: seletores de imagem/arquivo do Studio chamam `/v3/upload`).
3
4
  **API Endpoint:** `/v3/upload`
5
+ **Coleção MongoDB:** `upload` (schemaless — mapeada para `HashMap.class` em `Entity.UPLOAD`, sem entidade tipada)
4
6
 
5
- ## O que é
7
+ > Engenharia reversa baseada exclusivamente no código-fonte de `funifier-service` (commit `830037e`). Classe principal: `com.funifier.rest.v3.rest.UploadRest` (605 linhas). Os exemplos de resposta no Javadoc da própria classe estão **desatualizados** (vide seção 7) — esta documentação descreve o comportamento real do runtime.
6
8
 
7
- Upload de arquivos para uso na gamificação. Permite enviar imagens, documentos e outros tipos de arquivos para utilização em avatares, desafios, recursos visuais ou qualquer outra funcionalidade.
9
+ ---
10
+
11
+ ## 1. Visão Geral
12
+
13
+ O módulo `upload` é o ponto de entrada para **persistência de arquivos binários** (imagens, documentos, qualquer mídia) em armazenamento de objetos em nuvem, mantendo um **registro de metadados** de cada arquivo na coleção MongoDB `upload`.
14
+
15
+ Papel arquitetural:
16
+
17
+ - **Dois artefatos por upload**: (1) o arquivo físico enviado ao provedor de nuvem (S3 / Azure Blob / Google Cloud Storage) e (2) um documento de metadados na coleção `upload`. Os dois são gravados em sequência dentro do mesmo handler; não há transação entre eles.
18
+ - **Abstração multi-cloud**: o provedor é escolhido em tempo de execução por `Configuration.getCloud()` (`"azure"`, `"google"`, ou qualquer outro valor → Amazon S3, que é o default). A escolha é **por servidor/configuração**, não por requisição.
19
+ - **Multi-tenant por apiKey**: todo arquivo é armazenado sob o prefixo `games/{apiKey}/{session}`. O `apiKey` identifica a gamificação dona do arquivo e também determina qual banco MongoDB recebe o registro (`FrontController.getInstance(apiKey)`).
20
+ - **Schemaless**: `Entity.UPLOAD` mapeia para `HashMap.class` — não existe POJO/entidade tipada. O documento gravado é exatamente o `Map` montado no handler. Não há validação de schema; qualquer campo dentro de `extra` é aceito e persistido verbatim.
21
+
22
+ Problemas que resolve:
23
+
24
+ - Upload de avatares, logos, capas, documentos e mídia genérica usada por outros módulos (player, catalog, challenge, etc.) através da URL pública retornada.
25
+ - Geração automática de **thumbnails** e **transformações de imagem** (resize / mudança de formato) no endpoint `/image`.
26
+ - Catálogo consultável dos arquivos enviados por gamificação (endpoint `GET /v3/upload`).
27
+
28
+ Relação com outros módulos:
29
+
30
+ - **`trigger`** — o endpoint `/file` dispara as triggers `before_create` e `after_create` da entidade `upload` (`Trigger.ENTITY_UPLOAD`). **O endpoint `/image` NÃO dispara triggers** (vide seção 6).
31
+ - **`player`** — a URL retornada é tipicamente copiada para `Player.image` (`{ small, medium, original }`). Não há referência forte (FK); é apenas cópia de string de URL.
32
+ - **AI (`AIRest`)** — o módulo de geração de imagens por IA (`AIRest.java:2234`) também grava na coleção `upload`: baixa a imagem da URL da OpenAI, reenvia via `S3Service.create(..., session="image")` e salva um documento `upload`. Esse caminho usa **S3 diretamente** (ignora `getCloud()`).
33
+ - **`static`** — módulo distinto (coleção `static`); usa o mesmo `S3Service`/`StaticManager` mas **não** compartilha a coleção `upload`.
34
+
35
+ ---
36
+
37
+ ## 2. Arquitetura e Fluxos
38
+
39
+ ### 2.1 Classes envolvidas
40
+
41
+ | Classe | Papel |
42
+ |---|---|
43
+ | `com.funifier.rest.v3.rest.UploadRest` | Controller REST v3 (`/v3/upload`) — todos os 5 endpoints |
44
+ | `com.funifier.engine.aws.service.s3.S3Service` | Provedor **default** (Amazon S3) — `create` / `delete` / `deleteURL` |
45
+ | `com.funifier.engine.cloud.azure.AzureService` | Provedor Azure Blob Storage (quando `cloud="azure"`) |
46
+ | `com.funifier.engine.cloud.google.GoogleService` | Provedor Google Cloud Storage (quando `cloud="google"`) |
47
+ | `com.funifier.controller.Configuration` | `getCloud()` resolve o provedor ativo |
48
+ | `com.funifier.engine.guid.Guid` | `newShortGuid()` → `new org.bson.types.ObjectId().toString()` (hex de 24 chars) |
49
+ | `com.funifier.engine.integration.trigger.Trigger` | Constantes `ENTITY_UPLOAD`, `EVENT_BEFORE_CREATE`, `EVENT_AFTER_CREATE` |
50
+ | `com.funifier.rest.v3.AuthBean` | Resolução de credenciais (`getApiKey()`) |
51
+ | `net.coobird.thumbnailator.Thumbnails` | Biblioteca de resize/conversão de imagem (endpoint `/image`) |
52
+
53
+ Não há `Service`/`Repository`/`Manager`/`Scheduler` dedicado ao módulo `upload`. A persistência MongoDB é feita diretamente via Jongo: `manager.getJongoConnection().getCollection(Entity.UPLOAD.collection)`.
54
+
55
+ ### 2.2 Pipeline principal — `POST /v3/upload/file` (`uploadFile`)
56
+
57
+ Sequência real de execução (linhas 106-214 de `UploadRest.java`):
58
+
59
+ 1. **[Auth/Tenant]** `FrontController.getInstance(authBean.getApiKey()).getManagerFactory()` — resolve o tenant **pelo apiKey** (sem `.check()` de access_token; vide seção 8).
60
+ 2. **[Validação]** lê a parte `extra` do multipart. Se ausente/vazia → `400 BAD_REQUEST` com `{"message":"Field extra is required..."}`.
61
+ 3. **[Sanitização silenciosa]** `ex = ex.substring(0, ex.lastIndexOf("}")+1)` — **trunca tudo após a última `}`** antes de fazer parse JSON (vide seção 5).
62
+ 4. **[Default]** `session = extra.session ?? "upload"`.
63
+ 5. **[Loop por arquivo]** para cada `InputPart` da parte `file`:
64
+ 1. `getFileName(header)` extrai o nome do `Content-Disposition` (fallback `"unknown"`).
65
+ 2. lê os bytes via `IOUtils.toByteArray`.
66
+ 3. monta o `Map upload` com `_id` (novo `ObjectId`), `extra`, `size`, `filename` (nome original), `content_type`, `time` (`new Date()`), e `bytes`.
67
+ 4. **[Trigger BEFORE]** `triggerManager.execute(guid, upload, ENTITY_UPLOAD, EVENT_BEFORE_CREATE, ...)` — a trigger pode **mutar** `upload.bytes` e `upload.filename`; após ela o código relê `bytes`/`filename` e recalcula `size`.
68
+ 5. **[Upload nuvem]** conforme `getCloud()`: `S3Service.create` | `AzureService.create` | `GoogleService.create` → retorna `{filename_unique, bucket, extension, url}`, mesclado via `upload.putAll(after)`.
69
+ 6. **[Limpeza]** `upload.remove("bytes")` — os bytes **não** são persistidos no Mongo.
70
+ 7. **[Persistência]** `getCollection("upload").save(upload)`.
71
+ 8. **[Trigger AFTER]** `triggerManager.execute(..., EVENT_AFTER_CREATE, ...)`.
72
+ 9. adiciona ao array `uploads`.
73
+ 6. **[Resposta]** `201 CREATED` com `{"uploads":[...], "status":"OK"}`.
74
+
75
+ ```mermaid
76
+ flowchart TD
77
+ A["POST /v3/upload/file"] --> B{"extra presente?"}
78
+ B -->|"não"| E400["400 Field extra is required"]
79
+ B -->|"sim"| C["parse extra (trunca após última }) + session ?? upload"]
80
+ C --> D["para cada file part"]
81
+ D --> F["lê bytes + filename + content_type"]
82
+ F --> G["monta Map: _id, extra, size, filename, content_type, time, bytes"]
83
+ G --> H["trigger before_create (pode mutar bytes/filename)"]
84
+ H --> I{"getCloud()"}
85
+ I -->|"azure"| J1["AzureService.create"]
86
+ I -->|"google"| J2["GoogleService.create"]
87
+ I -->|"default"| J3["S3Service.create"]
88
+ J1 --> K["putAll(filename_unique, bucket, extension, url)"]
89
+ J2 --> K
90
+ J3 --> K
91
+ K --> L["remove(bytes)"]
92
+ L --> M["save() na coleção upload"]
93
+ M --> N["trigger after_create"]
94
+ N --> D
95
+ D --> O["201 CREATED uploads + status OK"]
96
+ ```
97
+
98
+ > **Síncrono.** Todo o pipeline roda na thread da requisição. **Sem transação**: se o `save()` no Mongo falhar após o upload na nuvem, o arquivo físico fica órfão (e vice-versa). `IOException` no loop é capturada e apenas logada (`printStackTrace`) — a resposta ainda retorna `201` com os uploads que conseguiram completar.
99
+
100
+ ### 2.3 Pipeline `POST /v3/upload/image` (`uploadImage`)
101
+
102
+ Difere de `/file` (linhas 250-420):
103
+
104
+ 1. `session = extra.session ?? "image"` (default diferente).
105
+ 2. lê `extra.thumbnails` (lista) e `extra.transform` (lista) — ambos opcionais.
106
+ 3. **[Transform]** se `transform` tem itens, aplica em sequência sobre os bytes originais via Thumbnailator: estágio `"size"` (`{width,height}`) e estágio `"outputFormat"` (`{format}`). Outros estágios são ignorados.
107
+ 4. grava o documento da **versão original** com `thumb="original"` (o arquivo físico é prefixado `original_` antes do guid da nuvem).
108
+ 5. **[Thumbnails]** para cada item de `thumbnails` (`{name,width,height}`): redimensiona, faz upload (prefixo `{name}_`) e grava **um documento `upload` separado** com `thumb={name}`.
109
+ 6. **NÃO dispara triggers.** **NÃO** adiciona/remove campo `bytes`.
110
+ 7. resposta `201 CREATED` com `uploads` contendo o original **+ um item por thumbnail**.
111
+
112
+ ```mermaid
113
+ flowchart TD
114
+ A["POST /v3/upload/image"] --> B{"extra presente?"}
115
+ B -->|"não"| E["400 Field extra is required"]
116
+ B -->|"sim"| C["session ?? image; lê thumbnails[] e transform[]"]
117
+ C --> D{"transform tem itens?"}
118
+ D -->|"sim"| T["aplica size / outputFormat (Thumbnailator)"]
119
+ D -->|"não"| U["bytes inalterados"]
120
+ T --> V["upload original (thumb=original, prefixo original_)"]
121
+ U --> V
122
+ V --> W["save() original"]
123
+ W --> X{"thumbnails tem itens?"}
124
+ X -->|"sim"| Y["para cada thumb: resize + upload (prefixo nome_) + save()"]
125
+ X -->|"não"| Z["fim do loop"]
126
+ Y --> Z
127
+ Z --> R["201 CREATED uploads[original + N thumbs]"]
128
+ ```
129
+
130
+ ### 2.4 Interação entre módulos (upload de arquivo)
131
+
132
+ ```mermaid
133
+ sequenceDiagram
134
+ participant C as Cliente
135
+ participant R as UploadRest
136
+ participant T as TriggerManager
137
+ participant S as Cloud (S3/Azure/Google)
138
+ participant M as MongoDB (coleção upload)
139
+ C->>R: POST /v3/upload/file (multipart: file, extra)
140
+ R->>R: valida extra, monta Map
141
+ R->>T: execute(before_create) [só /file]
142
+ T-->>R: upload (bytes/filename possivelmente mutados)
143
+ R->>S: create(bytes, filename, apiKey, session)
144
+ S-->>R: {filename_unique, bucket, extension, url}
145
+ R->>M: save(upload sem bytes)
146
+ R->>T: execute(after_create) [só /file]
147
+ R-->>C: 201 {uploads, status}
148
+ ```
149
+
150
+ ### 2.5 Pipeline de exclusão e consulta
151
+
152
+ - **`DELETE /v3/upload/{id}`** (`delete`, linhas 437-466): `findOne({_id})` → `remove({_id})` → lê `filename_unique` e `bucket` → **se** `url` contém o `apiKey`, apaga o arquivo físico no provedor → **sempre** chama `playerManager.delete(id)` no final (efeito colateral, vide seção 5).
153
+ - **`DELETE /v3/upload`** (corpo JSON, `delete`, linhas 482-504): **se** `url` contém o `apiKey`, apaga o arquivo no provedor e remove **todos** os documentos com aquele `url` (`remove({url:#})`).
154
+ - **`GET /v3/upload`** (`findAll`, linhas 529-590): monta aggregation MongoDB com `$match` (injetando `q` cru), filtro de data em `time`, `$sort` e `$limit`. Vide seção 4.
155
+
156
+ ---
157
+
158
+ ## 3. Estrutura dos Objetos
159
+
160
+ ### 3.1 Documento `upload` — coleção `upload`
161
+
162
+ Não há entidade tipada. Os campos abaixo são os efetivamente gravados pelo código (variam por endpoint).
163
+
164
+ | Campo | Tipo | Padrão | Origem | Descrição |
165
+ |---|---|---|---|---|
166
+ | `_id` | String | auto | `Guid.newShortGuid()` | Hex de `ObjectId` (24 chars). Gerado no handler. **≠** do guid usado no `filename_unique`. |
167
+ | `extra` | Object | — (obrigatório) | request | JSON arbitrário enviado na parte `extra`. Persistido verbatim em **todos** os documentos (inclusive em cada thumbnail). |
168
+ | `size` | int | — | runtime | Tamanho em bytes após triggers/transformações. |
169
+ | `filename` | String | `"unknown"` | `Content-Disposition` | **Nome original** do arquivo (não o nome único na nuvem). |
170
+ | `content_type` | String | — | header `Content-Type` da parte | MIME do arquivo enviado. |
171
+ | `time` | Date | `new Date()` | runtime | Momento do upload. Serializado como epoch em millis no JSON. |
172
+ | `filename_unique` | String | — | provedor (`create`) | Nome único no storage. **S3/Azure:** `{guid}_{filename}`. **Google:** caminho completo `games/{apiKey}/{session}/{guid}_{filename}`. |
173
+ | `bucket` | String | — | provedor | **S3:** `{bucketRegion}/games/{apiKey}/{session}`. **Azure:** `games/{apiKey}/{session}`. **Google:** apenas o nome do bucket. |
174
+ | `extension` | String | — | provedor | `"." + filename.split("\\.")[1]` — **segundo** token (vide seção 5, bug de extensão). |
175
+ | `url` | String | — | provedor | URL pública direta do arquivo. |
176
+ | `thumb` | String | ausente em `/file` | runtime (`/image`) | `"original"` ou o `name` do thumbnail. **Só existe nos documentos de `/image`.** |
177
+ | `exception` | Object | ausente | provedor (em falha) | Quando o upload na nuvem falha, `create()` retorna `{exception:{...}}` e o documento é salvo **mesmo assim**, sem `url`/`bucket` (vide seção 5). |
8
178
 
9
- ## Quando usar
179
+ ### 3.2 Campos NÃO persistidos / computados
10
180
 
11
- - Para upload de imagens de avatares
12
- - Para enviar documentos para treinamentos
13
- - Para adicionar recursos visuais a desafios e itens
181
+ - **`bytes`** (`byte[]`) — adicionado ao `Map` apenas no fluxo `/file` para passar pela trigger e pelo provedor; **removido com `upload.remove("bytes")` antes do `save()`**. Nunca chega ao Mongo. (No `/image` o campo `bytes` nunca é adicionado ao `Map`.)
14
182
 
15
- ## API Endpoints
183
+ ### 3.3 Campos legados / desatualizados (apenas no Javadoc, NÃO no runtime)
16
184
 
17
- ### Upload de Imagem
18
- **Método:** POST
19
- **Endpoint:** `/v3/upload/image`
20
- **Content-Type:** multipart/form-data
185
+ O Javadoc de `uploadFile`/`uploadImage` mostra campos que **o código não grava**:
21
186
 
22
- **Campos do FormData:**
23
- - `file`: conteúdo da imagem (ex: profile-pic.jpg)
24
- - `extra`: `{ "session": "images" }`
187
+ - `filename_original` — **não existe** no runtime; o nome original fica em `filename`.
188
+ - `bucket: "funifier/games/..."` formato **antigo**; o código atual usa `{bucketRegion}/games/...` (S3) — vide comentários `//OLD VERSION` / `//NEW VERSION : COM BUCKET` em `S3Service`.
189
+ - O bloco comentado de thumbnail em `uploadFile` (linhas 157-164) é **código morto** (geração de thumb 160×160 no `/file`), desabilitado. O `/file` não gera thumbnails.
25
190
 
26
- **Headers:**
27
- - `Authorization: {token}`
28
- - `Content-Type: undefined` (gerenciado pelo FormData)
191
+ ### 3.4 Estrutura de `extra` no endpoint `/image`
29
192
 
30
- **⚠️ O campo `extra` é obrigatório.** Sem ele, a API retorna erro `Field extra is required`.
193
+ `extra` pode conter, além de `session`:
194
+
195
+ | Campo de `extra` | Tipo | Significado |
196
+ |---|---|---|
197
+ | `thumbnails` | `[{name, width, height}]` | Cada item gera um documento/arquivo adicional redimensionado. `name` vira o valor de `thumb`. |
198
+ | `transform` | `[{stage, ...}]` | Pipeline aplicado **à imagem original**. Estágios reconhecidos: `{"stage":"size","width":W,"height":H}` e `{"stage":"outputFormat","format":"png"}`. Demais estágios são ignorados. |
199
+
200
+ > `width`/`height` são lidos com cast direto `(int) map.get("width")` — exige inteiro JSON. Valor fracionário (ex.: `160.0`) gera `ClassCastException` → `500` (vide seção 5).
201
+
202
+ ### 3.5 Enum de provedor (`Configuration.getCloud()`)
203
+
204
+ | Valor de `cloud` | Provedor | Observação |
205
+ |---|---|---|
206
+ | `"azure"` | `AzureService` | — |
207
+ | `"google"` | `GoogleService` | — |
208
+ | qualquer outro / vazio | `S3Service` (Amazon) | `getCloud()` retorna `"amazon"` como default quando vazio. |
209
+
210
+ ### 3.6 Ciclo de vida
211
+
212
+ O documento `upload` **não possui campo de status/estado**; é criado e (eventualmente) removido. Não há máquina de estados — por isso nenhum `stateDiagram` é aplicável. As únicas variações de "tipo" de documento são via `thumb` (`original` vs. nome do thumbnail) no fluxo `/image`.
213
+
214
+ ---
215
+
216
+ ## 4. Endpoints
217
+
218
+ > **Atenção:** o `@Produces` no nível dos métodos de upload é `text/plain`, embora o corpo seja JSON. Os métodos de consulta/exclusão usam `application/json`.
219
+
220
+ ### 4.1 `POST /v3/upload/file`
221
+
222
+ | Aspecto | Detalhe |
223
+ |---|---|
224
+ | Finalidade | Upload de **qualquer tipo de arquivo** (1 ou N arquivos). |
225
+ | Autenticação | Header `Authorization` (Bearer/Basic/Studio/Account) ou `X-Api-Key` / `?api_key` — resolve apiKey. |
226
+ | Content-Type | `multipart/form-data` |
227
+ | Dispara triggers | **Sim** (`before_create` + `after_create`). |
228
+ | Status sucesso | `201 CREATED` |
229
+
230
+ **Partes do multipart:**
231
+
232
+ | Parte | Obrigatória | Descrição |
233
+ |---|---|---|
234
+ | `file` | Sim | Conteúdo binário. Pode repetir para múltiplos arquivos. |
235
+ | `extra` | **Sim** | String JSON. Ausência → `400`. `session` define o subdiretório (default `"upload"`). |
236
+
237
+ **Comportamento real:** parse de `extra` trunca tudo após a última `}`. Falha de upload na nuvem **não** retorna erro HTTP — grava documento com `exception`. `IOException` no loop é silenciada.
238
+
239
+ **Exemplo (S3 / default), `extra = {"session":"images","name":"arquivo"}`, arquivo `funifier.png`:**
31
240
 
32
- **Exemplo de Resposta:**
33
241
  ```json
34
242
  {
35
243
  "uploads": [
36
244
  {
37
- "bucket": "funifier/games/{apiKey}/images",
38
- "extension": ".jpg",
39
- "filename": "profile-pic.jpg",
40
- "filename_unique": "{id}_original_profile-pic.jpg",
41
- "size": 5559,
42
- "content_type": "image/jpeg",
43
- "thumb": "original",
44
- "extra": { "session": "images" },
45
- "_id": "6998ecbc434ba010175f4dd1",
46
- "time": 1771629756465,
47
- "url": "https://s3.amazonaws.com/funifier/games/{apiKey}/images/{id}_original_profile-pic.jpg"
245
+ "_id": "5926104c8517667bbdf63c33",
246
+ "extra": { "session": "images", "name": "arquivo" },
247
+ "size": 3622,
248
+ "filename": "funifier.png",
249
+ "content_type": "image/png",
250
+ "time": 1495666764893,
251
+ "filename_unique": "5926104c8517667bbdf63c32_funifier.png",
252
+ "bucket": "funifier-na1/games/57bde3635e1c92c50b3ce6ff/images",
253
+ "extension": ".png",
254
+ "url": "https://s3.amazonaws.com/funifier-na1/games/57bde3635e1c92c50b3ce6ff/images/5926104c8517667bbdf63c32_funifier.png"
48
255
  }
49
256
  ],
50
257
  "status": "OK"
51
258
  }
52
259
  ```
53
260
 
54
- ### Exclusão de Arquivo por ID
55
- **Método:** DELETE
56
- **Endpoint:** `/v3/upload/{id}`
261
+ ### 4.2 `POST /v3/upload/image`
57
262
 
58
- Remove o registro do banco de dados e exclui o arquivo físico do storage (S3/Azure/Google Cloud).
263
+ | Aspecto | Detalhe |
264
+ |---|---|
265
+ | Finalidade | Upload de imagem com transformação e geração de thumbnails. |
266
+ | Autenticação | igual a 4.1 |
267
+ | Content-Type | `multipart/form-data` |
268
+ | Dispara triggers | **Não** |
269
+ | Status sucesso | `201 CREATED` |
59
270
 
60
- **Headers:**
61
- - `Authorization: {token}`
271
+ **Comportamento real:** `session` default `"image"`. Gera **N+1 documentos** (1 original + N thumbnails). O campo `filename` permanece o nome original; o nome físico recebe prefixo `original_`/`{thumbName}_` mais o guid do provedor.
62
272
 
63
- **Resposta:** `204 No Content`
64
-
65
- **Exemplo:**
273
+ **Request (cURL):**
274
+ ```bash
275
+ curl -X POST "{server}/v3/upload/image" \
276
+ -H "Authorization: Bearer {token}" \
277
+ -F 'file=@./funifier.png;type=image/png' \
278
+ -F 'extra={"session":"images","transform":[{"stage":"size","width":640,"height":640},{"stage":"outputFormat","format":"png"}],"thumbnails":[{"name":"small","width":160,"height":160},{"name":"medium","width":260,"height":260}]}'
66
279
  ```
67
- DELETE /v3/upload/6998ecbc434ba010175f4dd1
68
- Authorization: Bearer eyJhbGciOiJIUzUxMiIs...
280
+
281
+ **Response (resumido):**
282
+ ```json
283
+ {
284
+ "uploads": [
285
+ { "_id": "...", "thumb": "original", "filename": "funifier.png", "filename_unique": "..._original_funifier.png", "url": "...", "extension": ".png", "size": 50123 },
286
+ { "_id": "...", "thumb": "small", "filename": "funifier.png", "filename_unique": "..._small_funifier.png", "url": "...", "size": 5559 },
287
+ { "_id": "...", "thumb": "medium", "filename": "funifier.png", "filename_unique": "..._medium_funifier.png", "url": "...", "size": 9871 }
288
+ ],
289
+ "status": "OK"
290
+ }
69
291
  ```
70
292
 
71
- **Observação:** A exclusão só é feita se o arquivo pertencer ao mesmo `apiKey` da autorização (verificado pela URL do arquivo).
293
+ ### 4.3 `DELETE /v3/upload/{id}`
294
+
295
+ | Aspecto | Detalhe |
296
+ |---|---|
297
+ | Finalidade | Remover registro por `_id` e (condicionalmente) o arquivo físico. |
298
+ | Path param | `id` — `_id` do documento `upload`. |
299
+ | Status sucesso | `204 NO_CONTENT` |
300
+
301
+ **Comportamento real:**
302
+ - O documento é **sempre** removido do Mongo se existir (`remove({_id})`), **antes** de qualquer checagem de propriedade.
303
+ - O arquivo físico só é apagado **se** `url != null && url.length > 10 && url.contains(apiKey)`.
304
+ - **Efeito colateral:** ao final, `playerManager.delete(id)` é chamado **incondicionalmente** (vide seção 5).
305
+ - Sempre retorna `204`, mesmo se o `id` não existir.
306
+
307
+ ### 4.4 `DELETE /v3/upload` (corpo JSON)
308
+
309
+ | Aspecto | Detalhe |
310
+ |---|---|
311
+ | Finalidade | Remover arquivo e registros por `url`. |
312
+ | Content-Type | `application/json` |
313
+ | Body | `{ "url": "https://.../arquivo.ext" }` |
314
+ | Status sucesso | `204 NO_CONTENT` |
315
+
316
+ **Comportamento real:** só age **se** `url.contains(apiKey)`. Apaga o arquivo no provedor e remove **todos** os documentos com aquele `url` exato. Se a URL não contém o apiKey, **nada acontece** (e ainda assim retorna `204`).
317
+
318
+ ### 4.5 `GET /v3/upload`
319
+
320
+ | Aspecto | Detalhe |
321
+ |---|---|
322
+ | Finalidade | Listar/consultar documentos `upload` do tenant. |
323
+ | Status sucesso | `200 OK` |
324
+
325
+ **Query params:**
326
+
327
+ | Param | Tipo | Default | Descrição |
328
+ |---|---|---|---|
329
+ | `q` | String (fragmento Mongo) | — | **Injetado cru** no `$match` (vide seção 8). Ex.: `extra.session:"images"`. |
330
+ | `published_min` | String | — | Limite inferior em `time` (RFC 3339 ou keyword `-1d`, `-30m`, etc.). Inclusivo (`$gte`). |
331
+ | `published_max` | String | — | Limite superior em `time`. (`$lte`). |
332
+ | `orderby` | String | — | Campo de ordenação. **Injetado cru** no `$sort`. |
333
+ | `reverse` | boolean | `false` | `true` → ordem decrescente (`-1`). |
334
+ | `max_results` | int | `100` | `$limit`. Valores `<= 0` são forçados para `100`. |
335
+
336
+ **Pipeline gerado (real):**
337
+ ```
338
+ { $match : { _id: {$exists:true} [, <q>] [, time: {$gte:#, $lte:#}] } }
339
+ [ { $sort : { <orderby> : 1|-1 } } ]
340
+ [ { $limit : <max_results> } ]
341
+ ```
342
+ Campos nulos são removidos da resposta (`JsonUtil.toJsonRemoveNullFields`).
72
343
 
73
344
  ---
74
345
 
75
- ### Exclusão de Arquivo por URL
76
- **Método:** DELETE
77
- **Endpoint:** `/v3/upload`
78
- **Content-Type:** application/json
346
+ ## 5. Regras de Negócio (no código, fora do schema)
79
347
 
80
- Remove o arquivo pelo campo `url`.
348
+ - **`extra` é obrigatório** em `/file` e `/image`. Ausência → `400`.
349
+ - **Truncamento silencioso de `extra`**: `ex.substring(0, ex.lastIndexOf("}")+1)` descarta qualquer conteúdo após a última `}`. Se a string não contém `}`, `lastIndexOf` retorna `-1` → `substring(0,0)` → string vazia → parse vazio. Conteúdo extra (ex.: múltiplos objetos concatenados) é silenciosamente cortado.
350
+ - **`session` define o caminho** de armazenamento (`games/{apiKey}/{session}`). Defaults divergentes: `"upload"` (`/file`) vs `"image"` (`/image`).
351
+ - **Bug de extensão**: `"." + filename.split("\\.")[1]` usa o **segundo** token, não o último. `arquivo.tar.gz` → `extension = ".tar"`. **Arquivo sem ponto** → `ArrayIndexOutOfBoundsException` não tratada → `500`.
352
+ - **Falha de upload é silenciosa**: em erro de S3/Azure/Google, `create()` retorna `{exception:{...}}` (sem `url`), e o handler **mescla e salva o documento mesmo assim**, retornando `201`. Resultado: documento `upload` sem `url` e com campo `exception`.
353
+ - **Sem transação / sem rollback**: arquivo na nuvem e documento Mongo são independentes. Falha parcial deixa órfãos.
354
+ - **Propriedade por URL**: exclusão física só ocorre quando `url.contains(apiKey)`. Como a URL contém `games/{apiKey}/`, isso normalmente é verdadeiro para arquivos próprios; URLs de outras gamificações não são apagadas.
355
+ - **Efeito colateral em `DELETE /{id}`**: `playerManager.delete(id)` é chamado com o `_id` do upload. Como `_id` de upload é um `ObjectId` aleatório, normalmente **não** corresponde a nenhum player (no-op), mas é uma chamada não óbvia e potencialmente destrutiva se houver colisão de id. Comportamento legado.
356
+ - **`findAll` sem escopo de usuário/tenant interno**: a query roda no banco da própria gamificação (resolvido por apiKey), retornando **todos** os uploads daquela gamificação — não há filtro por usuário, sessão ou propriedade além do que vier em `q`.
357
+ - **`extra` redundante**: o objeto `extra` inteiro (inclusive `thumbnails`/`transform`) é persistido em cada documento gerado, incluindo cada thumbnail.
81
358
 
82
- **Body:**
83
- ```json
84
- { "url": "https://s3.amazonaws.com/funifier/games/{apiKey}/images/{filename}" }
359
+ ---
360
+
361
+ ## 6. Comportamentos Automáticos
362
+
363
+ | Comportamento | Trigger | Impacto | Persistência |
364
+ |---|---|---|---|
365
+ | Geração de `_id` | toda criação | `ObjectId` hex novo por documento | Sim (`_id`) |
366
+ | Trigger `before_create` (entidade `upload`) | **só** `POST /file` | Pode mutar `bytes` e `filename` antes do upload | Indireta (afeta arquivo/`size`) |
367
+ | Trigger `after_create` (entidade `upload`) | **só** `POST /file` | Pós-processamento (integrações, logs) | Conforme a trigger |
368
+ | Upload na nuvem | toda criação | Cria arquivo físico **público para leitura** | S3/Azure/Google |
369
+ | Transformação de imagem | `/image` com `transform` | Resize / mudança de formato da original | Sim (arquivo transformado) |
370
+ | Geração de thumbnails | `/image` com `thumbnails` | 1 arquivo + 1 documento por thumbnail | Sim (N documentos) |
371
+ | Índice `extra.session` | startup (`ConnectionPool`) | Cria índice na coleção `upload` | Sim (índice MongoDB) |
372
+ | Remoção de player | `DELETE /{id}` | `playerManager.delete(id)` incondicional | Sim (coleção `player`, se colidir) |
373
+
374
+ ```mermaid
375
+ flowchart LR
376
+ subgraph file["POST /file"]
377
+ A1["before_create"] --> A2["upload nuvem"] --> A3["save()"] --> A4["after_create"]
378
+ end
379
+ subgraph image["POST /image"]
380
+ B1["transform (opcional)"] --> B2["upload original + save()"] --> B3["thumbnails: resize + upload + save() (N)"]
381
+ end
85
382
  ```
86
383
 
87
- **Resposta:** `204 No Content`
384
+ > **Diferença crítica:** triggers de `upload` (`before_create`/`after_create`) **só** existem no `/file`. Qualquer automação dependente dessas triggers **não** roda para imagens enviadas via `/image`.
88
385
 
89
386
  ---
90
387
 
388
+ ## 7. Suportado vs NÃO Suportado
91
389
 
92
- ### Uso com Player (Avatar)
390
+ ### Suportado
93
391
 
94
- Para usar a imagem como avatar do jogador, após o upload:
392
+ - Upload de arquivo genérico (1 ou N) via `POST /v3/upload/file`, com triggers.
393
+ - Upload de imagem com `transform` (`size`, `outputFormat`) e `thumbnails` via `POST /v3/upload/image`.
394
+ - Armazenamento multi-cloud (S3 default, Azure, Google) selecionado por `Configuration.getCloud()`.
395
+ - Exclusão por `_id` (`DELETE /v3/upload/{id}`) e por `url` (`DELETE /v3/upload`).
396
+ - Consulta com `q` (Mongo), filtro de data (`published_min`/`published_max`), `orderby`, `reverse`, `max_results`.
397
+ - URLs públicas de leitura para download direto.
398
+ - Índice em `extra.session`.
95
399
 
96
- ```json
97
- POST /v3/player
98
- {
99
- "_id": "player_id",
100
- "image": {
101
- "small": { "url": "URL_DO_UPLOAD" },
102
- "medium": { "url": "URL_DO_UPLOAD" },
103
- "original": { "url": "URL_DO_UPLOAD" }
104
- }
105
- }
400
+ ### ❌ NÃO Suportado / Pegadinhas
401
+
402
+ - **`/image` não dispara triggers** (`before_create`/`after_create`) — apenas `/file`.
403
+ - **`filename_original`** — campo do Javadoc que o runtime **não grava**. Use `filename`.
404
+ - **Bloco de thumbnail no `/file`** (linhas 157-164) — **código morto** comentado; `/file` não gera thumbnails.
405
+ - **Validação de schema** — inexistente; coleção é `HashMap` schemaless.
406
+ - **Atualização (PUT/PATCH)** de um upload — **não há** endpoint de update; só create/delete/query.
407
+ - **Endpoint `DELETE /v3/upload/image`** mencionado em docs antigas — **não existe** no código. A exclusão por URL é `DELETE /v3/upload` (sem sufixo `/image`).
408
+ - **Rollback transacional** entre arquivo físico e documento Mongo — inexistente.
409
+ - **Filtro automático por usuário/propriedade** em `GET /v3/upload` — inexistente.
410
+ - **Tratamento de erro de upload** — falha de nuvem não vira erro HTTP; vira documento com `exception`.
411
+ - **Arquivos sem extensão** — quebram (`500`) por `filename.split("\\.")[1]`.
412
+ - **Acesso privado / ACL restrita** — arquivos S3/Google são gravados com leitura pública para `AllUsers`; não há opção de upload privado neste fluxo.
413
+
414
+ ---
415
+
416
+ ## 8. Segurança e Permissões
417
+
418
+ - **Autenticação por apiKey, sem validação de access_token no handler.** Os handlers chamam `FrontController.getInstance(authBean.getApiKey()).getManagerFactory()` diretamente — **não** chamam `authBean.getManagerFactory()` (que executaria `.check(accessToken)`) nem verificam escopo/permissão. A resolução do tenant depende apenas de extrair o apiKey das credenciais (`AuthBean.getApiKey()`: Bearer/Basic/Studio/Account/`X-Api-Key`/`?api_key`). Se existir validação de token, ela depende de filtro global de request **não presente nesta classe**. Documentado como observado, sem afirmar exploração.
419
+ - **Isolamento multi-tenant** é feito por **banco-por-gamificação** (cada apiKey → conexão MongoDB própria via `FrontController`) e por prefixo de storage `games/{apiKey}/{session}`. Não há campo de tenant filtrado nas queries.
420
+ - **Arquivos públicos por padrão:**
421
+ - S3: `acl.grantPermission(GroupGrantee.AllUsers, Permission.Read)` em todo `putObject`.
422
+ - Google: `Acl.of(Acl.User.ofAllUsers(), Acl.Role.READER)`.
423
+ - Azure: o container `funifier` é criado sem acesso público explícito (a linha `BlobContainerPublicAccessType.BLOB` está **comentada**) — a visibilidade depende da configuração default do container.
424
+ - **Superfície de injeção (`GET /v3/upload`):** os parâmetros `q` e `orderby` são **concatenados crus** na string da aggregation (`query.append(", " + q)` e `"{$sort : {" + orderby + " : #}}"`). Isso permite manipular o `$match`/`$sort` com operadores MongoDB arbitrários (NoSQL injection dentro do escopo do banco do próprio tenant). Não há sanitização. O escopo de dano é limitado ao banco da gamificação autenticada, mas qualquer campo/operador pode ser injetado.
425
+ - **Exclusão guardada por substring de URL:** `url.contains(apiKey)` é uma checagem fraca de propriedade — baseada em substring da URL, não em verificação de ACL/dono real.
426
+
427
+ ---
428
+
429
+ ## 9. Observabilidade e Troubleshooting
430
+
431
+ **Diagnóstico — o módulo está funcionando?**
432
+ - Confirme o provedor ativo: valor de `cloud` em `Configuration` (`getCloud()` → `amazon`/`azure`/`google`). Logs de startup imprimem `=== FUNIFIER_CLOUD_PROVIDER [...] ===`.
433
+ - Faça um upload de teste e verifique se o `url` retornado é acessível publicamente (GET na URL).
434
+
435
+ **Queries úteis (banco da gamificação):**
436
+ ```
437
+ GET /v3/upload?q=extra.session:"images"&orderby=time&reverse=true&max_results=10
438
+ GET /v3/upload?published_min=-1d&orderby=time&reverse=true
439
+ GET /v3/upload/<id> # (via q, pois não há rota GET /{id}: use q=_id:"<id>")
106
440
  ```
441
+ Índice disponível: **apenas `extra.session`** (`ConnectionPool.java:164`). **`time` NÃO é indexado**, embora `findAll` filtre e ordene por ele — consultas por data em coleções grandes podem ser lentas (collection scan).
442
+
443
+ **Erros comuns e causas:**
107
444
 
108
- **Nota:** O campo `image` do player deve ter o formato `{ small: { url }, medium: { url }, original: { url } }`. A URL é obtida de `response.uploads[0].url`.
445
+ | Sintoma | Causa provável |
446
+ |---|---|
447
+ | `400 Field extra is required` | parte `extra` ausente ou vazia no multipart. |
448
+ | Documento salvo com campo `exception` e sem `url` | falha no provedor de nuvem (credenciais, região, rede). Upload "passou" (201) mas o arquivo não existe. |
449
+ | `500` ao enviar arquivo | nome de arquivo sem ponto/extensão (`split("\\.")[1]` estoura) ou `width`/`height` fracionário em `transform`/`thumbnails`. |
450
+ | `extension` errada (ex.: `.tar`) | nome com múltiplos pontos — usa o **segundo** token. |
451
+ | Triggers de upload não rodaram | upload foi via `/image` (não dispara triggers) e não via `/file`. |
452
+ | Arquivo não foi apagado no `DELETE` | `url` não contém o `apiKey`, ou `url` ausente/curta no documento. |
453
+ | Player sumiu após deletar upload | efeito colateral `playerManager.delete(id)` (colisão de id). |
109
454
 
110
- ### Upload de Arquivo Genérico
111
- **Método:** POST
112
- **Endpoint:** `/v3/upload/file`
113
- **Content-Type:** multipart/form-data
455
+ **O que verificar quando algo não funciona:** credenciais do provedor; valor de `cloud`; se a `url` foi realmente preenchida no documento (indica sucesso do upload físico); se o `_id` consultado existe; se `q` está sintaticamente válido como fragmento Mongo.
114
456
 
115
- Suporta **qualquer tipo de arquivo** (não apenas imagens). Aceita múltiplos arquivos.
457
+ ---
116
458
 
117
- **Campos:**
118
- - `file`: arquivo binário (pode repetir para vários)
119
- - `extra`: string JSON obrigatória (ex: `{"session": "docs"}`)
459
+ ## 10. Exemplos Práticos
120
460
 
121
- **cURL:**
461
+ ### 10.1 Mínimo funcional (arquivo genérico)
122
462
  ```bash
123
463
  curl -X POST "{server}/v3/upload/file" \
124
464
  -H "Authorization: Bearer {token}" \
125
465
  -F 'file=@./documento.pdf;type=application/pdf' \
126
- -F 'extra={"session":"docs","name":"manual"}'
466
+ -F 'extra={"session":"docs"}'
127
467
  ```
128
468
 
129
- ### Deletar Upload por URL
130
- **Método:** DELETE
131
- **Endpoint:** `/v3/upload/image`
132
-
133
- **Body:**
469
+ ### 10.2 Avançado (imagem com transform + thumbnails)
470
+ ```bash
471
+ curl -X POST "{server}/v3/upload/image" \
472
+ -H "Authorization: Bearer {token}" \
473
+ -F 'file=@./avatar.png;type=image/png' \
474
+ -F 'extra={
475
+ "session":"avatars",
476
+ "transform":[
477
+ {"stage":"size","width":640,"height":640},
478
+ {"stage":"outputFormat","format":"png"}
479
+ ],
480
+ "thumbnails":[
481
+ {"name":"small","width":160,"height":160},
482
+ {"name":"medium","width":260,"height":260}
483
+ ]
484
+ }'
485
+ ```
486
+ Resultado: 3 documentos (`thumb`: `original`, `small`, `medium`), cada um com `url` próprio. Para usar como avatar do player:
134
487
  ```json
135
- { "url": "https://s3.amazonaws.com/funifier/games/{APIKEY}/images/59359e..._logo.png" }
488
+ POST /v3/player
489
+ {
490
+ "_id": "player_id",
491
+ "image": {
492
+ "small": { "url": "URL_DO_THUMB_small" },
493
+ "medium": { "url": "URL_DO_THUMB_medium" },
494
+ "original": { "url": "URL_DO_ORIGINAL" }
495
+ }
496
+ }
136
497
  ```
137
498
 
138
- ### Listar Uploads
139
- **Método:** GET
140
- **Endpoint:** `/v3/upload`
499
+ ### 10.3 Anti-pattern (o que NÃO fazer)
500
+ ```bash
501
+ # RUIM: nome de arquivo sem extensão → 500 (ArrayIndexOutOfBounds em split("\\.")[1])
502
+ curl ... -F 'file=@./arquivo_sem_extensao'
503
+
504
+ # RUIM: width fracionário → ClassCastException → 500
505
+ -F 'extra={"transform":[{"stage":"size","width":640.0,"height":640.0}]}'
141
506
 
142
- ## Triggers de Upload
507
+ # RUIM: assumir que o upload falhou retorna erro HTTP.
508
+ # Se a nuvem falhar, a resposta ainda é 201 e o documento é salvo com "exception" e sem "url".
509
+ # SEMPRE valide a presença de uploads[i].url antes de usar.
143
510
 
144
- O upload dispara triggers `BEFORE_CREATE` e `AFTER_CREATE` na entidade `UPLOAD`. Isso permite:
145
- - Validar arquivos antes de salvar
146
- - Processar metadados após upload
147
- - Integrar com outros módulos
511
+ # RUIM: depender de trigger before/after_create ao usar /image ela NÃO dispara.
512
+ # Use /file se precisar de triggers.
513
+ ```
148
514
 
149
- ## Validações e Testes
515
+ ---
150
516
 
151
- - [ ] Upload de imagem retorna URL válida
152
- - [ ] Upload de arquivo genérico funciona
153
- - [ ] URL da imagem é acessível publicamente
154
- - [ ] Diferentes formatos são aceitos (jpg, png, pdf, etc.)
155
- - [ ] Delete por URL remove o arquivo
517
+ ## Checklist de Configuração
518
+
519
+ - [ ] Parte `extra` presente e com JSON válido (sem ela → `400`).
520
+ - [ ] `extra` termina na `}` correta (conteúdo após a última `}` é descartado silenciosamente).
521
+ - [ ] Nome do arquivo **possui extensão** com um único ponto (evita `500` e `extension` errada).
522
+ - [ ] `session` definido conscientemente (define o caminho `games/{apiKey}/{session}`; default `upload`/`image`).
523
+ - [ ] Em `/image`, `width`/`height` são **inteiros** (não use `640.0`).
524
+ - [ ] Validar `uploads[i].url` na resposta antes de usar (presença de `exception` indica falha silenciosa).
525
+ - [ ] Se precisar de automação por trigger, usar `/file` (o `/image` não dispara triggers).
526
+ - [ ] Provedor de nuvem (`Configuration.cloud`) e credenciais configurados; do contrário documentos são salvos com `exception`.
527
+ - [ ] Ciente de que arquivos ficam **públicos para leitura** (S3/Google).
528
+ - [ ] Em `GET /v3/upload`, tratar `q`/`orderby` como entrada confiável (concatenação crua — risco de NoSQL injection).