Grupos de relatórios#
Grupos organizam relatórios sem alterar o modelo de permissão. Eles funcionam como coleções nomeadas para filtros, menus e catálogos: o usuário ainda precisa ter permissão para listar, editar ou gerar os relatórios que aparecem no grupo.
O recurso está disponível para os backends memory, sqlite e postgres.
Modelo#
Um grupo tem:
| Campo | Tipo | Descrição |
|---|---|---|
id | string | Identificador do grupo |
name | string | Nome exibido para usuários |
namespace |
tenant ou global |
Escopo do grupo |
description | string | Descrição opcional |
colorHex |
string | Cor opcional do grupo em hexadecimal RGB (RRGGBB) |
createdAt | ISO-8601 | Data de criação |
updatedAt | ISO-8601 | Última atualização |
namespace: tenant cria um grupo privado do tenant da request.
namespace: global cria um grupo visível para todos os tenants e exige
can_edit_global para criar, editar ou excluir.
Escopo das associações#
A associação entre grupo e relatório também tem escopo:
| Associação | Quem cria | Visibilidade |
|---|---|---|
| Global | Operador com can_edit_global em grupo global |
Todos os tenants |
| Tenant | Usuário do tenant com can_edit |
Apenas o tenant da request |
Isso permite um catálogo global com duas camadas: o operador publica grupos e relatórios globais para todos, enquanto cada tenant pode acrescentar seus próprios relatórios privados a um grupo global sem expor esses relatórios para outros tenants.
Listar grupos — GET /api/v1/groups#
Permissão: canList
Retorna grupos do tenant atual mais grupos globais. Sem paginação, a resposta é
um array. Com page ou limit, a resposta usa o envelope paginado.
| Param | Tipo | Descrição |
|---|---|---|
page | int | Página, começando em 1 |
limit | int | Itens por página, até 200 |
curl http://localhost:8080/api/v1/groups \
-H "Authorization: Bearer $TOKEN"
[
{
"id": "grp-finance",
"namespace": "tenant",
"name": "Financeiro",
"description": "Relatórios financeiros",
"colorHex": "60A5FA",
"createdAt": "2026-04-28T12:00:00.000Z",
"updatedAt": "2026-04-28T12:00:00.000Z"
}
]
Criar grupo — POST /api/v1/groups#
Permissão: canInsert para grupos de tenant; can_edit_global
para grupos
globais.
{
"name": "Financeiro",
"namespace": "tenant",
"description": "Relatórios financeiros",
"colorHex": "#60A5FA"
}
| Campo | Obrigatório | Descrição |
|---|---|---|
name | Sim | Nome do grupo |
namespace |
Não | tenant por padrão; use global para catálogo compartilhado |
description | Não | Texto livre |
colorHex |
Não |
Cor livre do usuário. Aceita
#RGB
,
RGB
,
#RRGGBB
,
RRGGBB
,
0xAARRGGBB
ou
AARRGGBB
; persiste como
RRGGBB
sem alpha
|
| Status | Erro | Quando |
|---|---|---|
201 | Grupo criado | |
400 |
validation_error |
name ausente ou namespace inválido |
403 | forbidden | Permissão insuficiente |
Detalhes — GET /api/v1/groups/:id#
Permissão: canList
Retorna o grupo e os relatórios visíveis para o tenant atual dentro dele.
{
"id": "grp-finance",
"namespace": "tenant",
"name": "Financeiro",
"description": "Relatórios financeiros",
"colorHex": "60A5FA",
"createdAt": "2026-04-28T12:00:00.000Z",
"updatedAt": "2026-04-28T12:00:00.000Z",
"reports": [
{
"id": "rpt-cashflow",
"name": "Fluxo de caixa",
"namespace": "tenant",
"createdAt": "2026-04-28T12:00:00.000Z",
"updatedAt": "2026-04-28T12:00:00.000Z",
"storageKind": "inline"
}
]
}
| Status | Erro | Quando |
|---|---|---|
404 |
not_found |
Grupo inexistente ou não visível para o tenant |
Atualizar — PUT /api/v1/groups/:id#
Permissão: canEdit para grupos de tenant; can_edit_global para grupos
globais.
{
"name": "Financeiro e Fiscal",
"description": "Relatórios financeiros e fiscais",
"colorHex": "2DD4BF"
}
name é obrigatório no update. O namespace do grupo não é alterado por este
endpoint.
Envie colorHex: null ou omita o campo para remover a cor customizada. A UI do
manager usa uma cor automática baseada no tema quando colorHex está ausente.
Excluir — DELETE /api/v1/groups/:id#
Permissão: canDelete para grupos de tenant; can_edit_global
para grupos
globais.
Excluir um grupo remove suas associações, mas não exclui os relatórios.
curl -X DELETE http://localhost:8080/api/v1/groups/$GROUP_ID \
-H "Authorization: Bearer $TOKEN"
Adicionar relatório — POST /api/v1/groups/:id/reports#
Permissão: canEdit no escopo do tenant, ou can_edit_global
para criar
associação global em grupo global.
{
"reportId": "rpt-cashflow"
}
Regras principais:
- o relatório precisa existir e ser visível para o chamador;
-
operador com
can_edit_globalsó cria associação global com relatórionamespace: global; - usuário de tenant pode associar relatórios visíveis ao tenant, inclusive em grupos globais, mas essa associação fica privada para o tenant;
- repetir a mesma associação é idempotente.
| Status | Erro | Quando |
|---|---|---|
204 | Associação criada ou já existente | |
400 |
validation_error |
reportId ausente |
404 | not_found | Grupo ou relatório não encontrado |
422 |
namespace_mismatch |
Operador tentou criar associação global com relatório não global |
Remover relatório — DELETE /api/v1/groups/:id/reports/:reportId#
Permissão: canEdit no escopo do tenant, ou can_edit_global
para remover
associação global em grupo global.
curl -X DELETE http://localhost:8080/api/v1/groups/$GROUP_ID/reports/$REPORT_ID \
-H "Authorization: Bearer $TOKEN"
Remover uma associação inexistente retorna 204.
Filtrar relatórios por grupo#
GET /api/v1/reports?group=<groupId> retorna os relatórios visíveis para o
tenant atual dentro do grupo informado.
curl "http://localhost:8080/api/v1/reports?group=$GROUP_ID" \
-H "Authorization: Bearer $TOKEN"
Quando group é usado, ele define o conjunto de resultados. Use GET /groups
ou GET /groups/:id para descobrir os grupos disponíveis antes de filtrar.
Permissões por grupo — C3#
Além das permissões do token e da autorização por tenant, o Sulfite pode aplicar
uma terceira camada de restrição por grupo. Essa camada é lida da view
sulfite_group_permissions no banco do tenant.
A view deve expor estas colunas:
| Coluna | Tipo esperado | Descrição |
|---|---|---|
email | text | Email do usuário autenticado |
group_id |
text |
Id do grupo em report_groups.id |
can_list |
boolean |
Permite ver o grupo e usar GET /reports?group= |
can_insert |
boolean |
Teto para criação no escopo do grupo |
can_edit |
boolean |
Permite editar grupo tenant e alterar associações |
can_delete | boolean | Permite excluir grupo tenant |
Exemplo:
CREATE VIEW public.sulfite_group_permissions AS
SELECT
u.email,
gp.group_id,
gp.can_list,
gp.can_insert,
gp.can_edit,
gp.can_delete
FROM app_users u
JOIN user_group_permissions gp ON gp.role_id = u.role_id
WHERE u.active = true;
Comportamento operacional:
| Situação | Resultado |
|---|---|
| View não existe | Fallback para permissões anteriores |
View existe, mas não há linha para (email, group_id) |
Fallback para permissões anteriores |
| View existe e retorna linha | Interseção com as permissões anteriores |
Token não tem email e a view existe | Permissões de grupo negadas |
| Erro de conexão ou query | Request falha fechado com 503 |
can_edit_global não pertence à view. Ele vem apenas do token e continua sendo
a autorização para criar, editar e excluir recursos globais.
Persistência e migrações#
SQLite cria as tabelas automaticamente e usa chaves estrangeiras com cascade para remover associações quando um grupo ou relatório é excluído.
PostgreSQL em modo single-node também cria o schema automaticamente. Em cluster, o servidor não executa DDL no boot; aplique a migration antes de subir as réplicas:
psql "$DATABASE_URL" \
-f packages/sulfite_server/migrations/005_report_groups.sql
Se SULFITE_CLUSTER_MODE=true e as tabelas report_groups,
report_group_memberships ou a coluna report_groups.color_hex
não existirem,
o servidor falha no boot com uma mensagem pedindo a migration.
E2E com Postgres real#
A suíte inclui um teste opt-in para validar RFC-043 contra um Postgres real. Ele
cria um schema temporário, sobe o server em processo com storage Postgres, cria a
view sulfite_group_permissions, executa requests HTTP reais e remove o schema
ao final.
cd packages/sulfite_server
RFC043_E2E_ENABLED=1 \
SULFITE_RFC043_E2E_DATABASE_URL="$DATABASE_URL" \
dart test test/rfc043_postgres_e2e_test.dart
Use uma URL de banco com permissão para CREATE SCHEMA, CREATE VIEW,
CREATE TABLE e DROP SCHEMA. O teste isola tudo em um schema
sulfite_rfc043_e2e_* para não tocar nos dados de demonstração.
Exemplo: grupo global com relatório global#
# Cria um grupo global.
curl -X POST http://localhost:8080/api/v1/groups \
-H "Authorization: Bearer $OPERATOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Faturamento", "namespace": "global"}'
# Adiciona um relatório global ao grupo.
curl -X POST http://localhost:8080/api/v1/groups/$GROUP_ID/reports \
-H "Authorization: Bearer $OPERATOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reportId": "rpt-invoice"}'
# Um tenant lista os relatórios visíveis dentro do grupo.
curl "http://localhost:8080/api/v1/reports?group=$GROUP_ID" \
-H "Authorization: Bearer $TENANT_TOKEN"