funifier-mcp 0.2.25 → 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 +5 -2
- 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 +1011 -77
- 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/core/api-client.d.ts +21 -1
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/api-client.js +154 -1
- package/dist/core/api-client.js.map +1 -1
- package/dist/core/constants.d.ts +14 -0
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +14 -0
- package/dist/core/constants.js.map +1 -1
- package/dist/core/types/Folder.d.ts +16 -0
- package/dist/core/types/Folder.d.ts.map +1 -0
- package/dist/core/types/Folder.js +3 -0
- package/dist/core/types/Folder.js.map +1 -0
- package/dist/core/types/FolderContent.d.ts +10 -0
- package/dist/core/types/FolderContent.d.ts.map +1 -0
- package/dist/core/types/FolderContent.js +3 -0
- package/dist/core/types/FolderContent.js.map +1 -0
- package/dist/core/types/FolderContentType.d.ts +10 -0
- package/dist/core/types/FolderContentType.d.ts.map +1 -0
- package/dist/core/types/FolderContentType.js +3 -0
- package/dist/core/types/FolderContentType.js.map +1 -0
- package/dist/core/types/FolderLog.d.ts +11 -0
- package/dist/core/types/FolderLog.d.ts.map +1 -0
- package/dist/core/types/FolderLog.js +3 -0
- package/dist/core/types/FolderLog.js.map +1 -0
- package/dist/core/types/index.d.ts +4 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +4 -0
- package/dist/core/types/index.js.map +1 -1
- package/dist/mcp/bundle.js +121 -87
- package/dist/mcp/check-update.d.ts +2 -0
- package/dist/mcp/check-update.d.ts.map +1 -0
- package/dist/mcp/check-update.js +44 -0
- package/dist/mcp/check-update.js.map +1 -0
- package/dist/mcp/index.js +5 -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/_char-guard.js +1 -1
- package/dist/mcp/tools/_char-guard.js.map +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
- package/dist/mcp/tools/_fetch-current.js +12 -0
- package/dist/mcp/tools/_fetch-current.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 +33 -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 +4 -0
- package/dist/mcp/tools/folder.d.ts.map +1 -0
- package/dist/mcp/tools/folder.js +68 -0
- package/dist/mcp/tools/folder.js.map +1 -0
- 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 +5 -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 +26 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +192 -1
- 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/datasource-funifier-docs/.search-index.json +0 -17318
- package/datasource-funifier-docs/.skills-map.json +0 -73
- 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,67 +1,197 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `public`
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/public`
|
|
4
|
-
**API Endpoint:** `/v3/public`
|
|
4
|
+
**API Endpoint (CRUD):** `/v3/public`
|
|
5
|
+
**API Endpoint (execução):** `/v3/pub/{apikey}/{slug}`
|
|
6
|
+
**Coleção MongoDB:** `public_endpoint`
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
> Documentação de engenharia reversa baseada **exclusivamente** no código-fonte de `funifier-service`.
|
|
9
|
+
> Arquivos analisados:
|
|
10
|
+
> - `com/funifier/rest/v3/rest/PublicRest.java` (CRUD autenticado)
|
|
11
|
+
> - `com/funifier/rest/v3/rest/PubRest.java` (execução pública)
|
|
12
|
+
> - `com/funifier/engine/pub/PublicManager.java` (lógica de compilação/execução)
|
|
13
|
+
> - `com/funifier/engine/pub/PublicEndpoint.java` (entidade)
|
|
14
|
+
> - `com/funifier/engine/pub/PublicResponse.java` (DTO de resposta)
|
|
15
|
+
> - `com/funifier/engine/integration/trigger/TriggerExpressionChecker.java` (sandbox Groovy)
|
|
16
|
+
> - `com/funifier/engine/util/Entity.java` (`PUBLIC_ENDPOINT("public_endpoint", ...)`)
|
|
17
|
+
> - `com/funifier/engine/app/AppManager.java` (criação programática / instalação de app)
|
|
7
18
|
|
|
8
|
-
|
|
19
|
+
---
|
|
9
20
|
|
|
10
|
-
##
|
|
21
|
+
## 1. Visão Geral
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
- Para integrar com automações (Zapier, Make)
|
|
14
|
-
- Para criar webhooks de recebimento
|
|
15
|
-
- Para endpoints que não requerem autenticação por token
|
|
23
|
+
O módulo `public` permite definir **endpoints HTTP dinâmicos** cuja lógica é um **script Groovy** armazenado no banco. Cada endpoint é um documento na coleção `public_endpoint`. A invocação acontece por uma URL **sem token de autenticação**: `GET|POST /v3/pub/{apikey}/{slug}`, onde `apikey` é a chave pública da gamificação (resolve o tenant) e `slug` é o `_id` do documento.
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
**Papel arquitetural:**
|
|
18
26
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
- [ ] Escrever script Java com método `public Object handle(Object payload)`
|
|
23
|
-
- [ ] Ativar endpoint (active: true)
|
|
24
|
-
- [ ] Testar via URL pública
|
|
27
|
+
- É a superfície "serverless" da plataforma: o cliente envia um payload e o servidor executa um script Groovy arbitrário (dentro de um sandbox) que tem acesso ao `ManagerFactory` daquela gamificação — ou seja, a todos os managers (Player, Point, Achievement, Database, etc.).
|
|
28
|
+
- Resolve o problema de **integração com sistemas externos que não conseguem autenticar via token** (formulários, automações tipo Zapier/Make, webhooks de terceiros) — basta conhecer a `apikey` pública e o `slug`.
|
|
29
|
+
- A entidade carrega comentários internos chamando-a de *"prepared aggregate"* (ex.: `PublicEndpoint.java:40`, e o método comentado `// public String getScript(PreparedAggregate trigger)` em `PublicManager.java:109`). Isso é **legado**: o código foi adaptado da entidade `prepared_aggregate` (`Entity.PREPARED_AGGREGATE`). **Não existe nenhum pipeline de aggregate MongoDB no runtime do `public`** — o script é Groovy livre, não um aggregate preparado.
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
**Relação com outros módulos:**
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
**
|
|
30
|
-
|
|
33
|
+
- Compartilha a mesma técnica de sandbox de script com `trigger` e `scheduler`: o checker `TriggerExpressionChecker` (pacote `engine.integration.trigger`) é reutilizado aqui.
|
|
34
|
+
- A criação pode ser disparada pela instalação de um **App/pacote** (`AppManager.funifierCreateCustomEndpoint`), além do CRUD direto.
|
|
35
|
+
- Em runtime, o script pode tocar qualquer coleção/manager via `manager.getXxxManager()`.
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
]
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Arquitetura e Fluxos
|
|
40
|
+
|
|
41
|
+
### 2.1 Pipeline principal — execução (`/v3/pub/{apikey}/{slug}`)
|
|
42
|
+
|
|
43
|
+
Sequência real (`PubRest.executePublicEndpoint` → `PublicManager.run` → `executor`):
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
[Trigger HTTP] → PubRest.handleGet / handlePost
|
|
47
|
+
[Montagem payload]
|
|
48
|
+
GET → uriInfo.getQueryParameters() → payload.put(key, values.get(0)) // apenas o 1º valor de cada query param
|
|
49
|
+
POST → body (JSON desserializado em HashMap<String,Object>)
|
|
50
|
+
[Resolução tenant] → FrontController.getInstance(apikey).getManagerFactory()
|
|
51
|
+
[Lookup] → PublicManager.find(slug) // findOne({_id: slug})
|
|
52
|
+
[Validação 1] → endpoint == null → HTTP 404 (PublicResponse 404)
|
|
53
|
+
[Validação 2] → !endpoint.active → HTTP 403 (PublicResponse 403)
|
|
54
|
+
[Validação 3] → !method.equalsIgnoreCase(endpoint.method) → HTTP 406 (PublicResponse statusCode=405)
|
|
55
|
+
[Execução] → PublicManager.run(endpoint, payload, manager)
|
|
56
|
+
[Resposta] → Response.status(result.statusCode).entity(result.payload)
|
|
57
|
+
+ header X-Response-Time: <ms>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Lógica interna de `run()`:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
se endpoint.updated == null:
|
|
64
|
+
save(endpoint) # rede de segurança p/ docs inseridos fora do save()
|
|
65
|
+
|
|
66
|
+
lastCompilation = dates[endpoint._id]
|
|
67
|
+
clazz = compiled[endpoint._id]
|
|
68
|
+
|
|
69
|
+
se clazz == null OU lastCompilation == null OU lastCompilation.before(endpoint.updated):
|
|
70
|
+
script = getScript(endpoint.script) # injeta imports + wrapper class
|
|
71
|
+
clazz = compile(script) # SecureASTCustomizer + TriggerExpressionChecker
|
|
72
|
+
compiled[endpoint._id] = clazz
|
|
73
|
+
dates[endpoint._id] = agora
|
|
74
|
+
|
|
75
|
+
result = executor(clazz, endpoint, payload, manager) # roda handle(payload) com timeout
|
|
76
|
+
retorna PublicResponse(200, "OK", result)
|
|
77
|
+
|
|
78
|
+
# em qualquer exceção (compilação OU execução):
|
|
79
|
+
retorna PublicResponse(500, "Erro ao compilar ou executar o script", ex.getMessage())
|
|
46
80
|
```
|
|
47
81
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
**
|
|
82
|
+
Características:
|
|
83
|
+
|
|
84
|
+
- **Síncrono.** A requisição HTTP fica bloqueada até o script terminar (ou estourar timeout).
|
|
85
|
+
- **Sem transação.** Não há transação MongoDB envolvendo o que o script faz. Cada operação do script no banco é independente.
|
|
86
|
+
- **Dois timeouts** (ver seção 5): `@TimedInterrupt(5s)` no wrapper Groovy + `Future.get(timeout)` no executor (default 10s, ou `endpoint.timeout`).
|
|
87
|
+
|
|
88
|
+
### Fluxo de execução — `/v3/pub`
|
|
89
|
+
|
|
90
|
+
```mermaid
|
|
91
|
+
flowchart TD
|
|
92
|
+
A["Cliente: GET/POST /v3/pub/{apikey}/{slug}"] --> B[PubRest.executePublicEndpoint]
|
|
93
|
+
B --> C[FrontController.getInstance apikey]
|
|
94
|
+
C --> D["PublicManager.find(slug)"]
|
|
95
|
+
D --> E{endpoint == null?}
|
|
96
|
+
E -- sim --> E1["HTTP 404 — PublicResponse(404)"]
|
|
97
|
+
E -- não --> F{active?}
|
|
98
|
+
F -- não --> F1["HTTP 403 — PublicResponse(403)"]
|
|
99
|
+
F -- sim --> G{method confere?}
|
|
100
|
+
G -- não --> G1["HTTP 406 — PublicResponse statusCode=405"]
|
|
101
|
+
G -- sim --> H["PublicManager.run()"]
|
|
102
|
+
H --> I{cache de classe válido?}
|
|
103
|
+
I -- não --> J["getScript + compile + cacheia"]
|
|
104
|
+
I -- sim --> K[reusa classe compilada]
|
|
105
|
+
J --> L["executor: handle(payload) — single-thread + timeout"]
|
|
106
|
+
K --> L
|
|
107
|
+
L --> M{exceção / timeout?}
|
|
108
|
+
M -- sim --> M1["HTTP 500 — corpo = ex.getMessage()"]
|
|
109
|
+
M -- não --> N["HTTP 200 — corpo = retorno de handle()"]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Interação entre componentes
|
|
113
|
+
|
|
114
|
+
```mermaid
|
|
115
|
+
sequenceDiagram
|
|
116
|
+
participant C as Cliente
|
|
117
|
+
participant R as PubRest
|
|
118
|
+
participant M as PublicManager
|
|
119
|
+
participant G as Groovy (FunifierPublicEndpoint)
|
|
120
|
+
C->>R: POST /v3/pub/{apikey}/{slug} + payload
|
|
121
|
+
R->>M: find(slug)
|
|
122
|
+
M-->>R: PublicEndpoint (ou null)
|
|
123
|
+
R->>M: run(endpoint, payload, manager)
|
|
124
|
+
M->>M: compile + cache (se dates[_id] < updated)
|
|
125
|
+
M->>G: setManager(manager)
|
|
126
|
+
M->>G: handle(payload) [executor single-thread, timeout]
|
|
127
|
+
G-->>M: result
|
|
128
|
+
M-->>R: PublicResponse(200, "OK", result)
|
|
129
|
+
R-->>C: HTTP 200 + result (somente o payload)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 2.2 Pipeline secundário — CRUD (`/v3/public`)
|
|
133
|
+
|
|
134
|
+
`PublicRest` expõe o gerenciamento autenticado (ver seção 4). É um CRUD fino sobre `PublicManager`:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
GET /v3/public/{id} → PublicManager.find(id) → 200 + JSON (campos null omitidos)
|
|
138
|
+
POST /v3/public → PublicManager.save(obj) → 201 + JSON do objeto salvo
|
|
139
|
+
DELETE /v3/public/{id} → PublicManager.delete(id) → 204 No Content
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 3. Estrutura dos Objetos
|
|
145
|
+
|
|
146
|
+
### 3.1 `PublicEndpoint` — documento raiz (coleção `public_endpoint`)
|
|
147
|
+
|
|
148
|
+
Anotações: `@Data @NoArgsConstructor @AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown=true)`. **Todos os campos são `public`** (acesso direto, sem getters/setters obrigatórios no script).
|
|
149
|
+
|
|
150
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
151
|
+
| ------------- | --------------------- | ------------- | ----------- | --------- |
|
|
152
|
+
| `_id` | String | auto | não | Slug usado na URL pública (`/v3/pub/{apikey}/{_id}`). Se vazio/`null` no `save()`, gerado via `Guid.shortTimeMillis()`. |
|
|
153
|
+
| `apikey` | String | `null` | não | "apikey da gamificação". **Não é usado na execução** — o tenant vem da `apikey` da URL. Campo informativo/legado. |
|
|
154
|
+
| `title` | String | `null` | não | Nome amigável (uso interno/Studio). |
|
|
155
|
+
| `description` | String | `null` | não | Descrição interna do propósito. |
|
|
156
|
+
| `active` | boolean | `false` | não* | Se `false`, a execução retorna **403**. Como `boolean` primitivo, ausência no JSON ⇒ `false` ⇒ endpoint **desativado por padrão**. |
|
|
157
|
+
| `method` | String | `"POST"` | não | Método HTTP aceito. Comparado via `equalsIgnoreCase`. Valores usados: `"POST"` ou `"GET"`. |
|
|
158
|
+
| `script` | String | `null` | sim (de fato)| Corpo do script Groovy. Deve definir o método `handle(payload)`. |
|
|
159
|
+
| `timeout` | Long | `null` | não | Timeout de execução **em segundos**. `null` ou `<= 0` ⇒ usa **10s**. |
|
|
160
|
+
| `sample` | String | `null` | não | Exemplo de payload esperado (documentação; não usado em runtime). |
|
|
161
|
+
| `updated` | Date | auto | não | Setado para `new Date()` a cada `save()`. **Chave da invalidação de cache** de compilação. |
|
|
162
|
+
| `extra` | Map\<String,Object\> | `{}` | não | Campo livre. **Atenção:** é um campo literal chamado `extra`, **não** um catch-all do Jackson (não há `@JsonAnySetter`). Só é populado se o JSON tiver a chave `"extra"`. |
|
|
51
163
|
|
|
52
|
-
|
|
53
|
-
**Método:** POST (ou conforme configurado)
|
|
54
|
-
**Endpoint:** `/v3/pub/{apikey}/{slug}`
|
|
55
|
-
**Descrição:** Executa o endpoint público. Não requer token de autenticação.
|
|
164
|
+
\* Tecnicamente nenhum campo tem validação de obrigatoriedade no código; `script` é obrigatório na prática (sem ele não há `handle` para invocar).
|
|
56
165
|
|
|
57
|
-
|
|
166
|
+
**Campos computados / não persistidos pelo cliente:**
|
|
167
|
+
- `updated` é sempre sobrescrito pelo servidor no `save()` — qualquer valor enviado pelo cliente é ignorado.
|
|
58
168
|
|
|
59
|
-
|
|
169
|
+
**Campos aceitos e silenciosamente ignorados:**
|
|
170
|
+
- `apikey` — aceito no schema, **não tem efeito** no fluxo de execução (`PubRest` resolve o tenant pela `apikey` da URL).
|
|
171
|
+
- `sample` — persistido, mas nunca lido pelo runtime.
|
|
172
|
+
- Qualquer chave JSON desconhecida — descartada por `@JsonIgnoreProperties(ignoreUnknown=true)` (**não** vai para `extra`).
|
|
60
173
|
|
|
61
|
-
|
|
174
|
+
**Geração de `_id`:** `Guid.shortTimeMillis()` ⇒ `encode(System.currentTimeMillis(), Guid.range)` — string curta derivada do timestamp. Só ocorre quando `_id` é vazio/`null`.
|
|
62
175
|
|
|
63
|
-
|
|
64
|
-
|
|
176
|
+
### 3.2 `PublicResponse` — DTO de resposta
|
|
177
|
+
|
|
178
|
+
| Campo | Tipo | Descrição |
|
|
179
|
+
| ------------ | ------ | --------- |
|
|
180
|
+
| `statusCode` | int | Código lógico (200, 404, 403, 405, 500). **Nem sempre igual ao status HTTP** (ver seção 4). |
|
|
181
|
+
| `message` | String | Mensagem resumida. **Descartada no sucesso** (ver abaixo). |
|
|
182
|
+
| `payload` | Object | Dado de retorno. No sucesso, é o valor retornado por `handle()`. |
|
|
183
|
+
|
|
184
|
+
> **Assimetria crítica do corpo da resposta** (`PubRest`):
|
|
185
|
+
> - **Sucesso** → `entity(result.payload)`: o corpo é **apenas** o retorno de `handle()`. `statusCode` e `message` do `PublicResponse` são **descartados**.
|
|
186
|
+
> - **Erro de pré-checagem** (404/403/406) → `entity(new PublicResponse(...))`: o corpo é o **objeto `PublicResponse` completo** em JSON.
|
|
187
|
+
> - **Erro no script** (capturado dentro de `run()`) → `run()` devolve `PublicResponse(500, "Erro...", ex.getMessage())` e `PubRest` ainda faz `entity(result.payload)` ⇒ o corpo é a **string crua da mensagem de exceção**, com HTTP 500.
|
|
188
|
+
|
|
189
|
+
### 3.3 Wrapper de execução (`FunifierPublicEndpoint`)
|
|
190
|
+
|
|
191
|
+
O `script` do usuário **não roda standalone**: `getScript()` o injeta dentro de uma classe Groovy gerada. Estrutura literal montada (`PublicManager.getScript`, linhas 110-207):
|
|
192
|
+
|
|
193
|
+
```groovy
|
|
194
|
+
// imports automáticos (NÃO escreva 'import' no seu script)
|
|
65
195
|
import groovyx.net.http.*
|
|
66
196
|
import static groovyx.net.http.Method.*
|
|
67
197
|
import static groovyx.net.http.ContentType.*
|
|
@@ -77,34 +207,49 @@ import org.apache.http.client.methods.HttpPost
|
|
|
77
207
|
import org.apache.http.impl.client.HttpClientBuilder
|
|
78
208
|
import org.apache.commons.io.IOUtils
|
|
79
209
|
import java.lang.StringBuffer
|
|
80
|
-
|
|
210
|
+
// resize de imagens
|
|
211
|
+
import net.coobird.thumbnailator.Thumbnails
|
|
212
|
+
import java.io.InputStream
|
|
213
|
+
import java.io.ByteArrayInputStream
|
|
214
|
+
import java.io.ByteArrayOutputStream
|
|
215
|
+
import java.io.BufferedReader
|
|
216
|
+
import java.io.IOException
|
|
81
217
|
import com.fasterxml.jackson.core.JsonProcessingException
|
|
82
|
-
// Unirest
|
|
218
|
+
// Unirest
|
|
83
219
|
import com.mashape.unirest.http.HttpResponse
|
|
84
220
|
import com.mashape.unirest.http.JsonNode
|
|
221
|
+
import com.mashape.unirest.http.ObjectMapper
|
|
85
222
|
import com.mashape.unirest.http.Unirest
|
|
86
223
|
import com.mashape.unirest.http.exceptions.UnirestException
|
|
87
224
|
import com.mashape.unirest.request.GetRequest
|
|
88
225
|
import com.mashape.unirest.request.HttpRequestWithBody
|
|
89
|
-
// Mail
|
|
226
|
+
// Mail
|
|
90
227
|
import org.simplejavamail.email.Email
|
|
91
228
|
import org.simplejavamail.email.EmailBuilder
|
|
92
229
|
import org.simplejavamail.mailer.MailerBuilder
|
|
93
|
-
// Funifier
|
|
94
|
-
import com.funifier.engine.action
|
|
95
|
-
import com.funifier.engine.
|
|
96
|
-
import com.funifier.engine.
|
|
97
|
-
import com.funifier.engine.
|
|
98
|
-
import com.funifier.engine.
|
|
99
|
-
import com.funifier.engine.
|
|
100
|
-
import com.funifier.engine.
|
|
230
|
+
// Entidades Funifier
|
|
231
|
+
import com.funifier.engine.action.Action
|
|
232
|
+
import com.funifier.engine.action.ActionLog
|
|
233
|
+
import com.funifier.engine.achievement.Achievement
|
|
234
|
+
import com.funifier.engine.lottery.Lottery
|
|
235
|
+
import com.funifier.engine.lottery.LotteryTicket
|
|
236
|
+
import com.funifier.engine.challenge.Challenge
|
|
237
|
+
import com.funifier.engine.challenge.ChallengeRule
|
|
238
|
+
import com.funifier.engine.challenge.ChallengeRuleFilter
|
|
239
|
+
import com.funifier.engine.challenge.Requirement
|
|
240
|
+
import com.funifier.engine.challenge.RewardPoint
|
|
241
|
+
import com.funifier.engine.constant.Operator
|
|
242
|
+
import com.funifier.engine.constant.TimeScale
|
|
243
|
+
import com.funifier.engine.guid.Guid
|
|
244
|
+
import com.funifier.engine.level.Level
|
|
101
245
|
import com.funifier.engine.notify.*
|
|
102
|
-
import com.funifier.engine.player
|
|
103
|
-
import com.funifier.engine.point
|
|
104
|
-
import com.funifier.engine.team
|
|
105
|
-
import com.funifier.engine.catalog
|
|
106
|
-
import com.funifier.engine.
|
|
107
|
-
|
|
246
|
+
import com.funifier.engine.player.Player
|
|
247
|
+
import com.funifier.engine.point.Point
|
|
248
|
+
import com.funifier.engine.team.Team
|
|
249
|
+
import com.funifier.engine.catalog.Catalog
|
|
250
|
+
import com.funifier.engine.catalog.CatalogItem
|
|
251
|
+
import com.funifier.engine.mail.FunifierMail
|
|
252
|
+
// Utils Funifier
|
|
108
253
|
import com.funifier.engine.util.DateUtil
|
|
109
254
|
import com.funifier.engine.util.JsonUtil
|
|
110
255
|
import com.funifier.engine.util.HttpUtil
|
|
@@ -112,142 +257,327 @@ import com.funifier.engine.util.HttpClientFunifier
|
|
|
112
257
|
import com.funifier.engine.util.MustacheUtils
|
|
113
258
|
import com.funifier.controller.ManagerFactory
|
|
114
259
|
|
|
115
|
-
@TimedInterrupt(value = 5L, unit = TimeUnit.SECONDS)
|
|
116
|
-
class FunifierPublicEndpoint {
|
|
260
|
+
@TimedInterrupt(value = 5L, unit = TimeUnit.SECONDS)class FunifierPublicEndpoint {
|
|
117
261
|
ManagerFactory manager = null;
|
|
118
|
-
void setManager(ManagerFactory c)
|
|
262
|
+
void setManager(ManagerFactory c){ manager = c; };
|
|
119
263
|
|
|
120
|
-
//
|
|
264
|
+
// <<< AQUI ENTRA O CONTEÚDO DE endpoint.script >>>
|
|
121
265
|
}
|
|
122
266
|
```
|
|
123
267
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
Object result = future.get(timeout, TimeUnit.SECONDS);
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**Nota:** O `@TimedInterrupt(value = 5L, unit = TimeUnit.SECONDS)` no wrapper Groovy é uma segunda camada de proteção. O timeout do executor (campo `timeout`) é a camada principal e deve ser >= ao TimedInterrupt para funcionar corretamente.
|
|
171
|
-
|
|
172
|
-
**Estrutura da classe PublicEndpoint (Java):**
|
|
173
|
-
```java
|
|
174
|
-
public class PublicEndpoint {
|
|
175
|
-
public String _id; // slug (URL identifier)
|
|
176
|
-
public String apikey; // gamification apikey
|
|
177
|
-
public String title; // friendly name
|
|
178
|
-
public String description; // internal description
|
|
179
|
-
public boolean active; // enabled/disabled
|
|
180
|
-
public String method; // "POST" or "GET" (default: "POST")
|
|
181
|
-
public String script; // Groovy script body
|
|
182
|
-
public Long timeout; // timeout in SECONDS (null = default 10s)
|
|
183
|
-
public String sample; // example payload
|
|
184
|
-
public Date updated; // last update timestamp
|
|
185
|
-
public Map<String, Object> extra; // extra metadata
|
|
268
|
+
Observações fiéis ao código:
|
|
269
|
+
- O método de entrada **deve se chamar `handle`** e receber 1 argumento: `executor()` chama `obj.invokeMethod("handle", new Object[]{ payload })`.
|
|
270
|
+
- O campo `manager` (`ManagerFactory`) é injetado via `setManager` antes do `handle`. É o acesso a toda a plataforma.
|
|
271
|
+
- A anotação `@TimedInterrupt(...)` é concatenada **sem espaço** antes de `class` (`...SECONDS)class FunifierPublicEndpoint`) — reproduzido aqui exatamente como o código gera.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 4. Endpoints
|
|
276
|
+
|
|
277
|
+
### `GET /v3/public/{id}` — buscar endpoint (CRUD)
|
|
278
|
+
|
|
279
|
+
| Aspecto | Detalhe |
|
|
280
|
+
| ------------ | ------- |
|
|
281
|
+
| Finalidade | Recuperar um `PublicEndpoint` por `_id`. |
|
|
282
|
+
| Autenticação | Header `Authorization` (resolvido por `AuthBean` → Basic `apiKey:secretKey`, Bearer JWT, Studio, Account). |
|
|
283
|
+
| Resposta | `200` + JSON do endpoint, **com campos `null` omitidos** (`JsonUtil.toJsonRemoveNullFields`). |
|
|
284
|
+
|
|
285
|
+
**Comportamento real:** se o `_id` não existir, `findOne(...).as(PublicEndpoint.class)` retorna `null` e a resposta é **`200` com corpo literal `null`** (não há 404 neste endpoint de CRUD).
|
|
286
|
+
|
|
287
|
+
### `POST /v3/public` — criar/atualizar endpoint (upsert)
|
|
288
|
+
|
|
289
|
+
| Aspecto | Detalhe |
|
|
290
|
+
| -------------------- | ------- |
|
|
291
|
+
| Finalidade | Criar ou atualizar um `PublicEndpoint`. |
|
|
292
|
+
| Autenticação | Header `Authorization` (idem acima). |
|
|
293
|
+
| Full replace ou patch| **Full replace.** `col.save(obj)` substitui o documento inteiro pelo objeto recebido. Campos omitidos voltam ao default da classe (ex.: `method` → `"POST"`, `active` → `false`). |
|
|
294
|
+
|
|
295
|
+
**Comportamento real:**
|
|
296
|
+
- `_id` vazio/`null` ⇒ gerado automaticamente (`Guid.shortTimeMillis`).
|
|
297
|
+
- `updated` ⇒ sempre setado para o instante do save (valor do cliente ignorado).
|
|
298
|
+
- Efeito colateral: remove `compiled[_id]` do cache do nó atual (força recompilar no próximo `run`).
|
|
299
|
+
- Resposta `201 Created` + JSON do objeto salvo (campos null omitidos), incluindo `_id` e `updated` resultantes.
|
|
300
|
+
|
|
301
|
+
**Exemplo de request:**
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"_id": "recebe-email",
|
|
305
|
+
"title": "Recebe email",
|
|
306
|
+
"active": true,
|
|
307
|
+
"method": "POST",
|
|
308
|
+
"timeout": 20,
|
|
309
|
+
"script": "def handle(payload){ payload._id = payload.email; manager.getJongoConnection().getCollection('email__c').save(payload); return ['ok': true] }"
|
|
186
310
|
}
|
|
187
311
|
```
|
|
188
312
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
313
|
+
### `DELETE /v3/public/{id}` — remover endpoint (CRUD)
|
|
314
|
+
|
|
315
|
+
| Aspecto | Detalhe |
|
|
316
|
+
| ------------ | ------- |
|
|
317
|
+
| Finalidade | Remover o documento e limpar o cache de compilação. |
|
|
318
|
+
| Autenticação | Header `Authorization`. |
|
|
319
|
+
| Resposta | `204 No Content`. |
|
|
320
|
+
|
|
321
|
+
**Comportamento real:** `remove({_id:#}, id)` é idempotente (sem erro se não existir). Limpa `compiled[id]` **e** `dates[id]` do nó atual.
|
|
322
|
+
|
|
323
|
+
### `GET /v3/pub/{apikey}/{slug}` — execução pública (GET)
|
|
324
|
+
|
|
325
|
+
| Aspecto | Detalhe |
|
|
326
|
+
| ------------ | ------- |
|
|
327
|
+
| Finalidade | Executar o script do endpoint cujo `_id == slug`. |
|
|
328
|
+
| Autenticação | **Nenhuma** (sem token/secret). A `apikey` da URL é a chave pública da gamificação e seleciona o tenant. |
|
|
329
|
+
| Payload | Query params ⇒ `Map`. **Apenas o primeiro valor** de cada parâmetro é mantido. |
|
|
330
|
+
|
|
331
|
+
### `POST /v3/pub/{apikey}/{slug}` — execução pública (POST)
|
|
332
|
+
|
|
333
|
+
| Aspecto | Detalhe |
|
|
334
|
+
| ------------ | ------- |
|
|
335
|
+
| Finalidade | Idem GET, recebendo corpo JSON. |
|
|
336
|
+
| Autenticação | **Nenhuma** (apenas a `apikey` pública da URL). |
|
|
337
|
+
| Payload | Corpo JSON desserializado em `HashMap<String,Object>`. **Deve ser um objeto JSON** (array/escalar falha na desserialização). |
|
|
338
|
+
|
|
339
|
+
**Comportamento real (ambos):**
|
|
340
|
+
|
|
341
|
+
| Condição | Status HTTP | Corpo |
|
|
342
|
+
| ------------------------------------- | ----------- | ----- |
|
|
343
|
+
| `slug` não encontrado | `404` | `PublicResponse(404, "Endpoint '...' não encontrado", null)` |
|
|
344
|
+
| `active == false` | `403` | `PublicResponse(403, "Este endpoint está desativado", null)` |
|
|
345
|
+
| Método HTTP não confere | **`406`** | `PublicResponse(405, "Método HTTP '...' não permitido...", null)` |
|
|
346
|
+
| Sucesso | `200` | **somente** o retorno de `handle()` (serializado em JSON) |
|
|
347
|
+
| Exceção no script (compilar/executar) | `500` | **string** da mensagem de exceção |
|
|
348
|
+
|
|
349
|
+
> **Inconsistência confirmada no código:** na checagem de método, `Response.status(Response.Status.NOT_ACCEPTABLE)` ⇒ **HTTP 406**, mas o corpo `PublicResponse.statusCode = 405`. A intenção do desenvolvedor era `405 Method Not Allowed`; o status HTTP efetivo é `406`.
|
|
194
350
|
|
|
195
|
-
|
|
351
|
+
Todas as respostas de `/v3/pub` incluem o header **`X-Response-Time: <ms>`**.
|
|
196
352
|
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 5. Regras de Negócio
|
|
356
|
+
|
|
357
|
+
Regras presentes no código que **não** estão expressas no schema:
|
|
358
|
+
|
|
359
|
+
1. **Endpoint desativado por padrão.** `active` é `boolean` primitivo (default `false`). Criar um endpoint sem `"active": true` resulta em **403** na execução.
|
|
360
|
+
2. **Método default `POST`.** Omitir `method` no save grava `"POST"` (inicializador do campo). Chamar via GET nesse caso ⇒ **406**.
|
|
361
|
+
3. **Comparação de método case-insensitive** (`equalsIgnoreCase`): `get`/`GET`/`Get` são equivalentes. Se `method` ficar `null` no documento (ex.: gravado fora do `save()` com `method:null`), a checagem `endpoint.method.equalsIgnoreCase(...)` lança **NPE** (não tratada → 500 do container).
|
|
362
|
+
4. **Cache de compilação por nó + coerência via `updated`.** Cada nó do cluster mantém `compiled[_id]` e `dates[_id]` em memória. Recompila quando `clazz == null || dates[_id] == null || dates[_id].before(endpoint.updated)`. Como `save()` atualiza `updated`, qualquer nó que ainda tenha a classe antiga recompila no próximo `run` (o nó que salvou também já limpou o cache local).
|
|
363
|
+
5. **Auto-save em `run()` se `updated == null`.** Endpoints inseridos por fora (ex.: via `/v3/database/public_endpoint` sem passar pelo `save()`) recebem `updated` na primeira execução.
|
|
364
|
+
6. **Dois timeouts independentes:**
|
|
365
|
+
- `@TimedInterrupt(value = 5L, unit = SECONDS)` — transformação AST do Groovy; interrompe loops/métodos longos em **5s** (hardcoded no wrapper).
|
|
366
|
+
- `Future.get(timeout, SECONDS)` no `executor` — **10s** por padrão, ou `endpoint.timeout` se `> 0`. É a parede de wall-clock da chamada a `handle()`.
|
|
367
|
+
- **Inversão:** o `TimedInterrupt` (5s) é menor que o default do executor (10s). Aumentar `timeout` **não** afeta o limite de 5s do `TimedInterrupt` para código que cai sob suas verificações (loops/métodos). Os dois mecanismos coexistem.
|
|
368
|
+
7. **Multi-tenant pela `apikey` da URL.** `FrontController.getInstance(apikey)` isola o `ManagerFactory` (e o MongoDB) daquela gamificação. O `slug` é buscado dentro desse tenant.
|
|
369
|
+
8. **Sem transação / consistência eventual.** O que o script faz no banco é aplicado operação a operação; não há rollback automático em caso de timeout no meio da execução.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 6. Comportamentos Automáticos
|
|
374
|
+
|
|
375
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
376
|
+
| ------------- | ------- | ------- | ------------ |
|
|
377
|
+
| Geração de `_id` | `save()` com `_id` vazio/null | Cria slug via `Guid.shortTimeMillis()` | Sim (documento) |
|
|
378
|
+
| Carimbo de `updated` | Todo `save()` | `updated = new Date()` (sobrescreve cliente) | Sim |
|
|
379
|
+
| Invalidação de cache local | `save()` | `compiled.remove(_id)` (não remove `dates[_id]`) | Não (memória do nó) |
|
|
380
|
+
| Limpeza de cache | `delete()` | `compiled.remove(id)` + `dates.remove(id)` | Não (memória do nó) |
|
|
381
|
+
| Recompilação cross-cluster | `run()` em nó com classe defasada | Recompila se `dates[_id] < updated` | Não (memória do nó) |
|
|
382
|
+
| Auto-save | `run()` com `updated == null` | Persiste `updated` antes de executar | Sim |
|
|
383
|
+
| Criação via instalação de App | `AppManager.funifierCreateCustomEndpoint` | Cria/atualiza endpoint a partir do pacote | Sim |
|
|
384
|
+
|
|
385
|
+
### Criação automática na instalação de um App/pacote
|
|
386
|
+
|
|
387
|
+
`AppManager` (linhas ~1131-1162) lê `funifierCreates.customEndpoints[]` na config do app:
|
|
388
|
+
|
|
389
|
+
```mermaid
|
|
390
|
+
flowchart TD
|
|
391
|
+
A["App install: funifierCreates.customEndpoints[]"] --> B{elemento é JSONObject?}
|
|
392
|
+
B -- sim --> C["funifierCreateCustomEndpoint(doc)"]
|
|
393
|
+
C --> D["PublicManager.save() — upsert em public_endpoint"]
|
|
394
|
+
B -- "não, é String id" --> E{categoria do doc == backend?}
|
|
395
|
+
E -- sim --> F["[skip] sem payload"]
|
|
396
|
+
E -- não --> G["[skip] categoria não-java"]
|
|
197
397
|
```
|
|
198
|
-
POST /v3/public
|
|
199
|
-
Authorization: Basic <base64(apiKey + ":")>
|
|
200
|
-
Content-Type: application/json
|
|
201
398
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
399
|
+
`funifierCreateCustomEndpoint(JSONObject)` simplesmente faz `manager.getPublicManager().save(JsonUtil.fromJson(payload, PublicEndpoint.class))`. O bloco de código comentado logo abaixo (`AppManager.java:447-458`) mostra a **implementação anterior**, que fazia `Unirest.post(FUNIFIER_SERVER + "/v3/public")` — hoje substituída por chamada direta ao manager.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 7. Suportado vs NÃO Suportado
|
|
404
|
+
|
|
405
|
+
### ✅ Suportado
|
|
406
|
+
|
|
407
|
+
- CRUD autenticado de endpoints (`GET/POST/DELETE /v3/public`).
|
|
408
|
+
- Execução pública sem token (`GET/POST /v3/pub/{apikey}/{slug}`).
|
|
409
|
+
- Script Groovy com acesso ao `ManagerFactory` (todos os managers da gamificação).
|
|
410
|
+
- Imports pré-carregados de HTTP (Unirest, Apache HttpClient, groovyx.net.http), JSON (`groovy.json`, `JsonUtil`), e-mail (`simplejavamail`, `FunifierMail`), resize de imagem (Thumbnailator) e entidades Funifier.
|
|
411
|
+
- Timeout configurável por endpoint (`timeout`, em segundos).
|
|
412
|
+
- Cache de classe compilada por nó, com invalidação automática por `updated`.
|
|
413
|
+
- Criação programática e via instalação de App.
|
|
414
|
+
|
|
415
|
+
### ❌ NÃO Suportado / Limitações confirmadas no código
|
|
416
|
+
|
|
417
|
+
- **Sem pipeline de aggregate.** Apesar dos comentários "prepared aggregate", **não há** montagem de aggregate MongoDB — é Groovy livre. Legado da entidade `prepared_aggregate`.
|
|
418
|
+
- **`apikey` (campo da entidade) ignorado na execução** — o tenant vem da URL.
|
|
419
|
+
- **`sample` ignorado em runtime** — apenas documentação.
|
|
420
|
+
- **`extra` não é catch-all** — só captura a chave JSON literal `"extra"`; chaves desconhecidas são descartadas.
|
|
421
|
+
- **GET multivalorado não suportado** — apenas o 1º valor de cada query param entra no payload.
|
|
422
|
+
- **Sem 404 no CRUD** — `GET /v3/public/{id}` inexistente retorna `200` com corpo `null`.
|
|
423
|
+
- **Status HTTP de método incorreto** — retorna `406` onde o corpo declara `405`.
|
|
424
|
+
- **Sem listagem nativa** — `PublicRest` não expõe `GET /v3/public` (lista). Para listar, usa-se `/v3/database/public_endpoint`.
|
|
425
|
+
- **Sem versionamento, sem histórico, sem rate limiting** no módulo.
|
|
426
|
+
- **`message` do `PublicResponse` perdida no sucesso** — o cliente nunca vê `"OK"`.
|
|
427
|
+
|
|
428
|
+
### ⚠️ Bug confirmado — vazamento de threads no `executor()`
|
|
429
|
+
|
|
430
|
+
`PublicManager.executor()` cria `Executors.newSingleThreadExecutor()` a **cada** execução. O `executor.shutdownNow()` só é chamado no bloco `catch` (timeout/erro). **No caminho de sucesso o `shutdown` não é chamado explicitamente.** Como `newSingleThreadExecutor()` usa threads **não-daemon**, o worker permanece vivo até ser reclamado — o `FinalizableDelegatedExecutorService` retornado faz `shutdown()` apenas na finalização pelo GC. Na prática: o encerramento é **deferido e dependente do GC**, e threads/executors **acumulam sob carga** (reclamação lenta e frágil, presa a um hook do JVM). (Trecho relevante: `executor()`, linhas ~252-273.)
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## 8. Segurança e Permissões
|
|
435
|
+
|
|
436
|
+
**Autenticação:**
|
|
437
|
+
- **CRUD (`/v3/public`):** exige header `Authorization`, resolvido por `AuthBean.getApiKey()` (suporta `Basic apiKey:secretKey`, `Bearer <jwt>`, `Studio <token>`, `Account ...`). O `PublicRest` em si **não faz checagem de role/permissão adicional** — o isolamento é dado pela `apikey` resolvida (tenant). Pela convenção Funifier, operações de escrita usam Basic com `apiKey:secretKey`.
|
|
438
|
+
- **Execução (`/v3/pub`):** **sem autenticação**. Conhecer a `apikey` pública + o `slug` é suficiente. Por isso, scripts públicos **não devem** expor dados sensíveis nem executar operações destrutivas sem validação própria dentro do `handle`.
|
|
439
|
+
|
|
440
|
+
**Isolamento por tenant:** `FrontController.getInstance(apikey)` garante que `find`/`run` operem apenas na gamificação da `apikey`.
|
|
441
|
+
|
|
442
|
+
**Sandbox do script** (`compile()` + `TriggerExpressionChecker`):
|
|
443
|
+
|
|
444
|
+
1. `SecureASTCustomizer` com **whitelist de tokens** (operadores permitidos):
|
|
445
|
+
`=`, `+`, `-`, `*`, `/`, `%`, `**`, `++`, `+=`, `--`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `||`, `||=`, `&&`, `&&=`, `[`, `]`.
|
|
446
|
+
Operadores **fora dessa lista são rejeitados na compilação** — por exemplo `instanceof` (`KEYWORD_INSTANCEOF` não está na whitelist) não compila.
|
|
447
|
+
2. `TriggerExpressionChecker` (blacklist):
|
|
448
|
+
- Bloqueia expressões cujo **tipo** seja `System`, `ProcessBuilder`, `File`, `GroovyShell` ou `GroovyObject`.
|
|
449
|
+
- Bloqueia expressões cujo **texto** contenha `.execute`, `.getDB`, `.getMongo` ou `.dropDatabase`.
|
|
450
|
+
|
|
451
|
+
**Superfícies de risco confirmadas no código:**
|
|
452
|
+
- O sandbox é **baseado em blacklist textual/por-tipo** — é contornável (ex.: reflexão, nomes construídos dinamicamente, classes não listadas). Não é uma sandbox forte.
|
|
453
|
+
- O script tem acesso total ao `ManagerFactory` ⇒ pode ler/escrever qualquer coleção do tenant. Combinado com a execução **sem token**, qualquer um que conheça `apikey` + `slug` aciona essa lógica.
|
|
454
|
+
- `endpoint.script` é compilado e executado — quem tem permissão de **escrita** no CRUD tem, na prática, **execução de código** no servidor (RCE dentro dos limites do sandbox).
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 9. Observabilidade e Troubleshooting
|
|
459
|
+
|
|
460
|
+
**Diagnóstico rápido:**
|
|
461
|
+
- O header `X-Response-Time` aparece em toda resposta de `/v3/pub` — útil para medir latência do script.
|
|
462
|
+
- Erros de instalação de App via pacote são impressos em stdout: `[funifier:public] criado: ...` / `Falha public: <msg>` (`AppManager`).
|
|
463
|
+
|
|
464
|
+
**Listar / inspecionar endpoints:**
|
|
465
|
+
```
|
|
466
|
+
GET /v3/database/public_endpoint # lista todos (via database genérico)
|
|
467
|
+
GET /v3/public/{id} # busca um (200 + null se não existir)
|
|
212
468
|
```
|
|
213
469
|
|
|
214
|
-
|
|
470
|
+
**Erros comuns e causas:**
|
|
215
471
|
|
|
216
|
-
|
|
472
|
+
| Sintoma | Causa provável |
|
|
473
|
+
| ------- | -------------- |
|
|
474
|
+
| `403 "Este endpoint está desativado"` | `active != true` no documento. |
|
|
475
|
+
| `406` ao chamar | `method` do endpoint difere do verbo usado (corpo dirá `405`). |
|
|
476
|
+
| `404 "Endpoint '...' não encontrado"` | `slug` ≠ `_id`, ou criado em outro tenant (`apikey` da URL errada). |
|
|
477
|
+
| `500` com string de exceção | Erro de **compilação** (token bloqueado, classe na blacklist) **ou** de **execução** do `handle`. A string é `ex.getMessage()`. |
|
|
478
|
+
| `500` por `TimeoutException` | `handle` excedeu `timeout` (default 10s) — note também o limite de 5s do `@TimedInterrupt`. |
|
|
479
|
+
| Mudança no script "não pega" | Cache de classe; só invalida via `save()` (que atualiza `updated`). Editar o doc direto no Mongo sem mudar `updated` mantém a classe antiga em cache. |
|
|
480
|
+
| `GET /v3/public/{id}` retornando `null` com 200 | Comportamento esperado — não há 404 no CRUD. |
|
|
217
481
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
482
|
+
**O que verificar quando "não funciona":**
|
|
483
|
+
1. `active == true`?
|
|
484
|
+
2. `method` bate com o verbo da chamada?
|
|
485
|
+
3. `script` define `def handle(payload) { ... }`?
|
|
486
|
+
4. A `apikey` da URL é a da gamificação correta?
|
|
487
|
+
5. O erro 500 traz a mensagem de exceção no corpo — leia-a.
|
|
224
488
|
|
|
225
|
-
###
|
|
489
|
+
### Notas operacionais (observadas em runtime — **não** provadas pelo código deste módulo)
|
|
226
490
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
491
|
+
> As notas abaixo vêm de experiência operacional com o motor Groovy/Jackson e **não** são deriváveis dos arquivos do módulo `public`. Trate-as como heurísticas, não como contrato:
|
|
492
|
+
> - `groovy.json.JsonSlurper` pode produzir estruturas (`LazyMap`) que falham na serialização da resposta — preferir `JsonUtil.fromJsonToMap(...)`.
|
|
493
|
+
> - `$` dentro de GStrings é interpretado como variável Groovy; comandos Mongo (`$set`, `$inc`) precisam ser escapados/concatenados.
|
|
494
|
+
> - Caracteres não-ASCII em comentários do script podem causar erro de parse.
|
|
495
|
+
> - Acesso a propriedades de entidades (ex.: `player.extra`) depende da definição da classe correspondente — confirmar no código da entidade.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 10. Exemplos Práticos
|
|
500
|
+
|
|
501
|
+
### 10.1 Mínimo funcional — eco do payload
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
POST /v3/public
|
|
505
|
+
{
|
|
506
|
+
"_id": "echo",
|
|
507
|
+
"active": true,
|
|
508
|
+
"method": "POST",
|
|
509
|
+
"script": "def handle(payload){ return payload }"
|
|
237
510
|
}
|
|
238
511
|
```
|
|
512
|
+
Chamada:
|
|
513
|
+
```
|
|
514
|
+
POST /v3/pub/{apikey}/echo
|
|
515
|
+
{ "nome": "Ana" }
|
|
516
|
+
→ 200 { "nome": "Ana" } # corpo = retorno de handle()
|
|
517
|
+
```
|
|
239
518
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
## Boas Práticas
|
|
519
|
+
### 10.2 Avançado — gravar em coleção + usar managers + timeout
|
|
243
520
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
521
|
+
```json
|
|
522
|
+
POST /v3/public
|
|
523
|
+
{
|
|
524
|
+
"_id": "recebe-lead",
|
|
525
|
+
"title": "Recebe lead de formulário externo",
|
|
526
|
+
"active": true,
|
|
527
|
+
"method": "POST",
|
|
528
|
+
"timeout": 20,
|
|
529
|
+
"sample": "{ \"email\": \"user@x.com\", \"nome\": \"User\" }",
|
|
530
|
+
"script": "def handle(payload){ payload._id = payload.email; manager.getJongoConnection().getCollection('lead__c').save(payload); def resp = [:]; resp.ok = true; resp.id = payload.email; return resp }"
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
Chamada (sem token):
|
|
534
|
+
```
|
|
535
|
+
POST /v3/pub/{apikey}/recebe-lead
|
|
536
|
+
{ "email": "ana@x.com", "nome": "Ana" }
|
|
537
|
+
→ 200 { "ok": true, "id": "ana@x.com" }
|
|
538
|
+
```
|
|
247
539
|
|
|
248
|
-
|
|
540
|
+
### 10.3 Anti-padrão — o que NÃO fazer
|
|
249
541
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
542
|
+
```json
|
|
543
|
+
{
|
|
544
|
+
"_id": "ruim",
|
|
545
|
+
"script": "def handle(payload){ for(i in 0..1000000000){ }; return 'fim' }"
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
Problemas:
|
|
549
|
+
- Sem `"active": true` ⇒ retorna **403** (esquecimento mais comum).
|
|
550
|
+
- Loop longo ⇒ interrompido pelo `@TimedInterrupt` (5s) ⇒ **500**.
|
|
551
|
+
- Usar `import` dentro do script ⇒ erro de compilação (imports já estão no wrapper).
|
|
552
|
+
- Nomear o método diferente de `handle` ⇒ `invokeMethod("handle", ...)` falha ⇒ **500**.
|
|
553
|
+
- Usar `instanceof`, `System.exit`, `.execute()`, `.getDB()` ⇒ bloqueado pelo sandbox ⇒ **500**.
|
|
554
|
+
- Retornar estrutura de `JsonSlurper` que não serializa ⇒ erro na resposta (ver Notas operacionais).
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## Checklist de Configuração
|
|
559
|
+
|
|
560
|
+
- [ ] `_id` (slug) definido — ou deixe vazio para gerar automaticamente.
|
|
561
|
+
- [ ] `"active": true` — **sem isso, todas as chamadas retornam 403** (default é `false`).
|
|
562
|
+
- [ ] `method` bate com o verbo que o cliente vai usar (`"POST"` é o default).
|
|
563
|
+
- [ ] `script` define **`def handle(payload) { ... }`** (o nome `handle` é obrigatório).
|
|
564
|
+
- [ ] **Não** usar `import` no script — todos os imports já estão no wrapper.
|
|
565
|
+
- [ ] `timeout` (segundos) ajustado se o script faz I/O externo (default 10s; lembre do limite de 5s do `@TimedInterrupt`).
|
|
566
|
+
- [ ] Tenant correto: a `apikey` da URL `/v3/pub/{apikey}/{slug}` é a da gamificação onde o endpoint foi salvo.
|
|
567
|
+
- [ ] Não retornar dados sensíveis — a execução é **pública e sem token**.
|
|
568
|
+
- [ ] Validar o `payload` dentro do `handle` (não confie na entrada).
|
|
569
|
+
- [ ] Armadilha silenciosa: omitir `method`/`active` no `POST /v3/public` reseta esses campos (full replace) — `method` volta a `"POST"`, `active` volta a `false`.
|
|
570
|
+
- [ ] Após editar via Mongo direto, garanta que `updated` mude — senão o cache de classe não invalida.
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## REGRAS DE OURO (resumo do comportamento real)
|
|
575
|
+
|
|
576
|
+
1. `/v3/public` = CRUD autenticado; `/v3/pub/{apikey}/{slug}` = execução pública sem token.
|
|
577
|
+
2. Execução é **síncrona**, com dois timeouts (5s TimedInterrupt + 10s/`timeout` executor).
|
|
578
|
+
3. No **sucesso**, o corpo é só o retorno de `handle()`; em **erro de script**, é a string da exceção (500); em **pré-checagem**, é um `PublicResponse` JSON.
|
|
579
|
+
4. Método errado ⇒ HTTP **406** (corpo diz 405). Inativo ⇒ 403. Inexistente ⇒ 404.
|
|
580
|
+
5. Cache de classe por nó, invalidado por `updated`.
|
|
581
|
+
6. Sandbox é blacklist (contornável); script tem acesso total ao tenant via `manager`.
|
|
582
|
+
7. **Bug conhecido:** vazamento de thread no caminho de sucesso do `executor()`.
|
|
583
|
+
8. Não existe aggregate pipeline — "prepared aggregate" é só legado de nomenclatura.
|