README
¶
Go-Data — OData v4 para APIs RESTful em Go (Golang)
Go-Data é uma biblioteca leve e extensível para criação de APIs RESTful baseadas no padrão OData v4 usando Go (Golang).
Ela oferece suporte completo ao formato JSON, inclui um servidor embutido com Fiber v3, e funciona com múltiplos bancos de dados (PostgreSQL, MySQL, Oracle).
📋 Índice
- Características
- Instalação
- Configuração com .env
- Exemplo de Uso
- Configuração do Servidor
- Autenticação JWT
- Autenticação Basic
- Segurança
- Performance
- Rate Limiting
- Multi-Tenant
- Eventos de Entidade
- ObjectManager (ORM)
- Service Operations
- Rotas Customizadas
- Configuração Programática
- Mapeamento de Entidades
- Bancos de Dados Suportados
- Endpoints OData
- Consultas OData
- Operadores Suportados
- Mapeamento de Tipos
- Contribuindo
- Exemplos
- Referências
- Licença
- Suporte
✨ Características
🌐 Protocolo OData v4
- Suporte ao protocolo OData v4 com resposta JSON
- Geração automática de metadados JSON
- Service Document automático
- Operações CRUD completas
🚀 Servidor Fiber v3
- Servidor HTTP embutido baseado no Fiber v3
- Suporte a HTTPS/TLS
- Configuração de CORS
- Middleware de logging e recovery
- Shutdown graceful
💾 Múltiplos Bancos de Dados
- PostgreSQL
- Oracle
- MySQL
- Pool de conexões automático
🔧 Mapeamento Automático
- Sistema de tags para mapeamento de structs
- Relacionamentos bidirecionais
- Operações em cascata
- Tipos nullable personalizados
🔍 Consultas OData
- Filtros ($filter)
- Ordenação ($orderby)
- Paginação ($top, $skip)
- Seleção de campos ($select)
- Expansão de relacionamentos ($expand) com otimização N+1
- Contagem ($count)
- Campos computados ($compute)
- Busca textual ($search)
- Batch requests ($batch): Múltiplas operações em uma requisição com suporte a transações
🔐 Autenticação
- JWT: Tokens de acesso e refresh, roles, scopes e configuração flexível
- Basic Auth: HTTP Basic Authentication com validação customizável
- Interface
AuthProviderpermite implementar qualquer estratégia de autenticação - Middleware de autenticação obrigatória e opcional
- Controle de acesso baseado em roles e scopes
- Privilégios de administrador
- Configuração de autenticação por entidade
- Entidades somente leitura
⚡ Performance
- Otimização N+1 para $expand: Usa batching automático para evitar múltiplas queries
- String Builder: Concatenação otimizada em query building
- Benchmarks completos: Suite de testes de performance com profiling
🗄️ ObjectManager (ORM)
- Sistema ORM completo similar ao TObjectManager do Aurelius
- Identity Mapping e cache automático de entidades
- Change Tracking para detectar modificações
- Cached Updates com operações em lote
- Gerenciamento de transações integrado
- Métodos: Find, Save, Update, Remove, Merge, Flush
- Integração transparente com eventos
🛡️ Rate Limiting
- Controle de taxa de requisições por IP, usuário ou API key
- Configuração flexível de limites e janelas de tempo
- Headers informativos de rate limit nas respostas
- Estratégias customizáveis de geração de chaves
- Suporte a burst de requisições simultâneas
- Limpeza automática de clientes inativos
- Integração transparente com middleware do servidor
🏢 Multi-Tenant
- Suporte completo a multi-tenant com isolamento de dados
- Identificação automática via headers, subdomains, path ou JWT
- Pool de conexões gerenciado automaticamente para cada tenant
- Configuração via .env com múltiplos bancos de dados
- Endpoints específicos para gerenciamento de tenants
- Escalabilidade com adição dinâmica de novos tenants
⚙️ Configuração Automática
- Carregamento automático de configurações via arquivo
.env - Busca automática do arquivo
.envna árvore de diretórios - Valores padrão sensatos quando
.envnão encontrado - Configuração completa de banco de dados, servidor, TLS e JWT
🔧 Execução como Serviço (Kardianos)
- Integração transparente usando biblioteca kardianos/service
- Suporte completo a Windows Service, systemd (Linux) e launchd (macOS)
- Métodos unificados:
Install(),Start(),Stop(),Restart(),Status(),Uninstall() - Detecção automática de contexto de execução (serviço vs. modo normal)
- Shutdown graceful e auto-restart em caso de falha
- Logging integrado com Event Log/journalctl/Console nativo
- Configuração automática por plataforma com dependências específicas
🚀 Instalação
go get github.com/fitlcarlos/go-data
🛠️ Configuração com .env
O Go-Data suporta configuração automática através de arquivos .env, similar ao Spring Boot. O sistema busca automaticamente por arquivos .env no diretório atual e diretórios pai.
Exemplo de arquivo .env
# Configurações do Banco de Dados
DB_TYPE=postgresql
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=testdb
DB_SCHEMA=public
DB_CONNECTION_STRING=
DB_MAX_OPEN_CONNS=25
DB_MAX_IDLE_CONNS=5
DB_CONN_MAX_LIFETIME=600s
# Configurações do Servidor OData
SERVER_HOST=localhost
SERVER_PORT=8080
SERVER_ROUTE_PREFIX=/odata
SERVER_ENABLE_CORS=true
SERVER_ALLOWED_ORIGINS=*
SERVER_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
SERVER_ALLOWED_HEADERS=*
SERVER_EXPOSED_HEADERS=OData-Version,Content-Type
SERVER_ALLOW_CREDENTIALS=false
SERVER_ENABLE_LOGGING=true
SERVER_LOG_LEVEL=INFO
SERVER_LOG_FILE=
SERVER_ENABLE_COMPRESSION=false
SERVER_MAX_REQUEST_SIZE=10485760
SERVER_SHUTDOWN_TIMEOUT=30s
# Configurações de SSL/TLS
SERVER_TLS_CERT_FILE=
SERVER_TLS_KEY_FILE=
# Configurações de JWT
JWT_ENABLED=false
JWT_SECRET_KEY=
JWT_ISSUER=go-data-server
JWT_EXPIRES_IN=1h
JWT_REFRESH_IN=24h
JWT_ALGORITHM=HS256
JWT_REQUIRE_AUTH=false
# Configurações de Rate Limit
RATE_LIMIT_ENABLED=true
RATE_LIMIT_REQUESTS_PER_MINUTE=100
RATE_LIMIT_BURST_SIZE=20
RATE_LIMIT_WINDOW_SIZE=1m
RATE_LIMIT_HEADERS=true
# Configurações do Serviço
SERVICE_NAME=godata-service
SERVICE_DISPLAY_NAME=GoData OData Service
SERVICE_DESCRIPTION=Serviço GoData OData v4 para APIs RESTful
# Configurações Multi-Tenant
MULTI_TENANT_ENABLED=false
TENANT_IDENTIFICATION_MODE=header
TENANT_HEADER_NAME=X-Tenant-ID
DEFAULT_TENANT=default
# Configurações específicas por tenant (exemplo)
TENANT_EMPRESA_A_DB_DRIVER=postgresql
TENANT_EMPRESA_A_DB_HOST=localhost
TENANT_EMPRESA_A_DB_PORT=5432
TENANT_EMPRESA_A_DB_NAME=empresa_a
TENANT_EMPRESA_A_DB_USER=user_a
TENANT_EMPRESA_A_DB_PASSWORD=password_a
Descrição das Variáveis
Configurações do Banco de Dados
- DB_TYPE: Tipo do banco de dados (postgresql, mysql, oracle)
- DB_HOST: Endereço do servidor de banco de dados
- DB_PORT: Porta do servidor de banco de dados
- DB_NAME: Nome do banco de dados
- DB_USER: Usuário do banco de dados
- DB_PASSWORD: Senha do banco de dados
- DB_SCHEMA: Schema do banco de dados (opcional)
- DB_CONNECTION_STRING: String de conexão customizada (opcional)
- DB_MAX_OPEN_CONNS: Máximo de conexões abertas (padrão: 25)
- DB_MAX_IDLE_CONNS: Máximo de conexões inativas (padrão: 5)
- DB_CONN_MAX_LIFETIME: Tempo de vida das conexões (padrão: 10m)
Configurações do Servidor
- SERVER_HOST: Endereço do servidor OData (padrão: localhost)
- SERVER_PORT: Porta do servidor OData (padrão: 9090)
- SERVER_ROUTE_PREFIX: Prefixo das rotas OData (padrão: /odata)
- SERVER_ENABLE_CORS: Habilita CORS (padrão: true)
- SERVER_ALLOWED_ORIGINS: Origins permitidas para CORS (padrão: *)
- SERVER_ALLOWED_METHODS: Métodos HTTP permitidos
- SERVER_ALLOWED_HEADERS: Headers permitidos
- SERVER_EXPOSED_HEADERS: Headers expostos
- SERVER_ALLOW_CREDENTIALS: Permite credenciais CORS (padrão: false)
- SERVER_ENABLE_LOGGING: Habilita logging (padrão: true)
- SERVER_LOG_LEVEL: Nível de logging (padrão: INFO)
- SERVER_LOG_FILE: Arquivo de log (opcional)
- SERVER_ENABLE_COMPRESSION: Habilita compressão (padrão: false)
- SERVER_MAX_REQUEST_SIZE: Tamanho máximo da requisição (padrão: 10MB)
- SERVER_SHUTDOWN_TIMEOUT: Timeout para shutdown graceful (padrão: 30s)
Configurações TLS
- SERVER_TLS_CERT_FILE: Caminho para o arquivo de certificado TLS
- SERVER_TLS_KEY_FILE: Caminho para o arquivo de chave TLS
Configurações JWT
- JWT_ENABLED: Habilita autenticação JWT (padrão: false)
- JWT_SECRET_KEY: Chave secreta para assinatura JWT
- JWT_ISSUER: Emissor do token JWT (padrão: go-data-server)
- JWT_EXPIRES_IN: Tempo de expiração do token de acesso (padrão: 1h)
- JWT_REFRESH_IN: Tempo de expiração do token de refresh (padrão: 24h)
- JWT_ALGORITHM: Algoritmo de assinatura JWT (padrão: HS256)
- JWT_REQUIRE_AUTH: Requer autenticação para todas as rotas (padrão: false)
Configurações do Serviço
- SERVICE_NAME: Nome do serviço (padrão: godata-service)
- SERVICE_DISPLAY_NAME: Nome de exibição do serviço (padrão: GoData OData Service)
- SERVICE_DESCRIPTION: Descrição do serviço (padrão: Serviço GoData OData v4 para APIs RESTful)
Configurações Multi-Tenant
- MULTI_TENANT_ENABLED: Habilita suporte multi-tenant (padrão: false)
- TENANT_IDENTIFICATION_MODE: Método de identificação do tenant (header, subdomain, path, jwt)
- TENANT_HEADER_NAME: Nome do header para identificação (padrão: X-Tenant-ID)
- DEFAULT_TENANT: Nome do tenant padrão (padrão: default)
- TENANT_[NOME]_DB_DRIVER: Tipo de banco para tenant específico
- TENANT_[NOME]_DB_HOST: Host do banco para tenant específico
- TENANT_[NOME]_DB_PORT: Porta do banco para tenant específico
- TENANT_[NOME]_DB_NAME: Nome do banco para tenant específico
- TENANT_[NOME]_DB_USER: Usuário do banco para tenant específico
- TENANT_[NOME]_DB_PASSWORD: Senha do banco para tenant específico
Uso Transparente
O método NewServer() é transparente e carrega automaticamente as configurações do arquivo .env quando disponível:
package main
import (
"log"
"github.com/fitlcarlos/go-data/odata"
)
func main() {
// Cria servidor automaticamente:
// - Se .env existe: carrega configurações completas (servidor + banco)
// - Se .env não existe: retorna servidor básico para configuração manual
server := odata.NewServer()
// Registrar entidades
server.RegisterEntity("Users", User{})
// Iniciar servidor
log.Fatal(server.Start())
}
Como Funciona
- Busca Automática: O
NewServer()busca automaticamente por arquivos.envno diretório atual e diretórios pai (até a raiz do sistema) - Configuração Automática: Se encontrar
.envcomDB_TYPEválido, configura automaticamente o provider de banco e servidor - Fallback Gracioso: Se não encontrar
.envouDB_TYPEinválido, retorna servidor básico para configuração manual - Zero Configuração: Não precisa chamar métodos específicos - tudo é automático
Exemplo com Arquivo .env
- Crie um arquivo
.envna raiz do projeto:
# Configuração PostgreSQL
DB_TYPE=postgresql
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=mypassword
DB_NAME=mydatabase
# Configuração do servidor
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
SERVER_ROUTE_PREFIX=/api/v1
# JWT (opcional)
JWT_ENABLED=true
JWT_SECRET_KEY=minha-chave-secreta-super-segura
JWT_ISSUER=minha-aplicacao
# Multi-Tenant (opcional)
MULTI_TENANT_ENABLED=true
TENANT_IDENTIFICATION_MODE=header
TENANT_HEADER_NAME=X-Tenant-ID
DEFAULT_TENANT=default
# Configurações por tenant
TENANT_EMPRESA_A_DB_DRIVER=postgresql
TENANT_EMPRESA_A_DB_HOST=postgres-a.empresa.com
TENANT_EMPRESA_A_DB_PORT=5432
TENANT_EMPRESA_A_DB_NAME=empresa_a
TENANT_EMPRESA_A_DB_USER=user_a
TENANT_EMPRESA_A_DB_PASSWORD=password_a
TENANT_EMPRESA_B_DB_DRIVER=mysql
TENANT_EMPRESA_B_DB_HOST=mysql-b.empresa.com
TENANT_EMPRESA_B_DB_PORT=3306
TENANT_EMPRESA_B_DB_NAME=empresa_b
TENANT_EMPRESA_B_DB_USER=user_b
TENANT_EMPRESA_B_DB_PASSWORD=password_b
- Use o servidor transparente:
func main() {
// Carrega automaticamente todas as configurações do .env
server := odata.NewServer()
// Registra entidades
server.RegisterEntity("Users", User{})
server.RegisterEntity("Products", Product{})
// Inicia - todas as configurações já estão aplicadas
log.Fatal(server.Start())
}
Configuração Manual (Fallback)
Se não usar .env ou precisar de configurações específicas, ainda pode configurar manualmente:
// Configuração manual tradicional
provider := providers.NewPostgreSQLProvider(db)
server := odata.NewServerWithProvider(provider, "localhost", 8080, "/api")
// Ou configuração completa
config := odata.DefaultServerConfig()
config.Host = "localhost"
config.Port = 8080
server := odata.NewServerWithConfig(provider, config)
📝 Exemplo de Uso
Servidor Automático com .env
package main
import (
"log"
"github.com/fitlcarlos/go-data/odata"
)
// Entidade de exemplo
type User struct {
ID int `json:"id" odata:"key"`
Name string `json:"name" odata:"required"`
Email string `json:"email" odata:"required"`
}
func main() {
// Servidor automático (carrega .env se disponível)
server := odata.NewServer()
// Registrar entidades
server.RegisterEntity("Users", User{})
// Iniciar servidor
log.Fatal(server.Start())
}
Servidor Básico
package main
import (
"database/sql"
"log"
"github.com/fitlcarlos/go-data/odata"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// Conecta ao banco
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/database")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Cria provider
provider := providers.NewMySQLProvider(db)
// Cria servidor com configurações específicas
server := odata.NewServerWithProvider(provider, "localhost", 8080, "/odata")
// Registra entidades
server.RegisterEntity("Users", User{})
// Inicia servidor
log.Fatal(server.Start())
}
Definindo Entidades
type User struct {
TableName string `table:"users"`
ID int64 `json:"id" primaryKey:"idGenerator:sequence"`
Nome string `json:"nome" prop:"[required]; length:100"`
Email string `json:"email" prop:"[required, Unique]; length:255"`
Idade nullable.Int64 `json:"idade"`
Ativo bool `json:"ativo" prop:"[required]; default"`
DtInc time.Time `json:"dt_inc" prop:"[required, NoUpdate]; default"`
// Relacionamentos
Orders []Order `json:"Orders" manyAssociation:"foreignKey:user_id; references:id"`
}
type Order struct {
TableName string `table:"orders"`
ID int64 `json:"id" primaryKey:"idGenerator:sequence"`
UserID int64 `json:"user_id" prop:"[required]"`
Total float64 `json:"total" prop:"[required]; precision:10; scale:2"`
DtPedido time.Time `json:"dt_pedido" prop:"[required]"`
// Relacionamento N:1
User *User `json:"User" association:"foreignKey:user_id; references:id"`
}
⚙️ Configuração do Servidor
Configuração Personalizada
config := &odata.ServerConfig{
Host: "0.0.0.0",
Port: 8080,
// CORS
EnableCORS: true,
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
// Logging
EnableLogging: true,
LogLevel: "INFO",
// Limites
MaxRequestSize: 5 * 1024 * 1024, // 5MB
// Prefixo das rotas
RoutePrefix: "/api/odata",
// Timeout
ShutdownTimeout: 30 * time.Second,
}
server := odata.NewServerWithConfig(provider, config)
HTTPS/TLS
config := odata.DefaultServerConfig()
config.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
}
config.CertFile = "server.crt"
config.CertKeyFile = "server.key"
🔧 Configuração Programática
O Go-Data oferece uma API fluente para configurar o servidor programaticamente após sua criação, permitindo sobrescrever configurações do .env ou aplicar configurações dinâmicas.
Métodos Setter Fluentes
Todos os métodos setter retornam *Server, permitindo encadeamento (method chaining):
Configurações Básicas
server := odata.NewServer()
server.SetPort(9000).
SetHost("0.0.0.0").
SetRoutePrefix("/api/v2")
CORS
server.SetCORS(true).
SetAllowedOrigins([]string{"https://example.com", "https://app.example.com"}).
SetAllowedMethods([]string{"GET", "POST", "PUT", "DELETE"}).
SetAllowedHeaders([]string{"Content-Type", "Authorization", "X-Custom-Header"})
Logging
server.SetEnableLogging(true).
SetLogLevel("DEBUG")
Limites e Timeouts
import "time"
server.SetMaxRequestSize(20 * 1024 * 1024). // 20MB
SetShutdownTimeout(60 * time.Second)
TLS/HTTPS
server.SetTLS("certs/server.crt", "certs/server.key")
Rate Limiting
// Habilita rate limiting com 200 req/min e burst de 50
server.SetRateLimit(200, 50)
// Desabilita rate limiting
server.DisableRateLimit()
Security Headers
// Habilita security headers com configuração padrão
server.SetSecurityHeaders(odata.DefaultSecurityHeadersConfig())
// Configuração estrita (produção)
server.SetSecurityHeaders(odata.StrictSecurityHeadersConfig())
// Configuração relaxada (desenvolvimento)
server.SetSecurityHeaders(odata.RelaxedSecurityHeadersConfig())
// Desabilita security headers
server.SetSecurityHeaders(odata.DisableSecurityHeaders())
Audit Logging
auditConfig := &odata.AuditLogConfig{
Enabled: true,
LogType: "file",
FilePath: "/var/log/godata-audit.log",
Format: "json",
}
server.SetAuditLog(auditConfig)
Sobrescrevendo Configurações do .env
Um caso de uso comum é carregar configurações básicas do .env e sobrescrever dinamicamente:
// 1. Carrega configurações do .env automaticamente
server := odata.NewServer()
// 2. Sobrescreve configurações via código (prioridade sobre .env)
server.SetPort(9000). // Override SERVER_PORT
SetHost("0.0.0.0"). // Override SERVER_HOST
SetRoutePrefix("/api/v2"). // Override SERVER_ROUTE_PREFIX
SetRateLimit(500, 100) // Override rate limit
// 3. Registra entidades
server.RegisterEntity("Users", User{})
// 4. Inicia - usa configurações mescladas (env + código)
server.Start()
Configuração Condicional
Você pode aplicar configurações diferentes baseado em ambiente:
server := odata.NewServer()
// Configuração baseada em ambiente
env := os.Getenv("APP_ENV")
if env == "production" {
server.SetHost("0.0.0.0").
SetPort(443).
SetTLS("/etc/ssl/cert.pem", "/etc/ssl/key.pem").
SetSecurityHeaders(odata.StrictSecurityHeadersConfig()).
SetRateLimit(100, 20).
SetLogLevel("WARN")
} else if env == "development" {
server.SetHost("localhost").
SetPort(3000).
SetSecurityHeaders(odata.RelaxedSecurityHeadersConfig()).
DisableRateLimit().
SetLogLevel("DEBUG")
}
server.Start()
Acesso às Configurações
Você também pode ler as configurações atuais:
// Obtém a configuração completa
config := server.GetConfig()
// Acessa valores específicos
port := config.Port
host := config.Host
prefix := config.RoutePrefix
// Modifica e aplica
config.Port = 9000
// As mudanças são aplicadas imediatamente
Exemplo Completo: Configuração Avançada
package main
import (
"log"
"os"
"time"
"github.com/fitlcarlos/go-data/odata"
)
func main() {
// 1. Carrega .env automaticamente
server := odata.NewServer()
// 2. Aplica configurações programáticas
server.
// Servidor
SetPort(8080).
SetHost("0.0.0.0").
SetRoutePrefix("/api/v1").
// CORS
SetCORS(true).
SetAllowedOrigins([]string{
"https://app.example.com",
"https://admin.example.com",
}).
// Segurança
SetSecurityHeaders(odata.StrictSecurityHeadersConfig()).
SetRateLimit(200, 50).
// Performance
SetMaxRequestSize(10 * 1024 * 1024).
SetShutdownTimeout(30 * time.Second).
// Logging
SetEnableLogging(true).
SetLogLevel("INFO")
// 3. Configuração condicional para TLS
if os.Getenv("ENABLE_TLS") == "true" {
server.SetTLS(
os.Getenv("TLS_CERT_FILE"),
os.Getenv("TLS_KEY_FILE"),
)
}
// 4. Audit logging para produção
if os.Getenv("APP_ENV") == "production" {
server.SetAuditLog(&odata.AuditLogConfig{
Enabled: true,
LogType: "file",
FilePath: "/var/log/api-audit.log",
Format: "json",
})
}
// 5. Registra entidades
server.RegisterEntity("Users", User{})
server.RegisterEntity("Products", Product{})
// 6. Inicia servidor
log.Fatal(server.Start())
}
Prioridade de Configuração
A ordem de prioridade para configurações é:
- Valores padrão (DefaultServerConfig)
- Arquivo .env (se encontrado e válido)
- Setters programáticos (maior prioridade)
Exemplo:
# .env
SERVER_PORT=8080
RATE_LIMIT_ENABLED=true
server := odata.NewServer() // Carrega PORT=8080 do .env
server.SetPort(9000) // Override: agora usa PORT=9000
Vantagens da Configuração Programática
✅ Flexibilidade: Ajuste configurações em tempo de execução
✅ Ambiente-específico: Diferentes configs para dev/prod
✅ Type-safe: Erros em tempo de compilação
✅ Encadeamento: API fluente e legível
✅ Override de .env: Mantém defaults mas permite exceções
Veja o exemplo completo em examples/config_override/ que demonstra todas as técnicas de configuração.
🔐 Autenticação JWT
O Go-Data oferece suporte à autenticação JWT através de um modelo desacoplado e flexível. O JWT não está embutido no servidor - você define sua própria lógica de autenticação e configura por entidade usando o padrão Functional Options.
Características
- ✅ Desacoplado: JWT como plugin opcional, não embutido
- ✅ Flexível: Controle total sobre geração e validação de tokens
- ✅ Customizável: Claims, algoritmos e lógica completamente personalizáveis
- ✅ Por Entidade: Configure autenticação diferente para cada entidade
- ✅ Múltiplos JWTs: Use diferentes JWTs no mesmo servidor
Interface AuthProvider
O Go-Data define uma interface AuthProvider que permite implementar qualquer estratégia de autenticação:
type AuthProvider interface {
ValidateToken(token string) (*UserIdentity, error)
GenerateToken(user *UserIdentity) (string, error)
ExtractToken(c fiber.Ctx) string
}
Uso Básico com JwtAuth
A implementação padrão JwtAuth oferece autenticação JWT completa com configuração automática via .env:
Opção 1: Configuração via .env (Recomendado)
# .env
JWT_SECRET=your-super-secret-key-with-at-least-32-characters
JWT_ISSUER=my-app
JWT_EXPIRATION=3600
JWT_REFRESH_EXPIRATION=86400
JWT_ALGORITHM=HS256
import "github.com/fitlcarlos/go-data/odata"
func main() {
server := odata.NewServer()
// 1. Criar JwtAuth (lê automaticamente do .env)
jwtAuth := odata.NewJwtAuth(nil)
// 2. Registrar entidades com WithAuth()
server.RegisterEntity("Users", User{},
odata.WithAuth(jwtAuth),
)
server.Start()
}
Opção 2: Override Parcial
// Usa JWT_SECRET do .env, mas override expiration
jwtAuth := odata.NewJwtAuth(&odata.JWTConfig{
ExpiresIn: 2 * time.Hour, // Override apenas isso
})
Opção 3: Configuração Manual Completa
// Configuração completamente manual (ignora .env)
jwtAuth := odata.NewJwtAuth(&odata.JWTConfig{
SecretKey: "manual-secret-key-min-32-chars",
Issuer: "my-app",
ExpiresIn: 1 * time.Hour,
RefreshIn: 24 * time.Hour,
Algorithm: "HS256",
})
server.RegisterEntity("Products", Product{},
odata.WithAuth(jwtAuth),
odata.WithReadOnly(false),
)
// 3. Criar suas próprias rotas de autenticação
router := server.GetRouter()
router.Post("/auth/login", handleLogin(jwtAuth))
router.Post("/auth/refresh", handleRefresh(jwtAuth))
router.Get("/auth/me", odata.AuthMiddleware(jwtAuth), handleMe())
server.Start()
}
Interface ContextAuthenticator
A partir da versão mais recente, o Go-Data oferece a interface ContextAuthenticator que fornece acesso ao contexto enriquecido durante a autenticação, incluindo ObjectManager, Connection, Provider, Pool e informações da requisição (IP, Headers, etc).
Benefícios do ContextAuthenticator
- 🔐 Login com banco de dados: Validar credenciais diretamente no banco
- 🔄 Refresh token inteligente: Recarregar roles/permissions atualizadas
- 📝 Audit logging: Registrar IP, device, tentativas de login
- 🚫 Validação em tempo real: Verificar se usuário está ativo durante refresh
- 🏢 Multi-tenant: Acesso ao pool de conexões e tenant ID
Definição da Interface
type ContextAuthenticator interface {
// AuthenticateWithContext autentica usuário durante login
// ctx fornece acesso ao banco de dados, IP do cliente, headers, etc
AuthenticateWithContext(ctx *AuthContext, username, password string) (*UserIdentity, error)
// RefreshToken recarrega/valida dados do usuário durante refresh token
// Permite validar se usuário ainda está ativo e atualizar roles/permissions
// O contexto está disponível caso você queira validar no banco de dados
RefreshToken(ctx *AuthContext, username string) (*UserIdentity, error)
}
Exemplo Completo
type DatabaseAuthenticator struct{}
// AuthenticateWithContext - Login com validação no banco
func (a *DatabaseAuthenticator) AuthenticateWithContext(ctx *odata.AuthContext, username, password string) (*odata.UserIdentity, error) {
conn := ctx.GetConnection()
// Buscar usuário no banco
var dbPassword string
var userID int64
var isActive bool
query := "SELECT id, password, is_active FROM users WHERE email = ?"
err := conn.QueryRow(query, username).Scan(&userID, &dbPassword, &isActive)
if err != nil {
log.Printf("❌ Login failed: user not found - %s from IP %s", username, ctx.IP())
return nil, errors.New("credenciais inválidas")
}
// Validar senha (use bcrypt em produção!)
if dbPassword != password {
log.Printf("❌ Login failed: invalid password - %s from IP %s", username, ctx.IP())
return nil, errors.New("credenciais inválidas")
}
if !isActive {
return nil, errors.New("usuário inativo")
}
// Audit log
conn.Exec("INSERT INTO audit_log (user_id, action, ip) VALUES (?, 'login', ?)", userID, ctx.IP())
return &odata.UserIdentity{
Username: username,
Roles: []string{"user"},
Custom: map[string]interface{}{
"user_id": userID,
"login_ip": ctx.IP(),
},
}, nil
}
// RefreshToken - Recarregar dados atualizados do usuário
func (a *DatabaseAuthenticator) RefreshToken(ctx *odata.AuthContext, username string) (*odata.UserIdentity, error) {
conn := ctx.GetConnection()
// Buscar dados ATUALIZADOS do usuário (roles podem ter mudado!)
var userID int64
var isActive bool
var isAdmin bool
query := "SELECT id, is_active, is_admin FROM users WHERE email = ?"
err := conn.QueryRow(query, username).Scan(&userID, &isActive, &isAdmin)
if err != nil || !isActive {
log.Printf("❌ Refresh failed: user not found or inactive - %s", username)
return nil, errors.New("usuário não encontrado ou inativo")
}
// Audit log
conn.Exec("INSERT INTO audit_log (user_id, action, ip) VALUES (?, 'refresh', ?)", userID, ctx.IP())
roles := []string{"user"}
if isAdmin {
roles = append(roles, "admin")
}
return &odata.UserIdentity{
Username: username,
Roles: roles,
Admin: isAdmin,
Custom: map[string]interface{}{
"user_id": userID,
"refreshed_ip": ctx.IP(),
},
}, nil
}
// Configurar no servidor
func main() {
server := odata.NewServer()
server.RegisterEntity("Users", User{})
// SetupAuthRoutes usa automaticamente ContextAuthenticator
authenticator := &DatabaseAuthenticator{}
server.SetupAuthRoutes(authenticator)
server.Start()
}
Endpoints Criados Automaticamente
O método SetupAuthRoutes() cria automaticamente:
POST /auth/login- Login com AuthenticateWithContextPOST /auth/refresh- Refresh usando RefreshTokenPOST /auth/logout- Logout (invalidação de token)GET /auth/me- Informações do usuário autenticado
Criando Rotas de Autenticação Manualmente
Se preferir não usar SetupAuthRoutes(), você pode criar suas próprias rotas de autenticação com total controle:
func handleLogin(jwtAuth *odata.JwtAuth) fiber.Handler {
return func(c fiber.Ctx) error {
var req LoginRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Dados inválidos"})
}
// Validar credenciais (seu código)
user, err := authenticateUser(req.Username, req.Password)
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "Credenciais inválidas"})
}
// Gerar tokens
accessToken, _ := jwtAuth.GenerateToken(user)
refreshToken, _ := jwtAuth.GenerateRefreshToken(user)
return c.JSON(fiber.Map{
"access_token": accessToken,
"refresh_token": refreshToken,
"token_type": "Bearer",
"expires_in": int64(jwtAuth.GetConfig().ExpiresIn.Seconds()),
"user": user,
})
}
}
Customização Avançada
Customizar Geração de Tokens
jwtAuth := odata.NewJwtAuth(config)
// Opção 1: Adicionar claims extras e chamar o método padrão
jwtAuth.TokenGenerator = func(user *odata.UserIdentity) (string, error) {
// Adicionar claims extras
if user.Custom == nil {
user.Custom = make(map[string]interface{})
}
user.Custom["ip"] = getCurrentIP()
user.Custom["device"] = getDeviceInfo()
user.Custom["generated_at"] = time.Now().Unix()
// ✅ Chamar o método padrão (PÚBLICO)
return jwtAuth.DefaultGenerateToken(user)
}
// Opção 2: Implementação completamente customizada
jwtAuth.TokenGenerator = func(user *odata.UserIdentity) (string, error) {
// Sua lógica JWT customizada do zero
token := jwt.NewWithClaims(jwt.SigningMethodHS512, customClaims)
return token.SignedString([]byte("custom-secret"))
}
Customizar Validação de Tokens
// Opção 1: Adicionar validações extras e chamar o método padrão
jwtAuth.TokenValidator = func(tokenString string) (*odata.UserIdentity, error) {
// Verificações extras ANTES da validação padrão
if isTokenBlacklisted(tokenString) {
return nil, errors.New("token revogado")
}
// ✅ Chamar validação padrão (PÚBLICO)
user, err := jwtAuth.DefaultValidateToken(tokenString)
if err != nil {
return nil, err
}
// Verificações extras DEPOIS da validação
if !isUserActive(user.Username) {
return nil, errors.New("usuário inativo")
}
return user, nil
}
// Opção 2: Implementação completamente customizada
jwtAuth.TokenValidator = func(tokenString string) (*odata.UserIdentity, error) {
// Parser JWT customizado
claims, err := parseCustomToken(tokenString)
if err != nil {
return nil, err
}
return &odata.UserIdentity{
Username: claims.Username,
Roles: claims.Roles,
// ...
}, nil
}
Customizar Extração de Tokens
// Opção 1: Tentar múltiplas fontes com fallback para o padrão
jwtAuth.TokenExtractor = func(c fiber.Ctx) string {
// 1. Tentar cookie primeiro
if token := c.Cookies("auth_token"); token != "" {
return token
}
// 2. Tentar query parameter (não recomendado em produção)
if token := c.Query("token"); token != "" {
return token
}
// 3. ✅ Fallback para extração padrão (Header Authorization: Bearer)
return jwtAuth.DefaultExtractToken(c)
}
// Opção 2: Implementação completamente customizada
jwtAuth.TokenExtractor = func(c fiber.Ctx) string {
// Extração customizada (ex: de um header customizado)
token := c.Get("X-Custom-Auth-Token")
return strings.TrimPrefix(token, "Token ")
}
Diferentes JWTs para Diferentes Entidades
// JWT para usuários admin
adminAuth := odata.NewJwtAuth(&odata.JWTConfig{
SecretKey: "admin-secret",
ExpiresIn: 30 * time.Minute, // Tokens admin expiram mais rápido
})
// JWT para usuários normais
userAuth := odata.NewJwtAuth(&odata.JWTConfig{
SecretKey: "user-secret",
ExpiresIn: 2 * time.Hour,
})
// JWT para API keys
apiKeyAuth := odata.NewJwtAuth(&odata.JWTConfig{
SecretKey: "api-secret",
ExpiresIn: 365 * 24 * time.Hour, // 1 ano
})
// Aplicar diferentes auths
server.RegisterEntity("Users", User{}, odata.WithAuth(adminAuth))
server.RegisterEntity("Products", Product{}, odata.WithAuth(userAuth))
server.RegisterEntity("Reports", Report{}, odata.WithAuth(apiKeyAuth), odata.WithReadOnly(true))
Implementar AuthProvider Customizado
Você pode implementar sua própria autenticação (OAuth, SAML, etc):
type OAuth2Provider struct {
clientID string
clientSecret string
}
func (o *OAuth2Provider) ValidateToken(token string) (*odata.UserIdentity, error) {
// Validar com servidor OAuth2
claims, err := validateOAuth2Token(token, o.clientID, o.clientSecret)
if err != nil {
return nil, err
}
return &odata.UserIdentity{
Username: claims.Email,
Roles: claims.Roles,
// ...
}, nil
}
func (o *OAuth2Provider) GenerateToken(user *odata.UserIdentity) (string, error) {
// OAuth2 não gera tokens diretamente
return "", errors.New("use OAuth2 authorization flow")
}
func (o *OAuth2Provider) ExtractToken(c fiber.Ctx) string {
return c.Get("Authorization")
}
// Usar
oauth := &OAuth2Provider{clientID: "...", clientSecret: "..."}
server.RegisterEntity("Users", User{}, odata.WithAuth(oauth))
Estrutura de UserIdentity
type UserIdentity struct {
Username string `json:"username"`
Roles []string `json:"roles"`
Scopes []string `json:"scopes"`
Admin bool `json:"admin"`
Custom map[string]interface{} `json:"custom"` // Claims customizados
}
// Métodos disponíveis
user.HasRole("manager") // Verifica role específica
user.HasAnyRole("admin", "user") // Verifica múltiplas roles
user.HasScope("write") // Verifica scope específico
user.IsAdmin() // Verifica se é admin
user.GetCustomClaim("department") // Obtém claim customizado
Middleware de Autenticação
// Middleware obrigatório
router.Get("/protected", odata.AuthMiddleware(jwtAuth), handler)
// Middleware opcional
router.Get("/public", odata.OptionalAuthMiddleware(jwtAuth), handler)
// Verificar usuário no handler
func handler(c fiber.Ctx) error {
user := odata.GetCurrentUser(c)
if user == nil {
return c.Status(401).JSON(fiber.Map{"error": "Não autenticado"})
}
if !user.HasRole("admin") {
return c.Status(403).JSON(fiber.Map{"error": "Sem permissão"})
}
return c.JSON(fiber.Map{"message": "Acesso permitido"})
}
Entity Options
// WithAuth - Configura autenticação
server.RegisterEntity("Users", User{}, odata.WithAuth(jwtAuth))
// WithReadOnly - Entidade somente leitura
server.RegisterEntity("Reports", Report{},
odata.WithAuth(jwtAuth),
odata.WithReadOnly(true),
)
// Sem autenticação (público)
server.RegisterEntity("PublicData", PublicData{})
Exemplo de Login Completo
# 1. Fazer login
POST /auth/login
Content-Type: application/json
{
"username": "admin",
"password": "password123"
}
# Resposta:
{
"access_token": "eyJhbGc...",
"refresh_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"username": "admin",
"roles": ["admin"],
"admin": true
}
}
# 2. Acessar endpoint protegido
GET /odata/Users
Authorization: Bearer eyJhbGc...
# 3. Renovar token
POST /auth/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGc..."
}
Exemplos Completos
Veja exemplos completos de autenticação:
examples/jwt/- JWT desacoplado com múltiplos usuáriosexamples/jwt_banco/- JWT com integração de banco de dadosexamples/basic_auth/- Basic Auth com validação em banco de dados
Configuração de Segurança
type JWTConfig struct {
SecretKey string // Chave secreta para assinatura
Issuer string // Emissor do token
ExpiresIn time.Duration // Tempo de expiração do access token
RefreshIn time.Duration // Tempo de expiração do refresh token
Algorithm string // Algoritmo de assinatura (HS256)
}
Migração do Modelo Antigo
Se você usava o modelo antigo embutido, veja como migrar:
// ANTES (modelo antigo - embutido)
server.SetupAuthRoutes(authenticator)
server.SetEntityAuth("Users", odata.EntityAuthConfig{...})
// DEPOIS (modelo novo - desacoplado)
jwtAuth := odata.NewJwtAuth(config)
server.RegisterEntity("Users", User{}, odata.WithAuth(jwtAuth))
router.Post("/auth/login", handleLogin(jwtAuth))
🔓 Autenticação Basic
O Go-Data oferece suporte à autenticação Basic (HTTP Basic Authentication) através do mesmo modelo desacoplado e flexível do JWT. A autenticação Basic é ideal para APIs internas, scripts, integração entre servidores e ambientes onde simplicidade é preferível.
Características
- ✅ Desacoplado: Implementa a interface
AuthProvider - ✅ Stateless: Sem necessidade de armazenamento de sessão
- ✅ Simples: Credenciais em Base64 no header Authorization
- ✅ Customizável: Validação de usuário completamente personalizável
- ✅ Por Entidade: Configure autenticação diferente para cada entidade
- ✅ WWW-Authenticate: Suporte ao header padrão RFC 7617
Uso Básico com BasicAuth
A implementação BasicAuth oferece autenticação HTTP Basic completa:
import (
"github.com/fitlcarlos/go-data/odata"
)
func main() {
server := odata.NewServer()
// 1. Criar BasicAuth com função de validação
basicAuth := odata.NewBasicAuth(
&odata.BasicAuthConfig{
Realm: "My API", // Nome do realm para o WWW-Authenticate header
},
validateUser, // Função que valida username/password
)
// 2. Registrar entidades com WithAuth()
server.RegisterEntity("Users", User{},
odata.WithAuth(basicAuth),
)
server.RegisterEntity("Products", Product{},
odata.WithAuth(basicAuth),
odata.WithReadOnly(false),
)
server.Start()
}
// validateUser valida credenciais e retorna UserIdentity
func validateUser(username, password string) (*odata.UserIdentity, error) {
// Validar contra banco de dados, cache, etc
user, err := db.GetUserByCredentials(username, password)
if err != nil {
return nil, errors.New("credenciais inválidas")
}
return &odata.UserIdentity{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Role: user.Role,
Claims: map[string]interface{}{
"department": user.Department,
},
}, nil
}
Middleware Específico para Basic Auth
O Basic Auth possui um middleware específico que envia o header WWW-Authenticate:
router := server.GetRouter()
// Rota protegida com Basic Auth
router.Get("/api/me", odata.BasicAuthMiddleware(basicAuth), func(c fiber.Ctx) error {
user := odata.GetUserFromContext(c)
return c.JSON(user)
})
// Também funciona com o middleware genérico
router.Get("/api/info", odata.AuthMiddleware(basicAuth), handler)
Customização da Validação
basicAuth := odata.NewBasicAuth(config, validateUser)
// Adicionar logging e métricas
originalValidator := basicAuth.UserValidator
basicAuth.UserValidator = func(username, password string) (*odata.UserIdentity, error) {
log.Printf("Tentativa de login: %s", username)
user, err := originalValidator(username, password)
if err != nil {
log.Printf("Login falhou: %s - %v", username, err)
metrics.IncrementFailedLogins()
return nil, err
}
log.Printf("Login bem-sucedido: %s", username)
metrics.IncrementSuccessfulLogins()
return user, nil
}
Customizar Extração de Credenciais
basicAuth := odata.NewBasicAuth(config, validateUser)
// Suportar múltiplas fontes de credenciais
basicAuth.TokenExtractor = func(c fiber.Ctx) string {
// 1. Tentar header padrão primeiro
if token := basicAuth.DefaultExtractToken(c); token != "" {
return token
}
// 2. Tentar header customizado
if customAuth := c.Get("X-Custom-Auth"); customAuth != "" {
// Processar formato customizado
return extractFromCustomHeader(customAuth)
}
return ""
}
Usar Basic Auth com Banco de Dados
func validateUser(username, password string) (*odata.UserIdentity, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var user User
query := `SELECT id, username, email, role, active
FROM users
WHERE username = ? AND password = ? AND active = 1`
err := db.QueryRowContext(ctx, query, username, password).Scan(
&user.ID, &user.Username, &user.Email, &user.Role, &user.Active,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.New("credenciais inválidas")
}
return nil, fmt.Errorf("erro ao consultar usuário: %w", err)
}
return &odata.UserIdentity{
ID: fmt.Sprintf("%d", user.ID),
Username: user.Username,
Email: user.Email,
Role: user.Role,
}, nil
}
Diferentes Auths para Diferentes Entidades
// Basic Auth para API interna
internalAuth := odata.NewBasicAuth(
&odata.BasicAuthConfig{Realm: "Internal API"},
validateInternalUser,
)
// JWT para API pública
publicAuth := odata.NewJwtAuth(&odata.JWTConfig{
SecretKey: "public-secret",
})
// Aplicar diferentes auths
server.RegisterEntity("InternalReports", Report{}, odata.WithAuth(internalAuth))
server.RegisterEntity("PublicProducts", Product{}, odata.WithAuth(publicAuth))
Exemplo de Requisição
# 1. Usando curl com -u (recomendado)
curl -u admin:admin123 http://localhost:3000/api/v1/Users
# 2. Usando header Authorization manual
curl -H "Authorization: Basic YWRtaW46YWRtaW4xMjM=" http://localhost:3000/api/v1/Users
# 3. Gerar Base64 manualmente
echo -n "admin:admin123" | base64
# Resultado: YWRtaW46YWRtaW4xMjM=
Resposta 401 com WWW-Authenticate
Quando credenciais são inválidas ou ausentes, o servidor responde com:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="My API"
Content-Type: application/json
{
"error": "Autenticação requerida"
}
Isso faz com que navegadores modernos exibam um prompt de login automaticamente.
Exemplo Completo
Veja um exemplo completo com banco de dados em examples/basic_auth/.
Quando Usar Basic Auth
✅ Recomendado para:
- APIs internas entre servidores
- Scripts e automações
- Ambientes com HTTPS garantido
- Integrações simples
- Prototipagem rápida
⚠️ Não recomendado para:
- APIs públicas expostas na internet
- Aplicações web frontend (use JWT)
- Ambientes sem HTTPS (credenciais são enviadas em Base64)
- Quando precisa de logout/expiração (use JWT)
Segurança
IMPORTANTE: Basic Auth DEVE ser usado APENAS com HTTPS/TLS. As credenciais são enviadas em Base64 (não criptografadas) e podem ser facilmente decodificadas.
// Configure TLS para produção
server := odata.NewServer(&odata.Config{
TLS: &odata.TLSConfig{
Enabled: true,
CertFile: "/path/to/cert.pem",
KeyFile: "/path/to/key.pem",
},
})
Comparação: Basic Auth vs JWT
| Característica | Basic Auth | JWT |
|---|---|---|
| Complexidade | Simples | Moderada |
| Stateless | ✅ Sim | ✅ Sim |
| Expiração | ❌ Não | ✅ Sim |
| Revogação | ❌ Difícil | ✅ Possível |
| Performance | ⚡ Rápida | ⚡ Rápida |
| Logout | ❌ Não | ✅ Sim |
| Refresh Token | ❌ Não | ✅ Sim |
| Casos de Uso | APIs internas | APIs públicas |
🔒 Segurança
O Go-Data implementa múltiplas camadas de segurança para proteger suas APIs contra ataques e vazamentos de dados.
Proteção contra SQL Injection
✅ Implementado automaticamente - Todas as queries usam Prepared Statements com parametrização via sql.Named.
// ✅ Seguro - Uso automático de prepared statements
server.RegisterEntity("Users", User{})
// Queries como: $filter=name eq 'value' são automaticamente parametrizadas
Validação de Inputs:
- Tamanho máximo de queries ($filter, $search, etc)
- Detecção de padrões de SQL injection
- Validação de nomes de propriedades
- Limites de profundidade em $expand
config := &odata.ValidationConfig{
MaxFilterLength: 5000, // 5KB
MaxSearchLength: 1000, // 1KB
MaxTopValue: 1000, // máximo 1000 registros
MaxExpandDepth: 5, // máximo 5 níveis
EnableXSSProtection: true,
}
server.GetConfig().ValidationConfig = config
Security Headers
O Go-Data inclui headers de segurança habilitados por padrão para proteção contra ataques comuns.
Headers Aplicados por Padrão
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self'; script-src 'self'; ...
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
X-Permitted-Cross-Domain-Policies: none
X-Download-Options: noopen
Configurações Predefinidas (Helpers)
O Go-Data oferece funções helper para diferentes perfis de segurança:
// 1. Padrão (Balanceado) - Recomendado para maioria dos casos
config := odata.DefaultSecurityHeadersConfig()
server.SetSecurityHeaders(config)
Características:
- ✅ Proteção contra clickjacking (X-Frame-Options: DENY)
- ✅ Previne MIME type sniffing
- ✅ Content Security Policy moderado
- ✅ HSTS com 1 ano
- ✅ Referrer policy balanceado
// 2. Estrito (Máxima Segurança) - Para aplicações críticas
config := odata.StrictSecurityHeadersConfig()
server.SetSecurityHeaders(config)
Características:
- 🔒 CSP muito restritivo (
default-src 'none') - 🔒 HSTS com 2 anos + preload
- 🔒 Bloqueia todas as features do browser
- 🔒 Referrer policy:
no-referrer - 🔒 Frame-Options: DENY
- ⚠️ Pode quebrar funcionalidades se não configurado corretamente
// 3. Relaxado (Desenvolvimento) - Para ambiente de desenvolvimento
config := odata.RelaxedSecurityHeadersConfig()
server.SetSecurityHeaders(config)
Características:
- 🟢 CSP permissivo (
default-src 'self' 'unsafe-inline' 'unsafe-eval') - 🟢 Permite iframes da mesma origem
- 🟢 HSTS desabilitado (para facilitar testes HTTP)
- 🟢 Todas as features do browser permitidas
- ⚠️ NÃO use em produção!
// 4. Desabilitado - Remove todos os headers de segurança
config := odata.DisableSecurityHeaders()
server.SetSecurityHeaders(config)
Quando usar:
- ⚠️ Apenas quando headers conflitam com infraestrutura existente
- ⚠️ Quando proxy/gateway já adiciona os headers
- ⚠️ Não recomendado na maioria dos casos
Configuração Customizada
Para controle total sobre os headers:
config := &odata.SecurityHeadersConfig{
Enabled: true,
// Proteção Clickjacking
XFrameOptions: "SAMEORIGIN", // ou "DENY", "ALLOW-FROM https://example.com"
// Prevenir MIME sniffing
XContentTypeOptions: "nosniff",
// XSS Protection (deprecated mas ainda útil)
XXSSProtection: "1; mode=block",
// Content Security Policy (CSP)
ContentSecurityPolicy: `
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
`,
// Forçar HTTPS (apenas se conexão já for HTTPS)
StrictTransportSecurity: "max-age=31536000; includeSubDomains; preload",
// Controlar informações de referrer
ReferrerPolicy: "strict-origin-when-cross-origin",
// Opções: no-referrer, no-referrer-when-downgrade, same-origin,
// origin, strict-origin, origin-when-cross-origin
// Controlar features do browser
PermissionsPolicy: "camera=(), microphone=(), geolocation=(self), payment=()",
// Headers customizados adicionais
CustomHeaders: map[string]string{
"X-Custom-Header": "value",
"X-API-Version": "1.0",
},
}
server.SetSecurityHeaders(config)
Comparação dos Perfis
| Recurso | Padrão | Estrito | Relaxado |
|---|---|---|---|
| X-Frame-Options | DENY | DENY | SAMEORIGIN |
| CSP default-src | 'self' | 'none' | 'self' 'unsafe-inline' 'unsafe-eval' |
| HSTS | 1 ano | 2 anos + preload | Desabilitado |
| Permissions | Básicas bloqueadas | Todas bloqueadas | Todas permitidas |
| Referrer-Policy | strict-origin-when-cross-origin | no-referrer | no-referrer-when-downgrade |
| Produção | ✅ Sim | ✅ Sim (apps críticas) | ❌ Não |
| Desenvolvimento | ⚠️ Pode dificultar | ❌ Muito restritivo | ✅ Sim |
Usando Helpers com Setters
Combine helpers com API fluente:
server := odata.NewServer()
// Ambiente de produção
if os.Getenv("APP_ENV") == "production" {
server.SetSecurityHeaders(odata.StrictSecurityHeadersConfig()).
SetRateLimit(100, 20).
SetAuditLog(&odata.AuditLogConfig{Enabled: true})
} else {
// Desenvolvimento
server.SetSecurityHeaders(odata.RelaxedSecurityHeadersConfig()).
DisableRateLimit()
}
Verificar Headers Aplicados
Você pode verificar os headers aplicados fazendo uma requisição:
curl -I http://localhost:8080/odata/Users
HTTP/1.1 200 OK
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self'; ...
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), ...
Melhores Práticas
- Use Strict em Produção: Para APIs críticas, use
StrictSecurityHeadersConfig() - Customize CSP: Ajuste CSP para suas necessidades específicas
- HSTS apenas em HTTPS: HSTS só funciona em conexões HTTPS
- Teste Thoroughly: Headers estritos podem quebrar funcionalidades
- Monitore Violations: Configure CSP report-uri para monitorar violações
Audit Logging
Sistema completo de auditoria para rastrear todas operações críticas com configuração flexível.
Configuração Básica
config := &odata.AuditLogConfig{
Enabled: true,
LogType: "file", // "file", "stdout", "stderr"
FilePath: "audit.log",
Format: "json", // "json" ou "text"
}
server.GetConfig().AuditLogConfig = config
AuditLogConfig Completo
Configure detalhadamente o sistema de auditoria:
config := &odata.AuditLogConfig{
// Configuração Básica
Enabled: true, // Habilitar audit logging (padrão: false)
LogType: "file", // Tipo: "file", "stdout", "stderr", "none" (padrão: "stdout")
FilePath: "/var/log/api-audit.log", // Caminho do arquivo (quando LogType = "file")
Format: "json", // Formato: "json" ou "text" (padrão: "json")
// Performance
BufferSize: 100, // Buffer para escrita assíncrona (padrão: 100)
AsyncWrite: true, // Escrita assíncrona (não bloqueia requisição)
FlushInterval: 5 * time.Second, // Intervalo para flush do buffer
// Filtros de Operações
LoggedOperations: []odata.AuditOperation{ // Operações a logar (vazio = todas)
odata.AuditOpCreate,
odata.AuditOpUpdate,
odata.AuditOpDelete,
odata.AuditOpAuthFailure,
odata.AuditOpUnauthorized,
},
// Controle de Dados
IncludeSensitiveData: false, // Incluir dados sensíveis (não recomendado em prod)
IncludeRequestBody: false, // Incluir corpo da requisição completo
IncludeResponseBody: false, // Incluir corpo da resposta
MaxBodySize: 1024, // Tamanho máximo de body a logar (bytes)
// Campos Adicionais
IncludeHeaders: []string{ // Headers específicos a incluir
"User-Agent",
"X-Forwarded-For",
"X-Request-ID",
},
ExcludeFields: []string{ // Campos a excluir do log
"password",
"token",
"secret",
},
// Rotação de Logs (quando LogType = "file")
MaxFileSize: 100 * 1024 * 1024, // 100MB - tamanho máximo por arquivo
MaxBackups: 10, // Número de arquivos de backup
MaxAge: 30, // Dias para manter logs antigos
Compress: true, // Comprimir logs antigos
}
server.SetAuditLog(config)
Operações Auditadas
Tipos de operações que podem ser auditadas:
const (
AuditOpCreate = "CREATE" // Criação de entidade
AuditOpUpdate = "UPDATE" // Atualização de entidade
AuditOpDelete = "DELETE" // Exclusão de entidade
AuditOpRead = "READ" // Leitura de entidade
AuditOpAuthSuccess = "AUTH_SUCCESS" // Login bem-sucedido
AuditOpAuthFailure = "AUTH_FAILURE" // Falha de autenticação
AuditOpAuthLogout = "AUTH_LOGOUT" // Logout
AuditOpUnauthorized = "UNAUTHORIZED" // Acesso negado
)
Exemplo de Log Entry (JSON)
{
"timestamp": "2025-10-27T10:30:45Z",
"user_id": "42",
"username": "john.doe",
"ip": "192.168.1.100",
"method": "POST",
"path": "/odata/Users",
"entity_name": "Users",
"entity_id": "123",
"operation": "CREATE",
"success": true,
"error_message": "",
"duration_ms": 45,
"user_agent": "Mozilla/5.0...",
"request_id": "abc-123-def",
"tenant_id": "empresa_a",
"extra": {
"changes": ["name", "email"],
"ip_location": "São Paulo, BR"
}
}
Exemplo de Log Entry (Text)
2025-10-27 10:30:45 [CREATE] john.doe (192.168.1.100) -> POST /odata/Users [SUCCESS] 45ms
2025-10-27 10:30:50 [UPDATE] admin (192.168.1.101) -> PATCH /odata/Users(123) [SUCCESS] 32ms
2025-10-27 10:30:55 [AUTH_FAILURE] - (192.168.1.150) -> POST /auth/login [FAILED] invalid credentials
Configurações Predefinidas
// Desenvolvimento (verboso)
devConfig := &odata.AuditLogConfig{
Enabled: true,
LogType: "stdout",
Format: "text",
IncludeSensitiveData: true, // OK para dev
IncludeRequestBody: true,
IncludeResponseBody: true,
LoggedOperations: []odata.AuditOperation{}, // Todas
}
// Produção (seguro e performático)
prodConfig := &odata.AuditLogConfig{
Enabled: true,
LogType: "file",
FilePath: "/var/log/api/audit.log",
Format: "json",
BufferSize: 200,
AsyncWrite: true,
IncludeSensitiveData: false, // Nunca em produção!
IncludeRequestBody: false,
LoggedOperations: []odata.AuditOperation{
odata.AuditOpCreate,
odata.AuditOpUpdate,
odata.AuditOpDelete,
odata.AuditOpAuthFailure,
odata.AuditOpUnauthorized,
},
MaxFileSize: 100 * 1024 * 1024,
MaxBackups: 30,
MaxAge: 90,
Compress: true,
}
// Compliance (máxima auditoria)
complianceConfig := &odata.AuditLogConfig{
Enabled: true,
LogType: "file",
FilePath: "/var/log/audit/compliance.log",
Format: "json",
IncludeSensitiveData: false,
IncludeRequestBody: true, // Logar tudo (exceto sensível)
LoggedOperations: []odata.AuditOperation{}, // Todas as operações
MaxFileSize: 500 * 1024 * 1024,
MaxBackups: 100,
MaxAge: 365, // 1 ano
Compress: true,
}
server.SetAuditLog(prodConfig)
Usando com Autenticação
jwtAuth := odata.NewJwtAuth(config)
// Com audit logging automático
router.Get("/protected",
odata.AuthMiddlewareWithAudit(jwtAuth, server.GetAuditLogger()),
handler)
Audit Logging Customizado
Você pode criar seu próprio audit logger implementando a interface:
type CustomAuditLogger struct {
// Seus campos
}
func (c *CustomAuditLogger) Log(entry odata.AuditLogEntry) error {
// Enviar para sistema externo (Elasticsearch, Splunk, etc)
return sendToElasticsearch(entry)
}
func (c *CustomAuditLogger) Close() error {
// Cleanup
return nil
}
// Usar custom logger
server.GetConfig().AuditLogConfig.CustomLogger = &CustomAuditLogger{}
Consultar Logs Programaticamente
Se usar arquivo JSON, você pode consultar os logs facilmente:
# Buscar falhas de autenticação
grep '"operation":"AUTH_FAILURE"' audit.log | jq .
# Buscar operações de um usuário específico
grep '"username":"john.doe"' audit.log | jq .
# Buscar operações em entidade específica
grep '"entity_name":"Users"' audit.log | jq .
# Buscar operações lentas (> 1 segundo)
jq 'select(.duration_ms > 1000)' audit.log
Integração com SIEM
Para integração com sistemas SIEM (Splunk, ELK, etc):
// Configurar para stdout e redirecionar para SIEM
config := &odata.AuditLogConfig{
Enabled: true,
LogType: "stdout",
Format: "json",
IncludeHeaders: []string{
"X-Forwarded-For",
"User-Agent",
"X-Request-ID",
},
}
// No Docker/Kubernetes, os logs stdout são automaticamente coletados
Input Validation
O Go-Data oferece validação automática e configurável para todos os inputs OData, protegendo contra SQL Injection, XSS e outros ataques.
Funções de Validação
// Validar filter
err := odata.ValidateFilterQuery("name eq 'john'", config)
// Validar propriedades
err := odata.ValidatePropertyName("username", config)
// Validar $top
err := odata.ValidateTopValue(100, config)
// Validar profundidade de $expand
err := odata.ValidateExpandDepth(expandOptions, 5, 1)
// Sanitizar input (remove XSS)
safe := odata.SanitizeInput(userInput, config)
ValidationConfig Completo
Configure limites e regras de validação:
config := &odata.ValidationConfig{
// Limites de Query
MaxFilterLength: 1000, // Tamanho máximo do $filter (padrão: 1000)
MaxSelectFields: 50, // Máximo de campos em $select (padrão: 50)
MaxExpandDepth: 5, // Profundidade máxima de $expand (padrão: 5)
MaxTopValue: 1000, // Valor máximo de $top (padrão: 1000)
MaxSkipValue: 10000, // Valor máximo de $skip (padrão: 10000)
MaxOrderByFields: 10, // Máximo de campos em $orderby (padrão: 10)
// Funções Permitidas
AllowedFunctions: []string{ // Funções OData permitidas
"contains", "startswith", "endswith",
"length", "indexof", "substring",
"tolower", "toupper", "trim",
"year", "month", "day", "hour", "minute", "second",
"round", "floor", "ceiling",
},
// Padrões Bloqueados (Regex)
BlockedPatterns: []string{ // Padrões perigosos a serem bloqueados
`(?i)(union|select|insert|update|delete|drop|create|alter|exec|execute)`,
`(?i)(script|iframe|object|embed|onclick|onerror|onload)`,
`--|;--|\|\||&&`,
`\$\{.*\}`, // Template injection
`<\?php`, // PHP injection
},
// Opções de Sanitização
EnableSanitization: true, // Habilitar sanitização de inputs (padrão: true)
StrictPropertyNames: true, // Validar nomes de propriedades (padrão: true)
AllowWildcards: false, // Permitir wildcards em filtros (padrão: false)
CaseSensitive: false, // Case-sensitive para funções (padrão: false)
// Proteção DoS
MaxQueryComplexity: 1000, // Complexidade máxima de query (padrão: 1000)
MaxArrayElements: 100, // Máximo de elementos em arrays (padrão: 100)
}
// Aplicar configuração
server.GetConfig().ValidationConfig = config
// Ou usar configuração padrão
server.GetConfig().ValidationConfig = odata.DefaultValidationConfig()
Configurações Predefinidas
// Desenvolvimento (permissivo)
devConfig := &odata.ValidationConfig{
MaxFilterLength: 2000,
MaxSelectFields: 100,
MaxExpandDepth: 10,
MaxTopValue: 5000,
StrictPropertyNames: false,
AllowWildcards: true,
}
// Produção (restritivo)
prodConfig := &odata.ValidationConfig{
MaxFilterLength: 500,
MaxSelectFields: 20,
MaxExpandDepth: 3,
MaxTopValue: 100,
MaxSkipValue: 1000,
StrictPropertyNames: true,
EnableSanitization: true,
AllowWildcards: false,
}
// Alta Performance (balanceado)
perfConfig := &odata.ValidationConfig{
MaxFilterLength: 1000,
MaxTopValue: 500,
MaxExpandDepth: 4,
EnableSanitization: true,
MaxQueryComplexity: 500,
}
server.GetConfig().ValidationConfig = prodConfig
Padrões Detectados Automaticamente
SQL Injection:
UNION,SELECT,INSERT,UPDATE,DELETE,DROP,CREATE,ALTEREXEC,EXECUTE,xp_,sp_--,;--,||,&&1=1,' OR '1'='1
XSS (Cross-Site Scripting):
<script>,</script>,<iframe>,</iframe>javascript:,vbscript:,data:text/htmlonclick=,onerror=,onload=,onmouseover=<object>,<embed>,<applet>
Template Injection:
${...},{{...}},<%...%>{@...@},[[...]]
Path Traversal:
../,..\\,..\- Sequências URL encoded
Outras Ameaças:
- Queries muito longas (DoS)
- Profundidade excessiva de $expand (DoS)
- Caracteres inválidos em nomes de propriedades
- Arrays muito grandes (Memory DoS)
Exemplo de Uso Completo
package main
import (
"github.com/fitlcarlos/go-data/odata"
)
func main() {
server := odata.NewServer()
// Configurar validação rigorosa para produção
server.GetConfig().ValidationConfig = &odata.ValidationConfig{
MaxFilterLength: 800,
MaxSelectFields: 30,
MaxExpandDepth: 4,
MaxTopValue: 200,
StrictPropertyNames: true,
EnableSanitization: true,
AllowedFunctions: []string{
"contains", "startswith", "endswith",
"tolower", "toupper",
"year", "month", "day",
},
BlockedPatterns: []string{
`(?i)(union|select|insert|update|delete)`,
`(?i)(script|iframe|onclick)`,
`--|;--|&&`,
},
}
// Registrar entidades
server.RegisterEntity("Users", User{})
server.RegisterEntity("Products", Product{})
// Iniciar servidor
server.Start()
}
Validação Customizada por Entidade
Você também pode validar inputs dentro de eventos:
server.OnEntityInsertingGlobal(func(args odata.EventArgs) error {
insertArgs := args.(*odata.EntityInsertingArgs)
// Validar campo específico
if name, ok := insertArgs.Data["name"].(string); ok {
config := server.GetConfig().ValidationConfig
// Sanitizar
sanitized := odata.SanitizeInput(name, config)
// Validar comprimento
if len(sanitized) > 100 {
args.Cancel("Nome muito longo")
return nil
}
// Validar padrões
if err := odata.ValidateAgainstPatterns(sanitized, config.BlockedPatterns); err != nil {
args.Cancel("Nome contém caracteres inválidos")
return nil
}
// Atualizar com valor sanitizado
insertArgs.Data["name"] = sanitized
}
return nil
})
Rate Limiting (Habilitado por Padrão)
⚠️ IMPORTANTE: Rate limiting está HABILITADO por padrão desde a versão atual.
// Configuração padrão (100 req/min)
config := odata.DefaultRateLimitConfig()
// config.Enabled = true (já habilitado)
// config.RequestsPerMinute = 100
// config.BurstSize = 20
// Para desabilitar (não recomendado)
server.GetConfig().RateLimitConfig.Enabled = false
Checklist de Segurança
- SQL Injection: Protegido com prepared statements
- XSS: Sanitização e CSP headers
- CSRF: Headers configuráveis
- Clickjacking: X-Frame-Options
- Rate Limiting: Habilitado por padrão
- Audit Logging: Sistema completo disponível
- Input Validation: Múltiplas validações automáticas
- Security Headers: 8+ headers implementados
- HTTPS/TLS: Configure manualmente para produção
- Secrets Management: Use variáveis de ambiente
Documentação de Segurança
Para guia completo de segurança, incluindo melhores práticas e como reportar vulnerabilidades, veja:
⚡ Performance
O Go-Data implementa múltiplas otimizações de performance para garantir baixa latência e alto throughput.
Otimização N+1 (Expand Batching)
O problema N+1 ocorre quando expandimos relacionamentos e executamos uma query para cada entidade relacionada. Go-Data resolve isso automaticamente usando batching.
Antes (N+1 Problem):
GET /odata/Products?$expand=Category
Queries executadas:
1. SELECT * FROM products -- 1 query inicial
2. SELECT * FROM categories WHERE id=1 -- Para produto 1
3. SELECT * FROM categories WHERE id=1 -- Para produto 2
4. SELECT * FROM categories WHERE id=2 -- Para produto 3
... (N queries, uma por produto)
Total: 1 + N queries = O(N) ❌ LENTO
Depois (Batching):
GET /odata/Products?$expand=Category
Queries executadas:
1. SELECT * FROM products -- 1 query inicial
2. SELECT * FROM categories WHERE id IN (1,2) -- 1 query em batch
Total: 2 queries = O(1) ✅ RÁPIDO (50x mais rápido!)
Exemplo de Uso
A otimização é automática e transparente:
// Registrar entidades normalmente
server.RegisterEntity("Products", Product{})
server.RegisterEntity("Categories", Category{})
// Cliente faz: GET /odata/Products?$expand=Category
// Sistema automaticamente:
// - Detecta expand
// - Coleta todos os CategoryIDs
// - Executa query em batch: WHERE CategoryID IN (1,2,3,...)
// - Associa resultados em memória
// Performance: 2 queries ao invés de N+1! 🚀
Configuração
Por padrão, batching está habilitado. Para debugging ou casos especiais:
config := odata.DefaultServerConfig()
config.DisableJoinForExpand = true // Força comportamento legado (não recomendado)
server := odata.NewServerWithConfig(config, db)
⚠️ Não recomendado desabilitar: Pode causar problemas sérios de performance em produção.
Logs de Performance
Habilite logs para monitorar otimizações:
config := odata.DefaultServerConfig()
config.LogLevel = "DEBUG"
Você verá logs como:
🔍 EXPAND: Using BATCHING for Category (evitando N+1)
🔍 EXPAND BATCH: Filter = CategoryID in (1,2,3) (querying 3 related entities)
✅ EXPAND BATCH: Retrieved 3 related entities in 1 query
✅ EXPAND BATCH: Associated related entities to 100 parent entities
Comparação de Performance
| Cenário | Antes (N+1) | Depois (Batching) | Ganho |
|---|---|---|---|
| 100 Products + Category | 101 queries (~1010ms) | 2 queries (~20ms) | 50x mais rápido |
| 1000 Products + Category | 1001 queries (~10s) | 2 queries (~20ms) | 500x mais rápido |
| Nested expand (2 níveis) | N×M queries | 3 queries | Drasticamente melhor |
String Builder Optimization
Construção otimizada de queries SQL usando strings.Builder ao invés de concatenação +:
- 12% menos alocações de memória
- 3-5% mais rápido em query building
- Especialmente eficiente em queries complexas com múltiplos filtros
Benchmarks
Execute benchmarks para medir performance:
# Todos os benchmarks
go test -bench=. -benchmem ./pkg/odata
# Benchmarks específicos
go test -bench=BenchmarkParse -benchmem ./pkg/odata # Parsers
go test -bench=BenchmarkExpand -benchmem ./pkg/odata # Expand operations
go test -bench=BenchmarkBuild -benchmem ./pkg/odata # Query building
# Com profiling (CPU + memória)
PROFILE=1 go test -bench=BenchmarkProfile -cpuprofile=cpu.prof -memprofile=mem.prof ./pkg/odata
# Visualizar profile no navegador
go tool pprof -http=:8080 cpu.prof
Metas de Performance
- ✅ Parsers: < 50µs para queries simples
- ✅ Query Building: < 100µs para queries completas
- ✅ Expand Operations: < 10ms com batching
- ✅ N+1 Elimination: 2 queries ao invés de N+1
- ✅ Memory: 10-15% menos alocações
📄 pkg/odata/PERFORMANCE.md - Documentação completa de performance
📄 pkg/odata/BENCHMARKS.md - Guia de benchmarks
🛡️ Rate Limiting
O Go-Data implementa um sistema robusto de rate limiting para proteger suas APIs contra abuso e garantir disponibilidade. O sistema oferece controle granular de taxa de requisições com múltiplas estratégias de identificação de clientes.
Características do Rate Limiting
- Controle de taxa por IP, usuário autenticado, API key ou tenant
- Configuração flexível de limites e janelas de tempo
- Headers informativos nas respostas HTTP
- Estratégias customizáveis de geração de chaves
- Suporte a burst de requisições simultâneas
- Limpeza automática de clientes inativos
- Integração transparente com middleware do servidor
Configuração via .env
# Habilitar rate limiting
RATE_LIMIT_ENABLED=true
# 100 requisições por minuto por cliente
RATE_LIMIT_REQUESTS_PER_MINUTE=100
# Permite burst de até 20 requisições simultâneas
RATE_LIMIT_BURST_SIZE=20
# Janela de tempo para contagem (1 minuto)
RATE_LIMIT_WINDOW_SIZE=1m
# Incluir headers de rate limit na resposta
RATE_LIMIT_HEADERS=true
Configuração Programática
import "github.com/fitlcarlos/go-data/odata"
// Configuração básica de rate limit
rateLimitConfig := &odata.RateLimitConfig{
Enabled: true,
RequestsPerMinute: 100,
BurstSize: 20,
WindowSize: time.Minute,
KeyGenerator: odata.defaultKeyGenerator, // Por IP
Headers: true,
}
// Configurar servidor com rate limit
config := odata.DefaultServerConfig()
config.RateLimitConfig = rateLimitConfig
server := odata.NewServerWithConfig(provider, config)
Estratégias de Rate Limiting
1. Por IP (Padrão)
// Limita por endereço IP do cliente
rateLimitConfig.KeyGenerator = odata.defaultKeyGenerator
2. Por Usuário Autenticado
// Limita por usuário autenticado (JWT)
rateLimitConfig.KeyGenerator = odata.UserBasedKeyGenerator
3. Por API Key
// Limita por chave de API
rateLimitConfig.KeyGenerator = odata.APIKeyBasedKeyGenerator
4. Por Tenant (Multi-Tenant)
// Limita por tenant em ambiente multi-tenant
rateLimitConfig.KeyGenerator = odata.TenantBasedKeyGenerator
5. Estratégia Customizada
// Implementar estratégia personalizada
rateLimitConfig.KeyGenerator = func(c fiber.Ctx) string {
// Sua lógica customizada
userID := c.Locals("user_id")
ip := c.IP()
return fmt.Sprintf("custom:%v:%s", userID, ip)
}
Headers de Resposta
Quando habilitado, o sistema inclui headers informativos:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1642678800
X-RateLimit-Retry-After: 30 (apenas quando bloqueado)
Resposta de Rate Limit Excedido
Quando o limite é excedido, o servidor retorna HTTP 429:
{
"error": {
"code": "RateLimitExceeded",
"message": "Rate limit exceeded. Try again in 30 seconds.",
"target": "rate_limit"
}
}
Configuração Avançada
// Configuração avançada com múltiplas estratégias
rateLimitConfig := &odata.RateLimitConfig{
Enabled: true,
RequestsPerMinute: 200,
BurstSize: 50,
WindowSize: 2 * time.Minute,
KeyGenerator: odata.UserBasedKeyGenerator,
SkipSuccessful: false, // Contar requisições bem-sucedidas
SkipFailed: false, // Contar requisições com falha
Headers: true,
}
// Aplicar configuração em runtime
server.SetRateLimitConfig(rateLimitConfig)
Monitoramento e Métricas
// Obter configuração atual
currentConfig := server.GetRateLimitConfig()
if currentConfig != nil {
log.Printf("Rate limit ativo: %d req/min",
currentConfig.RequestsPerMinute)
}
Exemplo Prático
package main
import (
"log"
"time"
"github.com/fitlcarlos/go-data/odata"
)
func main() {
// Configurar rate limit
rateLimitConfig := &odata.RateLimitConfig{
Enabled: true,
RequestsPerMinute: 60, // 1 requisição por segundo
BurstSize: 10, // Permite 10 requisições simultâneas
WindowSize: time.Minute,
KeyGenerator: odata.defaultKeyGenerator,
Headers: true,
}
// Configurar servidor
config := odata.DefaultServerConfig()
config.RateLimitConfig = rateLimitConfig
server := odata.NewServerWithConfig(nil, config)
// Registrar entidades
server.RegisterEntity("Users", User{})
// Iniciar servidor
if err := server.Start(); err != nil {
log.Fatalf("Erro ao iniciar servidor: %v", err)
}
}
Boas Práticas
- Configure limites apropriados baseados na capacidade do seu sistema
- Use burst size para permitir picos de tráfego legítimos
- Monitore headers para ajustar limites conforme necessário
- Implemente estratégias diferentes para diferentes tipos de clientes
- Teste em ambiente de produção para validar configurações
🏢 Multi-Tenant
O Go-Data oferece suporte completo a multi-tenant, permitindo que uma única instância do servidor gerencie múltiplos bancos de dados para diferentes tenants (clientes, organizações, etc.). Cada tenant mantém isolamento completo dos dados.
Características Multi-Tenant
- Identificação automática de tenant via headers, subdomains, path ou JWT
- Pool de conexões gerenciado automaticamente para cada tenant
- Configuração via .env com suporte a múltiplos bancos de dados
- Isolamento completo de dados por tenant
- Compatibilidade com Oracle, PostgreSQL e MySQL
- Endpoints específicos para monitoramento e gerenciamento de tenants
- Escalabilidade com adição dinâmica de novos tenants
Configuração Multi-Tenant
Arquivo .env
# Configuração Multi-Tenant
MULTI_TENANT_ENABLED=true
TENANT_IDENTIFICATION_MODE=header
TENANT_HEADER_NAME=X-Tenant-ID
DEFAULT_TENANT=default
# Configuração do servidor
SERVER_HOST=localhost
SERVER_PORT=8080
SERVER_ROUTE_PREFIX=/api/odata
# Configuração do banco padrão
DB_TYPE=oracle
DB_HOST=localhost
DB_PORT=1521
DB_NAME=ORCL
DB_USER=system
DB_PASSWORD=password
# Configuração específica por tenant
TENANT_EMPRESA_A_DB_DRIVER=oracle
TENANT_EMPRESA_A_DB_HOST=oracle1.empresa.com
TENANT_EMPRESA_A_DB_PORT=1521
TENANT_EMPRESA_A_DB_NAME=EMPRESA_A
TENANT_EMPRESA_A_DB_USER=user_a
TENANT_EMPRESA_A_DB_PASSWORD=password_a
TENANT_EMPRESA_B_DB_DRIVER=postgres
TENANT_EMPRESA_B_DB_HOST=postgres1.empresa.com
TENANT_EMPRESA_B_DB_PORT=5432
TENANT_EMPRESA_B_DB_NAME=empresa_b
TENANT_EMPRESA_B_DB_USER=user_b
TENANT_EMPRESA_B_DB_PASSWORD=password_b
TENANT_EMPRESA_C_DB_DRIVER=mysql
TENANT_EMPRESA_C_DB_HOST=mysql1.empresa.com
TENANT_EMPRESA_C_DB_PORT=3306
TENANT_EMPRESA_C_DB_NAME=empresa_c
TENANT_EMPRESA_C_DB_USER=user_c
TENANT_EMPRESA_C_DB_PASSWORD=password_c
Código do Servidor
package main
import (
"log"
"github.com/fitlcarlos/go-data/odata"
)
func main() {
// Cria servidor com carregamento automático de configurações multi-tenant
server := odata.NewServer()
// Registra as entidades (automaticamente multi-tenant se configurado)
server.RegisterEntity("Produtos", &Produto{})
server.RegisterEntity("Clientes", &Cliente{})
server.RegisterEntity("Pedidos", &Pedido{})
// Eventos globais com informações de tenant
server.OnEntityListGlobal(func(args odata.EventArgs) error {
if listArgs, ok := args.(*odata.EntityListArgs); ok {
tenantID := odata.GetCurrentTenant(listArgs.Context.FiberContext)
log.Printf("📋 Lista acessada: %s (tenant: %s)",
listArgs.EntityName, tenantID)
}
return nil
})
// Inicia o servidor
log.Fatal(server.Start())
}
Métodos de Identificação de Tenant
1. Header (Padrão)
# Listar produtos do tenant padrão
curl -X GET "http://localhost:8080/api/odata/Produtos"
# Listar produtos da empresa A
curl -X GET "http://localhost:8080/api/odata/Produtos" \
-H "X-Tenant-ID: empresa_a"
2. Subdomain
Configure TENANT_IDENTIFICATION_MODE=subdomain:
# Acesso via subdomain
curl -X GET "http://empresa_a.localhost:8080/api/odata/Produtos"
3. Path
Configure TENANT_IDENTIFICATION_MODE=path:
# Acesso via path
curl -X GET "http://localhost:8080/api/empresa_a/odata/Produtos"
4. JWT Token
Configure TENANT_IDENTIFICATION_MODE=jwt e inclua claim tenant_id:
# Acesso via JWT (com claim tenant_id)
curl -X GET "http://localhost:8080/api/odata/Produtos" \
-H "Authorization: Bearer <jwt_token_com_tenant_id>"
Endpoints de Gerenciamento Multi-Tenant
Listar Tenants
GET /tenants
Resposta:
{
"multi_tenant": true,
"tenants": ["default", "empresa_a", "empresa_b", "empresa_c"],
"total_count": 4
}
Estatísticas dos Tenants
GET /tenants/stats
Resposta:
{
"total_tenants": 3,
"tenants": {
"empresa_a": {
"tenant_id": "empresa_a",
"exists": true,
"provider_type": "*oracle.OracleProvider",
"open_connections": 5,
"in_use": 2,
"idle": 3
}
}
}
Health Check por Tenant
GET /tenants/empresa_a/health
Resposta:
{
"tenant_id": "empresa_a",
"status": "healthy",
"connection_stats": {
"open_connections": 5,
"in_use": 2,
"idle": 3
}
}
Entidades Multi-Tenant
As entidades incluem automaticamente o campo tenant_id para isolamento:
type Produto struct {
ID int64 `json:"id" db:"id" odata:"key"`
Nome string `json:"nome" db:"nome"`
Descricao string `json:"descricao" db:"descricao"`
Preco float64 `json:"preco" db:"preco"`
Categoria string `json:"categoria" db:"categoria"`
TenantID string `json:"tenant_id" db:"tenant_id"`
}
type Cliente struct {
ID int64 `json:"id" db:"id" odata:"key"`
Nome string `json:"nome" db:"nome"`
Email string `json:"email" db:"email"`
Telefone string `json:"telefone" db:"telefone"`
TenantID string `json:"tenant_id" db:"tenant_id"`
}
Adicionando Novos Tenants
Para adicionar um novo tenant, basta incluir no .env:
TENANT_NOVO_CLIENTE_DB_DRIVER=mysql
TENANT_NOVO_CLIENTE_DB_HOST=mysql.novocliente.com
TENANT_NOVO_CLIENTE_DB_PORT=3306
TENANT_NOVO_CLIENTE_DB_NAME=novo_cliente
TENANT_NOVO_CLIENTE_DB_USER=user
TENANT_NOVO_CLIENTE_DB_PASSWORD=password
E reiniciar o servidor. O tenant será automaticamente detectado e configurado.
Vantagens do Multi-Tenant
- Isolamento de dados: Cada tenant tem seu próprio banco de dados
- Escalabilidade: Adição dinâmica de novos tenants
- Flexibilidade: Diferentes tipos de banco por tenant
- Monitoramento: Estatísticas individuais por tenant
- Segurança: Isolamento completo entre tenants
- Performance: Pool de conexões otimizado por tenant
Considerações de Segurança
- Validação de tenant: Sempre valide se o tenant existe
- Autenticação: Use JWT com claim
tenant_idpara maior segurança - Auditoria: Todos os acessos são logados com tenant ID
- Isolamento: Dados são completamente isolados por tenant
Exemplo Completo
Veja o exemplo completo em examples/multi_tenant/ que demonstra:
- Configuração completa multi-tenant
- Entidades com isolamento por tenant
- Múltiplos métodos de identificação
- Endpoints de gerenciamento
- Monitoramento e health checks
- Diferentes tipos de banco por tenant
🎯 Eventos de Entidade
O Go-Data oferece um sistema completo de eventos de entidade, permitindo interceptar e customizar operações CRUD através de handlers de eventos. Este sistema é ideal para implementar validações customizadas, auditoria, log de atividades e regras de negócio complexas.
Tipos de Eventos Disponíveis
Eventos de Recuperação
OnEntityGet: Disparado após uma entidade ser recuperada, antes de ser enviada ao clienteOnEntityList: Disparado quando o cliente consulta uma coleção de entidades
Eventos de Inserção
OnEntityInserting: Disparado antes de uma entidade ser inserida (cancelável)OnEntityInserted: Disparado após uma entidade ser inserida
Eventos de Atualização
OnEntityModifying: Disparado antes de uma entidade ser atualizada (cancelável)OnEntityModified: Disparado após uma entidade ser atualizada
Eventos de Exclusão
OnEntityDeleting: Disparado antes de uma entidade ser excluída (cancelável)OnEntityDeleted: Disparado após uma entidade ser excluída
Eventos de Erro
OnEntityError: Disparado quando ocorre um erro durante operações da entidade
Registro de Eventos
Eventos Específicos por Entidade
Os eventos específicos por entidade se aplicam apenas à entidade nomeada. Estão disponíveis os seguintes métodos:
Métodos de Eventos Específicos por Entidade:
OnEntityGet("EntityName", handler)- Disparado após uma entidade específica ser consultadaOnEntityList("EntityName", handler)- Disparado após uma coleção de entidades específica ser consultadaOnEntityInserting("EntityName", handler)- Disparado antes de uma entidade específica ser inseridaOnEntityInserted("EntityName", handler)- Disparado após uma entidade específica ser inseridaOnEntityModifying("EntityName", handler)- Disparado antes de uma entidade específica ser atualizadaOnEntityModified("EntityName", handler)- Disparado após uma entidade específica ser atualizadaOnEntityDeleting("EntityName", handler)- Disparado antes de uma entidade específica ser excluídaOnEntityDeleted("EntityName", handler)- Disparado após uma entidade específica ser excluídaOnEntityError("EntityName", handler)- Disparado quando ocorre erro em uma entidade específica
Exemplos de uso:
// Validação antes da inserção
server.OnEntityInserting("Users", func(args odata.EventArgs) error {
insertArgs := args.(*odata.EntityInsertingArgs)
// Validação customizada
if name, ok := insertArgs.Data["name"].(string); ok && len(name) < 2 {
args.Cancel("Nome deve ter pelo menos 2 caracteres")
return nil
}
// Adicionar timestamps automaticamente
insertArgs.Data["created"] = time.Now()
insertArgs.Data["updated"] = time.Now()
return nil
})
// Ação após inserção
server.OnEntityInserted("Users", func(args odata.EventArgs) error {
insertedArgs := args.(*odata.EntityInsertedArgs)
// Enviar email de boas-vindas
// sendWelcomeEmail(insertedArgs.CreatedEntity)
log.Printf("Usuário criado: %+v", insertedArgs.CreatedEntity)
return nil
})
// Validação antes da atualização
server.OnEntityModifying("Users", func(args odata.EventArgs) error {
modifyArgs := args.(*odata.EntityModifyingArgs)
// Impedir alteração de email por usuários não-admin
if _, emailChanged := modifyArgs.Data["email"]; emailChanged {
if !isCurrentUserAdmin(modifyArgs.GetContext()) {
args.Cancel("Apenas administradores podem alterar email")
return nil
}
}
// Atualizar timestamp
modifyArgs.Data["updated"] = time.Now()
return nil
})
// Controle de acesso para exclusão
server.OnEntityDeleting("Users", func(args odata.EventArgs) error {
deleteArgs := args.(*odata.EntityDeletingArgs)
// Impedir exclusão se usuário tem dependências
if hasUserDependencies(deleteArgs.Keys) {
args.Cancel("Não é possível excluir usuário com dependências")
return nil
}
return nil
})
// Ação após exclusão
server.OnEntityDeleted("Users", func(args odata.EventArgs) error {
deletedArgs := args.(*odata.EntityDeletedArgs)
// Limpar dados relacionados
// cleanupRelatedData(deletedArgs.Keys)
log.Printf("Usuário excluído: %+v", deletedArgs.Keys)
return nil
})
// Ação após atualização
server.OnEntityModified("Users", func(args odata.EventArgs) error {
modifiedArgs := args.(*odata.EntityModifiedArgs)
// Invalidar cache
// invalidateUserCache(modifiedArgs.Keys)
log.Printf("Usuário atualizado: %+v", modifiedArgs.UpdatedEntity)
return nil
})
// Auditoria de consultas específicas
server.OnEntityGet("Users", func(args odata.EventArgs) error {
getArgs := args.(*odata.EntityGetArgs)
// Log de acesso
log.Printf("Usuário consultado: %+v", getArgs.Keys)
// Contabilizar acesso
// trackUserAccess(getArgs.Keys)
return nil
})
// Auditoria de listagens específicas
server.OnEntityList("Users", func(args odata.EventArgs) error {
listArgs := args.(*odata.EntityListArgs)
// Log de listagem
log.Printf("Lista de usuários consultada: %d resultados", len(listArgs.Results))
// Aplicar filtros adicionais baseados no usuário
// applyUserFilters(listArgs)
return nil
})
// Tratamento de erros específicos
server.OnEntityError("Users", func(args odata.EventArgs) error {
errorArgs := args.(*odata.EntityErrorArgs)
// Log específico para erros de usuário
log.Printf("Erro na entidade Users: %v", errorArgs.Error)
// Enviar notificação específica
// sendUserErrorNotification(errorArgs.Error)
return nil
})
Eventos Globais
Os eventos globais se aplicam a todas as entidades registradas no servidor. Estão disponíveis os seguintes métodos:
Métodos de Eventos Globais:
OnEntityGetGlobal()- Disparado após qualquer entidade ser consultadaOnEntityListGlobal()- Disparado após qualquer coleção de entidades ser consultadaOnEntityInsertingGlobal()- Disparado antes de qualquer entidade ser inseridaOnEntityInsertedGlobal()- Disparado após qualquer entidade ser inseridaOnEntityModifyingGlobal()- Disparado antes de qualquer entidade ser atualizadaOnEntityModifiedGlobal()- Disparado após qualquer entidade ser atualizadaOnEntityDeletingGlobal()- Disparado antes de qualquer entidade ser excluídaOnEntityDeletedGlobal()- Disparado após qualquer entidade ser excluídaOnEntityErrorGlobal()- Disparado quando ocorre erro em qualquer entidade
Exemplos de uso:
// Auditoria global para todas as inserções
server.OnEntityInsertingGlobal(func(args odata.EventArgs) error {
log.Printf("Inserindo entidade: %s por usuário: %s",
args.GetEntityName(),
args.GetContext().UserID)
// Registrar auditoria
// auditLog.Record("INSERT", args.GetEntityName(), args.GetContext().UserID)
return nil
})
// Log de todas as modificações
server.OnEntityModifyingGlobal(func(args odata.EventArgs) error {
log.Printf("Modificando entidade: %s", args.GetEntityName())
return nil
})
// Tratamento global de erros
server.OnEntityErrorGlobal(func(args odata.EventArgs) error {
errorArgs := args.(*odata.EntityErrorArgs)
log.Printf("Erro na entidade %s: %v",
args.GetEntityName(),
errorArgs.Error)
// Enviar notificação ou alerta
// errorNotification.Send(errorArgs.Error, errorArgs.Operation)
return nil
})
// Auditoria global para todas as consultas
server.OnEntityGetGlobal(func(args odata.EventArgs) error {
log.Printf("Entidade acessada: %s", args.GetEntityName())
return nil
})
// Auditoria global para todas as listagens
server.OnEntityListGlobal(func(args odata.EventArgs) error {
log.Printf("Lista de entidades acessada: %s", args.GetEntityName())
return nil
})
// Auditoria global para todas as exclusões (antes)
server.OnEntityDeletingGlobal(func(args odata.EventArgs) error {
log.Printf("Excluindo entidade: %s", args.GetEntityName())
return nil
})
Argumentos dos Eventos
EntityInsertingArgs
type EntityInsertingArgs struct {
Data map[string]interface{} // Dados sendo inseridos
ValidationErrors []string // Erros de validação
// Cancelável: true
}
EntityInsertedArgs
type EntityInsertedArgs struct {
CreatedEntity interface{} // Entidade criada
NewID interface{} // ID da nova entidade
// Cancelável: false
}
EntityModifyingArgs
type EntityModifyingArgs struct {
Keys map[string]interface{} // Chaves da entidade
Data map[string]interface{} // Dados sendo atualizados
OriginalEntity interface{} // Entidade original
ValidationErrors []string // Erros de validação
// Cancelável: true
}
EntityGetArgs
type EntityGetArgs struct {
Keys map[string]interface{} // Chaves da entidade
QueryParams map[string]interface{} // Parâmetros da consulta
// Cancelável: false
}
EntityListArgs
type EntityListArgs struct {
QueryOptions QueryOptions // Opções da consulta OData
Results []interface{} // Resultados da consulta
TotalCount int64 // Total de registros
CustomFilters map[string]interface{} // Filtros customizados
// Cancelável: true
}
EntityModifiedArgs
type EntityModifiedArgs struct {
Keys map[string]interface{} // Chaves da entidade
UpdatedEntity interface{} // Entidade atualizada
OriginalEntity interface{} // Entidade original
// Cancelável: false
}
EntityDeletingArgs
type EntityDeletingArgs struct {
Keys map[string]interface{} // Chaves da entidade
EntityToDelete interface{} // Entidade a ser excluída
ValidationErrors []string // Erros de validação
// Cancelável: true
}
EntityDeletedArgs
type EntityDeletedArgs struct {
Keys map[string]interface{} // Chaves da entidade excluída
DeletedEntity interface{} // Entidade excluída
// Cancelável: false
}
EntityErrorArgs
type EntityErrorArgs struct {
Error error // Erro ocorrido
Operation string // Operação que causou o erro
Keys map[string]interface{} // Chaves da entidade (se disponível)
Data interface{} // Dados relacionados ao erro
// Cancelável: false
}
Contexto dos Eventos
Todos os eventos recebem um contexto rico com informações sobre a requisição:
type EventContext struct {
Context context.Context // Contexto da requisição
FiberContext fiber.Ctx // Contexto do Fiber
EntityName string // Nome da entidade
EntityType string // Tipo da entidade
UserID string // ID do usuário atual
UserRoles []string // Roles do usuário
UserScopes []string // Scopes do usuário
RequestID string // ID da requisição
Timestamp int64 // Timestamp do evento
Extra map[string]interface{} // Dados extras
}
Cancelamento de Eventos
Alguns eventos podem ser cancelados para impedir a operação:
server.OnEntityInserting("Products", func(args odata.EventArgs) error {
insertArgs := args.(*odata.EntityInsertingArgs)
// Verificar se pode cancelar
if args.CanCancel() {
if price, ok := insertArgs.Data["price"].(float64); ok && price < 0 {
args.Cancel("Preço não pode ser negativo")
return nil
}
}
return nil
})
Exemplo Prático: Sistema de Auditoria
type AuditLog struct {
ID int64 `json:"id"`
Entity string `json:"entity"`
Operation string `json:"operation"`
UserID string `json:"user_id"`
Data string `json:"data"`
Timestamp time.Time `json:"timestamp"`
}
func setupAuditEvents(server *odata.Server) {
// Registrar todas as inserções
server.OnEntityInsertedGlobal(func(args odata.EventArgs) error {
return recordAudit("INSERT", args)
})
// Registrar todas as atualizações
server.OnEntityModifiedGlobal(func(args odata.EventArgs) error {
return recordAudit("UPDATE", args)
})
// Registrar todas as exclusões
server.OnEntityDeletedGlobal(func(args odata.EventArgs) error {
return recordAudit("DELETE", args)
})
}
func recordAudit(operation string, args odata.EventArgs) error {
audit := AuditLog{
Entity: args.GetEntityName(),
Operation: operation,
UserID: args.GetContext().UserID,
Data: fmt.Sprintf("%+v", args.GetEntity()),
Timestamp: time.Now(),
}
// Salvar no banco de dados
// auditService.Save(audit)
return nil
}
Exemplo Prático: Validação Avançada
func setupValidationEvents(server *odata.Server) {
// Validação de usuários
server.OnEntityInserting("Users", func(args odata.EventArgs) error {
insertArgs := args.(*odata.EntityInsertingArgs)
// Validações customizadas
if err := validateUser(insertArgs.Data); err != nil {
args.Cancel(err.Error())
return nil
}
return nil
})
// Validação de produtos
server.OnEntityInserting("Products", func(args odata.EventArgs) error {
insertArgs := args.(*odata.EntityInsertingArgs)
// Verificar se categoria existe
if categoryID, ok := insertArgs.Data["category_id"].(int64); ok {
if !categoryExists(categoryID) {
args.Cancel("Categoria não encontrada")
return nil
}
}
return nil
})
}
func validateUser(data map[string]interface{}) error {
// Validar email único
if email, ok := data["email"].(string); ok {
if emailExists(email) {
return fmt.Errorf("Email já está em uso")
}
}
// Validar idade
if age, ok := data["age"].(int64); ok && age < 18 {
return fmt.Errorf("Idade deve ser maior que 18 anos")
}
return nil
}
Gerenciamento de Eventos
// Obter número de handlers registrados
count := server.GetEventManager().GetHandlerCount(odata.EventEntityInserting, "Users")
// Listar todas as assinaturas
subscriptions := server.GetEventManager().ListSubscriptions()
// Limpar handlers de uma entidade específica
server.GetEventManager().ClearEntity("Users")
// Limpar todos os handlers
server.GetEventManager().Clear()
Resumo dos Métodos de Eventos
Eventos Específicos por Entidade:
server.OnEntityGet("EntityName", handler) // Após consulta individual
server.OnEntityList("EntityName", handler) // Após consulta de coleção
server.OnEntityInserting("EntityName", handler) // Antes de inserção (cancelável)
server.OnEntityInserted("EntityName", handler) // Após inserção
server.OnEntityModifying("EntityName", handler) // Antes de atualização (cancelável)
server.OnEntityModified("EntityName", handler) // Após atualização
server.OnEntityDeleting("EntityName", handler) // Antes de exclusão (cancelável)
server.OnEntityDeleted("EntityName", handler) // Após exclusão
server.OnEntityError("EntityName", handler) // Quando ocorre erro
Eventos Globais:
server.OnEntityGetGlobal(handler) // Após qualquer consulta individual
server.OnEntityListGlobal(handler) // Após qualquer consulta de coleção
server.OnEntityInsertingGlobal(handler) // Antes de qualquer inserção (cancelável)
server.OnEntityInsertedGlobal(handler) // Após qualquer inserção
server.OnEntityModifyingGlobal(handler) // Antes de qualquer atualização (cancelável)
server.OnEntityModifiedGlobal(handler) // Após qualquer atualização
server.OnEntityDeletingGlobal(handler) // Antes de qualquer exclusão (cancelável)
server.OnEntityDeletedGlobal(handler) // Após qualquer exclusão
server.OnEntityErrorGlobal(handler) // Quando ocorre qualquer erro
Exemplo Completo
Veja o exemplo completo em examples/events/ que demonstra:
- Configuração completa de eventos
- Validações customizadas
- Sistema de auditoria
- Controle de acesso baseado em contexto
- Tratamento de erros
- Cancelamento de operações
🗄️ ObjectManager (ORM)
O Go-Data inclui um ObjectManager completo, similar ao TObjectManager do Aurelius/XData, oferecendo funcionalidades ORM avançadas para manipulação de entidades. Este componente implementa padrões como Identity Mapping, Change Tracking e Cached Updates.
Características Principais
- ✅ Identity Mapping: Cache automático de entidades já carregadas
- ✅ Change Tracking: Detecção automática de modificações
- ✅ Cached Updates: Agrupa operações para execução em lote
- ✅ Transações: Gerenciamento completo de transações
- ✅ Batching: Otimização de operações em massa
- ✅ Integração com Eventos: Acesso transparente via
EventContext
Criando um ObjectManager
Dentro de Eventos (Recomendado)
O ObjectManager está disponível automaticamente no contexto de eventos:
server.OnEntityInserting("Orders", func(args odata.EventArgs) error {
// Obtém o ObjectManager do contexto do evento
manager := args.Manager()
// Agora você pode usar todas as funcionalidades do ORM
product, err := manager.Find("Products", "123")
if err != nil {
return err
}
return nil
})
Manualmente
Para uso fora de eventos:
import "context"
// Obtém o provider do servidor
provider := server.GetProvider()
// Cria um ObjectManager
ctx := context.Background()
manager := odata.NewObjectManager(provider, ctx)
// Ou a partir de um EventContext
manager := odata.CreateFromEventContext(eventCtx)
Operações CRUD Básicas
Find - Buscar Entidade
Busca uma entidade por ID, primeiro no cache, depois no banco:
// Busca no cache e depois no banco
user, err := manager.Find("Users", "42")
if err != nil {
return err
}
// Busca apenas no cache (não toca o banco)
cachedUser, err := manager.FindCached("Users", "42")
Save - Inserir Nova Entidade
Insere uma nova entidade no banco de dados:
newUser := map[string]interface{}{
"name": "João Silva",
"email": "[email protected]",
"age": 30,
}
err := manager.Save(newUser)
if err != nil {
return err
}
// A entidade é automaticamente adicionada ao cache
// e marcada como "attached" ao manager
Update - Atualizar Entidade
Marca uma entidade para atualização:
// Busca a entidade
user, err := manager.Find("Users", "42")
if err != nil {
return err
}
// Modifica os dados
userData := user.(map[string]interface{})
userData["email"] = "[email protected]"
// Marca para atualização
err = manager.Update(user)
// Persiste as mudanças
err = manager.Flush(user)
Remove - Excluir Entidade
Remove uma entidade do banco de dados:
// Busca a entidade
user, err := manager.Find("Users", "42")
if err != nil {
return err
}
// Remove do banco
err = manager.Remove(user)
if err != nil {
return err
}
SaveOrUpdate - Inteligente
Salva se for nova ou atualiza se já existir:
user := map[string]interface{}{
"id": 42, // Se tem ID, atualiza
"name": "João Silva",
"email": "[email protected]",
}
err := manager.SaveOrUpdate(user)
Identity Mapping & Cache
O ObjectManager mantém um cache de entidades para evitar buscas duplicadas:
// Primeira busca: vai ao banco
user1, _ := manager.Find("Users", "42")
// Segunda busca: retorna do cache
user2, _ := manager.Find("Users", "42")
// user1 e user2 são a mesma instância!
Gerenciamento de Cache:
// Verifica se está no cache
exists := manager.IsCached("Users", "42")
// Verifica se está attached ao manager
isAttached := manager.IsAttached(user)
// Remove do cache
manager.Evict(user)
// Limpa todo o cache
manager.ClearCache()
Change Tracking
O ObjectManager rastreia modificações nas entidades:
// Busca a entidade
user, _ := manager.Find("Users", "42")
// Modifica
userData := user.(map[string]interface{})
userData["email"] = "[email protected]"
// Marca como modificada
manager.Update(user)
// Verifica se tem mudanças
hasChanges := manager.HasChanges(user) // true
// Verifica se há alguma mudança pendente
anyChanges := manager.HasAnyChanges() // true
// Obtém todas as entidades modificadas
changed := manager.GetChangedObjects()
Cached Updates (Operações em Lote)
Para melhor performance, você pode habilitar o modo Cached Updates que agrupa operações:
// Habilita cached updates
manager.SetCachedUpdates(true)
// Configura tamanho do batch
manager.SetBatchSize(100)
// Todas as operações são armazenadas em memória
manager.Save(user1)
manager.Save(user2)
manager.Update(user3)
manager.Remove(user4)
// Verifica quantas operações estão pendentes
count := manager.GetCachedCount() // 4
// Executa todas as operações de uma vez (em batch otimizado)
err := manager.ApplyCachedUpdates()
if err != nil {
// Se falhar, nenhuma operação é aplicada
return err
}
// Desabilita cached updates
manager.SetCachedUpdates(false)
Gerenciamento de Transações
O ObjectManager oferece controle completo de transações:
Transação Manual
// Inicia transação
tx, err := manager.BeginTransaction()
if err != nil {
return err
}
// Executa operações
manager.Save(entity1)
manager.Update(entity2)
// Commit ou Rollback
if erro {
manager.RollbackTransaction(tx)
} else {
manager.CommitTransaction(tx)
}
Transação Automática (Recomendado)
err := manager.WithTransaction(func(tx *odata.TxManager) error {
// Executa operações dentro da transação
manager.Save(entity1)
manager.Update(entity2)
manager.Remove(entity3)
// Se retornar erro, rollback automático
if algumErro {
return fmt.Errorf("operação falhou")
}
// Se retornar nil, commit automático
return nil
})
if err != nil {
log.Printf("Transação falhou: %v", err)
}
Merge - Sincronizar Entidade Detached
O método Merge permite atualizar uma entidade que foi desanexada do manager:
// Entidade vinda de outra fonte (ex: JSON do cliente)
detachedUser := map[string]interface{}{
"id": 42,
"name": "Nome Atualizado",
"email": "[email protected]",
}
// Merge com a entidade no cache/banco
mergedUser, err := manager.Merge(detachedUser)
if err != nil {
return err
}
// A entidade no cache foi atualizada
// e está marcada como modificada
Flush - Persistir Mudanças
// Flush de uma entidade específica
err := manager.Flush(user)
// Flush de todas as mudanças pendentes
err := manager.FlushAll()
Consultas Customizadas
Para queries complexas, você pode executar SQL diretamente:
// Executa query customizada
query := "SELECT * FROM users WHERE age > ?"
rows, err := manager.ExecuteQuery(query, 18)
if err != nil {
return err
}
defer rows.Close()
// Processa resultados
for rows.Next() {
// ...
}
// Executa query dentro de transação
tx, _ := manager.BeginTransaction()
rows, err := manager.ExecuteQueryTransaction(tx, query, 18)
Integração com Eventos
O ObjectManager se integra perfeitamente com o sistema de eventos:
server.OnEntityInserting("Orders", func(args odata.EventArgs) error {
// Obtém ObjectManager do contexto
manager := args.Manager()
insertArgs := args.(*odata.EntityInsertingArgs)
productID := insertArgs.Data["product_id"]
// Busca produto relacionado
product, err := manager.Find("Products", fmt.Sprintf("%v", productID))
if err != nil {
args.Cancel("Produto não encontrado")
return nil
}
// Verifica estoque
productData := product.(map[string]interface{})
stock := productData["stock"].(int64)
quantity := insertArgs.Data["quantity"].(int64)
if stock < quantity {
args.Cancel("Estoque insuficiente")
return nil
}
// Atualiza estoque
productData["stock"] = stock - quantity
manager.Update(product)
manager.Flush(product)
return nil
})
Exemplo Completo: Sistema de Pedidos
func ProcessOrder(args odata.EventArgs) error {
manager := args.Manager()
insertArgs := args.(*odata.EntityInsertingArgs)
// Inicia transação
return manager.WithTransaction(func(tx *odata.TxManager) error {
// 1. Busca o produto
productID := insertArgs.Data["product_id"]
product, err := manager.Find("Products", fmt.Sprintf("%v", productID))
if err != nil {
return fmt.Errorf("produto não encontrado: %w", err)
}
// 2. Verifica estoque
productData := product.(map[string]interface{})
stock := productData["stock"].(int64)
quantity := insertArgs.Data["quantity"].(int64)
if stock < quantity {
return fmt.Errorf("estoque insuficiente")
}
// 3. Atualiza estoque
productData["stock"] = stock - quantity
manager.Update(product)
// 4. Cria entrada de histórico
history := map[string]interface{}{
"product_id": productID,
"quantity": -quantity,
"reason": "VENDA",
"date": time.Now(),
}
manager.Save(history)
// 5. Aplica mudanças
manager.Flush(product)
// Se tudo OK, commit automático
// Se erro, rollback automático
return nil
})
}
// Registra o evento
server.OnEntityInserting("Orders", ProcessOrder)
Comparação com Aurelius/XData
| Aurelius/XData | Go-Data ObjectManager |
|---|---|
TObjectManager |
ObjectManager |
Find<T>(id) |
Find(entityName, id) |
Save(entity) |
Save(entity) |
Update(entity) |
Update(entity) |
Remove(entity) |
Remove(entity) |
Merge(entity) |
Merge(entity) |
Flush |
Flush(entity) / FlushAll() |
BeginTransaction |
BeginTransaction() |
CommitTransaction |
CommitTransaction(tx) |
RollbackTransaction |
RollbackTransaction(tx) |
IsCached(entity) |
IsCached(name, id) |
IsAttached(entity) |
IsAttached(entity) |
Evict(entity) |
Evict(entity) |
ClearCache() |
ClearCache() |
Melhores Práticas
- Use dentro de Eventos: O ObjectManager é ideal para uso dentro de eventos
- Habilite Cached Updates para Bulk: Para muitas operações, use cached updates
- Sempre use Transações: Para operações críticas, envolva em transações
- Aproveite o Cache: O identity mapping evita queries duplicadas
- Flush Explícito: Para cached updates, não esqueça de chamar
FlushAll()
Performance
O ObjectManager oferece otimizações importantes:
- Identity Mapping: Elimina queries duplicadas
- Batching: Agrupa operações INSERT/UPDATE/DELETE
- Change Tracking: Apenas persiste o que foi modificado
- Cache Local: Reduz round-trips ao banco de dados
🎯 Service Operations
O Go-Data implementa Service Operations similares ao XData, mas usando padrões idiomáticos do Go. O sistema oferece um ServiceContext otimizado que equivale funcionalmente ao TXDataOperationContext do XData.
Características do Service Operations
- ✅ ServiceContext Otimizado: Equivale ao
TXDataOperationContext.Current.GetManager()do XData - ✅ Sintaxe Simples: Similar ao Fiber para registro de handlers
- ✅ Autenticação Flexível: Controle automático baseado na configuração JWT
- ✅ Multi-Tenant: Suporte automático a multi-tenant
- ✅ ObjectManager Integrado: Acesso direto ao ObjectManager do contexto
- ✅ Menos Boilerplate: 95% menos código que implementações tradicionais
ServiceContext
type ServiceContext struct {
Manager *ObjectManager // Equivale ao TXDataOperationContext.Current.GetManager()
FiberContext fiber.Ctx // Contexto do Fiber (já tem TenantID via GetCurrentTenant())
User *UserIdentity // Usuário autenticado (só se JWT habilitado)
}
Registro de Services
Service Sem Autenticação
server.Service("GET", "/Service/GetTopSelling", func(ctx *odata.ServiceContext) error {
products, err := ctx.GetManager().Query("Products").
Where("sales_count gt 100").
OrderBy("sales_count desc").
Top(10).
List()
if err != nil {
return ctx.Status(500).JSON(map[string]string{"error": err.Error()})
}
return ctx.JSON(map[string]interface{}{
"products": products,
"tenant": ctx.GetTenantID(),
})
})
Service Com Autenticação
server.ServiceWithAuth("POST", "/Service/CalculateTotal", func(ctx *odata.ServiceContext) error {
// ctx.User garantidamente não será nil se JWT habilitado
productIDs := ctx.Query("product_ids")
manager := ctx.GetManager()
// ... lógica do service
return ctx.JSON(result)
}, true)
Service Com Roles
server.ServiceWithRoles("GET", "/Service/AdminData", func(ctx *odata.ServiceContext) error {
// ctx.User garantidamente tem role "admin"
manager := ctx.GetManager()
// ... lógica administrativa
return ctx.JSON(data)
}, "admin")
Service Groups
products := server.ServiceGroup("Products")
products.ServiceWithAuth("GET", "GetTopSelling", func(ctx *odata.ServiceContext) error {
// Handler implementation
return ctx.JSON(result)
}, true)
products.ServiceWithRoles("GET", "AdminStats", func(ctx *odata.ServiceContext) error {
// Handler implementation
return ctx.JSON(result)
}, "admin")
Métodos do ServiceContext
// Acesso ao ObjectManager (equivale ao XData)
manager := ctx.GetManager()
// Informações do usuário
user := ctx.GetUser()
tenantID := ctx.GetTenantID()
// Verificações de autenticação
isAuth := ctx.IsAuthenticated()
isAdmin := ctx.IsAdmin()
hasRole := ctx.HasRole("manager")
// Acesso aos dados da requisição
params := ctx.QueryParams()
productID := ctx.Query("product_id")
body := ctx.Body()
// Resposta
ctx.JSON(data)
ctx.Status(200).JSON(data)
ctx.SetHeader("Content-Type", "application/json")
Comparação com XData
| Funcionalidade XData | Go-Data ServiceContext |
|---|---|
TXDataOperationContext.Current.GetManager() |
ctx.GetManager() |
TXDataOperationContext.Current.Request |
ctx.FiberContext |
TXDataOperationContext.Current.Response |
ctx.FiberContext |
| Service Contract Interface | ServiceHandler function |
| Service Implementation | Handler function direta |
| Routing automático | server.Service(method, endpoint, handler) |
| Memory management | ObjectManager automático |
| ~20 linhas de setup | ~3 linhas de setup |
Exemplo Completo
Veja o exemplo completo em examples/service_operations/ que demonstra:
- ServiceContext otimizado com ObjectManager integrado
- Acesso direto a Connection, Provider e Pool
- Criação de múltiplos ObjectManagers isolados
- Sintaxe simples similar ao Fiber para registro
- Controle automático de autenticação baseado em JWT
- Suporte completo a multi-tenant
- Service Groups para organização
- Equivalência funcional ao TXDataOperationContext do XData
🗂️ Mapeamento de Entidades
Tags Disponíveis
Tag table
TableName string `table:"users;schema=public"`
Tag prop
Nome string `prop:"[required]; length:100"`
Email string `prop:"[required, Unique]; length:255"`
DtInc time.Time `prop:"[required, NoUpdate]; default"`
Tag primaryKey
ID int64 `primaryKey:"idGenerator:sequence;name=seq_user_id"`
Tag association (N:1)
User *User `association:"foreignKey:user_id; references:id"`
Tag manyAssociation (1:N)
Orders []Order `manyAssociation:"foreignKey:user_id; references:id"`
Tag cascade
Orders []Order `cascade:"[SaveUpdate, Remove, Refresh]"`
Tipos Nullable
import "github.com/fitlcarlos/go-data/odata"
type User struct {
ID int64 `json:"id"`
Nome string `json:"nome"`
Idade nullable.Int64 `json:"idade"` // Pode ser null
Salario nullable.Float64 `json:"salario"` // Pode ser null
DtAlt nullable.Time `json:"dt_alt"` // Pode ser null
}
🛤️ Rotas Customizadas
O Go-Data simplifica o registro de rotas customizadas (não-OData) aplicando automaticamente o prefixo de rota e garantindo que todos os context helpers estejam disponíveis.
API Simplificada
func main() {
server := odata.NewServer()
// ✅ Rotas customizadas com prefixo automático
server.Post("/auth/login", Login)
server.Post("/auth/refresh", Refresh)
server.Get("/health", HealthCheck)
// Rotas finais: /api/v1/auth/login, /api/v1/auth/refresh, /api/v1/health
// (assumindo SERVER_ROUTE_PREFIX=/api/v1 no .env)
server.Start()
}
Context Helpers Disponíveis
Todas as rotas customizadas têm acesso aos mesmos helpers que as rotas OData:
func Login(c fiber.Ctx) error {
// ✅ Acesso à conexão SQL
conn := odata.GetConnection(c)
if conn == nil {
return c.Status(500).JSON(fiber.Map{"error": "Banco não disponível"})
}
// ✅ Acesso ao DatabaseProvider
provider := odata.GetProvider(c)
// ✅ Acesso ao ObjectManager (ORM)
manager := odata.GetObjectManager(c)
// ✅ Criar novo ObjectManager
newManager := odata.CreateObjectManager(c)
// ✅ Acesso ao pool multi-tenant (se habilitado)
pool := odata.GetConnectionPool(c)
// Usar conexão normalmente
var user User
err := conn.QueryRow("SELECT * FROM users WHERE email = $1", email).Scan(...)
return c.JSON(fiber.Map{"token": "..."})
}
Métodos Disponíveis
// HTTP Methods com prefixo automático
server.Get(path, handlers...) // GET request
server.Post(path, handlers...) // POST request
server.Put(path, handlers...) // PUT request
server.Delete(path, handlers...) // DELETE request
server.Patch(path, handlers...) // PATCH request
server.Head(path, handlers...) // HEAD request
server.Options(path, handlers...) // OPTIONS request
server.All(path, handlers...) // ALL methods
// Custom methods
server.Add([]string{"GET", "POST"}, path, handlers...)
Middlewares Customizados
Você pode adicionar middlewares às rotas customizadas:
// Middleware de exemplo
func LogMiddleware(c fiber.Ctx) error {
log.Printf("Request: %s %s", c.Method(), c.Path())
return c.Next()
}
// Aplicar middleware em rota específica
server.Post("/auth/login", LogMiddleware, Login)
// Aplicar middleware em todas as rotas
server.Use(LogMiddleware)
Exemplo Completo: Sistema de Autenticação
package main
import (
"github.com/fitlcarlos/go-data/odata"
"github.com/gofiber/fiber/v3"
)
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
func Login(c fiber.Ctx) error {
var req LoginRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Dados inválidos"})
}
// ✅ Usar context helper para acessar banco
conn := odata.GetConnection(c)
if conn == nil {
return c.Status(500).JSON(fiber.Map{"error": "Banco não disponível"})
}
// Buscar usuário
var userID int
var passwordHash string
err := conn.QueryRow(`
SELECT id, password_hash
FROM users WHERE email = $1
`, req.Username).Scan(&userID, &passwordHash)
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "Credenciais inválidas"})
}
// Validar senha (use bcrypt em produção)
// ...
// Gerar JWT
accessToken, _ := odata.GenerateJWT(map[string]interface{}{
"user_id": userID,
"email": req.Username,
})
return c.JSON(fiber.Map{
"access_token": accessToken,
"token_type": "Bearer",
})
}
func main() {
server := odata.NewServer()
// ✅ Rotas customizadas simplificadas
server.Post("/auth/login", Login)
// Entidades protegidas com JWT
jwtMiddleware := server.NewRouterJWTAuth()
server.RegisterEntity("Users", User{}, odata.WithMiddleware(jwtMiddleware))
server.Start()
}
Comparação: Antes vs Depois
Antes (Complexo):
router := server.GetRouter()
prefix := server.GetConfig().RoutePrefix
dbMiddleware := server.DatabaseMiddleware()
router.Post(prefix+"/auth/login", dbMiddleware, Login)
Depois (Simples):
server.Post("/auth/login", Login) // Tudo automático!
Vantagens
- ✅ Prefixo Automático: Aplicado automaticamente baseado em
SERVER_ROUTE_PREFIX - ✅ Context Completo: Todos os helpers (Connection, Provider, ObjectManager) disponíveis
- ✅ Middlewares Globais: Aplicados automaticamente (DatabaseMiddleware, RateLimiter, etc.)
- ✅ API Consistente: Mesma experiência das rotas OData
- ✅ Menos Código: Não precisa manipular router, prefixo ou middlewares manualmente
Endpoint de Diagnóstico
Para verificar se todos os context helpers estão funcionando, crie um endpoint de teste:
func TestContextHelpers(c fiber.Ctx) error {
result := fiber.Map{
"tests": fiber.Map{
"GetConnection": fiber.Map{
"available": odata.GetConnection(c) != nil,
},
"GetProvider": fiber.Map{
"available": odata.GetProvider(c) != nil,
},
"GetObjectManager": fiber.Map{
"available": odata.GetObjectManager(c) != nil,
},
"CreateObjectManager": fiber.Map{
"available": odata.CreateObjectManager(c) != nil,
},
"GetConnectionPool": fiber.Map{
"available": odata.GetConnectionPool(c) != nil,
},
},
}
// Testar query se conexão disponível
if conn := odata.GetConnection(c); conn != nil {
var version string
err := conn.QueryRow("SELECT version()").Scan(&version)
result["database_test"] = fiber.Map{
"success": err == nil,
"version": version,
}
}
return c.JSON(result)
}
func main() {
server := odata.NewServer()
server.Get("/test/context", TestContextHelpers)
server.Start()
}
Teste:
curl http://localhost:8080/api/v1/test/context
Se algum helper retornar available: false:
- Verifique se o arquivo
.envexiste e está configurado - Confirme que o banco de dados está acessível
- Veja logs do servidor para mais detalhes
Ver Também
- Exemplo Engage - Sistema completo com autenticação JWT e rotas customizadas (inclui endpoint de diagnóstico)
- Exemplo JWT - JWT básico
- Service Operations - Para lógica de negócio mais complexa
💾 Bancos de Dados Suportados
PostgreSQL
import (
"github.com/fitlcarlos/go-data/odata"
_ "github.com/jackc/pgx/v5/stdlib"
)
db, err := sql.Open("pgx", "postgres://user:password@localhost/database")
provider := odata.NewPostgreSQLProvider(db)
Oracle
import (
"github.com/fitlcarlos/go-data/odata"
_ "github.com/sijms/go-ora/v2"
)
db, err := sql.Open("oracle", "oracle://user:password@localhost:1521/xe")
provider := odata.NewOracleProvider(db)
MySQL
import (
"github.com/fitlcarlos/go-data/odata"
_ "github.com/go-sql-driver/mysql"
)
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/database")
provider := odata.NewMySQLProvider(db)
🌐 Endpoints OData
Service Document
GET /odata/
Metadados
GET /odata/$metadata
Operações CRUD
Listar Entidades
GET /odata/Users
Buscar por ID
GET /odata/Users(1)
Listar Entidades com Multi-Tenant
GET /odata/Users
X-Tenant-ID: empresa_a
Criar Entidade
POST /odata/Users
Content-Type: application/json
{
"nome": "João Silva",
"email": "[email protected]",
"idade": 30
}
Atualizar Entidade
PUT /odata/Users(1)
Content-Type: application/json
{
"nome": "João Santos",
"email": "[email protected]"
}
Atualizar Parcialmente
PATCH /odata/Users(1)
Content-Type: application/json
{
"idade": 32
}
Excluir Entidade
DELETE /odata/Users(1)
🔍 Consultas OData
Filtros ($filter)
GET /odata/Users?$filter=idade gt 25
GET /odata/Users?$filter=nome eq 'João'
GET /odata/Users?$filter=contains(nome, 'Silva')
Filtros com Multi-Tenant
GET /odata/Users?$filter=idade gt 25
X-Tenant-ID: empresa_a
Ordenação ($orderby)
GET /odata/Users?$orderby=nome asc
GET /odata/Users?$orderby=idade desc
GET /odata/Users?$orderby=nome asc,idade desc
Paginação ($top, $skip)
GET /odata/Users?$top=10
GET /odata/Users?$skip=20
GET /odata/Users?$top=10&$skip=20
Seleção de Campos ($select)
GET /odata/Users?$select=nome,email
Expansão de Relacionamentos ($expand)
GET /odata/Users?$expand=Orders
GET /odata/Users?$expand=Orders($filter=total gt 100)
Contagem ($count)
GET /odata/Users?$count=true
GET /odata/Users/$count
Campos Computados ($compute)
GET /odata/Orders?$compute=total mul 0.1 as tax
Busca Textual ($search)
GET /odata/Users?$search=João
Batch ($batch) - OData v4
O OData v4 suporta batch requests, permitindo executar múltiplas operações em uma única requisição HTTP. Isso reduz latência, suporta transações e melhora a performance em operações bulk.
Características:
- Múltiplas operações GET/POST/PUT/PATCH/DELETE em uma requisição
- Changesets transacionais (tudo ou nada)
- Reduz overhead de conexões HTTP
- Suporte a Content-ID para referenciar operações
Exemplo: Múltiplas leituras
POST /odata/$batch
Content-Type: multipart/mixed; boundary=batch_boundary
--batch_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
GET /api/v1/Products?$top=5 HTTP/1.1
Host: localhost:3000
--batch_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
GET /api/v1/Categories HTTP/1.1
Host: localhost:3000
--batch_boundary--
Exemplo: Changeset transacional
POST /odata/$batch
Content-Type: multipart/mixed; boundary=batch_boundary
--batch_boundary
Content-Type: multipart/mixed; boundary=changeset_boundary
--changeset_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1
POST /api/v1/Products HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{"name":"Produto Novo","price":99.90}
--changeset_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 2
POST /api/v1/Orders HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{"product_id": 1, "quantity": 5}
--changeset_boundary--
--batch_boundary--
Exemplo: Batch misto (leitura + changeset)
POST /odata/$batch
Content-Type: multipart/mixed; boundary=batch_boundary
--batch_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
GET /api/v1/Products?$top=3 HTTP/1.1
Host: localhost:3000
--batch_boundary
Content-Type: multipart/mixed; boundary=changeset_boundary
--changeset_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1
POST /api/v1/Categories HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{"name":"Nova Categoria"}
--changeset_boundary--
--batch_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
GET /api/v1/Orders HTTP/1.1
Host: localhost:3000
--batch_boundary--
Configuração do Batch:
O Go-Data oferece configuração flexível para batch requests através do BatchConfig:
// Usar configuração padrão (automática)
server := odata.NewServer()
// Batch habilitado automaticamente com valores padrão
// Ou customizar com BatchConfig
config := &odata.BatchConfig{
// Limites de segurança
MaxOperations: 100, // Máximo de operações por batch (padrão: 100)
MaxChangesets: 10, // Máximo de changesets (padrão: 10)
MaxOperationsPerChangeset: 50, // Máximo de operações por changeset (padrão: 50)
// Controle de tempo
Timeout: 30 * time.Second, // Timeout para todo o batch (padrão: 30s)
OperationTimeout: 5 * time.Second, // Timeout por operação individual (padrão: 5s)
// Transações
EnableTransactions: true, // Habilitar transações para changesets (padrão: true)
IsolationLevel: sql.LevelSerializable, // Nível de isolamento (opcional)
// Validação
ValidateContentID: true, // Validar Content-ID (padrão: true)
StrictMode: false, // Modo estrito (rejeita batch mal-formado)
// Performance
ParallelReads: true, // Executar leituras em paralelo (padrão: false)
MaxParallelReads: 5, // Máximo de leituras paralelas (padrão: 5)
}
// Aplicar configuração no servidor
server.SetBatchConfig(config)
Opções de Configuração Detalhadas:
| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
MaxOperations |
int | 100 | Número máximo de operações no batch |
MaxChangesets |
int | 10 | Número máximo de changesets no batch |
MaxOperationsPerChangeset |
int | 50 | Operações máximas por changeset |
Timeout |
Duration | 30s | Timeout para processar o batch completo |
OperationTimeout |
Duration | 5s | Timeout para cada operação individual |
EnableTransactions |
bool | true | Se changesets devem usar transações |
IsolationLevel |
sql.IsolationLevel | - | Nível de isolamento das transações |
ValidateContentID |
bool | true | Validar unicidade de Content-IDs |
StrictMode |
bool | false | Rejeitar batch com formato incorreto |
ParallelReads |
bool | false | Executar leituras em paralelo |
MaxParallelReads |
int | 5 | Limite de leituras paralelas |
Configurações Predefinidas:
// Desenvolvimento (permissivo)
devConfig := &odata.BatchConfig{
MaxOperations: 200,
MaxChangesets: 20,
Timeout: 60 * time.Second,
StrictMode: false,
ParallelReads: true,
}
// Produção (restritivo)
prodConfig := &odata.BatchConfig{
MaxOperations: 50,
MaxChangesets: 5,
Timeout: 15 * time.Second,
StrictMode: true,
EnableTransactions: true,
ValidateContentID: true,
}
// Performance (otimizado)
perfConfig := &odata.BatchConfig{
MaxOperations: 100,
Timeout: 30 * time.Second,
ParallelReads: true,
MaxParallelReads: 10,
EnableTransactions: true,
}
server.SetBatchConfig(prodConfig)
Benefícios:
- ⚡ Performance: Reduz latência ao combinar múltiplas requisições
- 🔄 Transações: Changesets garantem atomicidade (tudo ou nada)
- 🌐 Rede: Menos overhead de conexões HTTP
- 📊 Bulk: Ideal para operações em lote
Limitações Conhecidas:
⚠️ Importante: A implementação atual do $batch possui as seguintes limitações:
-
Transações por Changeset:
- Cada changeset é executado em uma transação separada
- Não há transação global para múltiplos changesets em um único batch
- Se você precisa de atomicidade entre changesets, use apenas um changeset
-
Content-ID:
- Content-IDs são resolvidos apenas dentro do mesmo changeset
- Referências entre changesets diferentes não são suportadas
- Recomendação: Use Content-IDs sequenciais (1, 2, 3...) para melhor compatibilidade
-
Autenticação:
- A autenticação é aplicada uma vez no batch request
- Todas as operações no batch usam as mesmas credenciais
- Não é possível usar credenciais diferentes para operações individuais
-
Limites de Performance:
MaxOperations: Máximo de 100 operações por batch (configurável)MaxChangesets: Máximo de 10 changesets por batch (configurável)Timeout: 30 segundos por padrão (configurável)- Batches muito grandes podem causar timeouts
-
Tipos de Operações:
- ✅ GET, POST, PUT, PATCH, DELETE suportados
- ❌ $batch aninhado não suportado (batch dentro de batch)
- ❌ Operações assíncronas não implementadas
-
Tratamento de Erros:
- Em changesets: um erro cancela todas as operações do changeset (rollback)
- Fora de changesets: cada operação é independente (erros não afetam outras operações)
- Erros são retornados com status HTTP apropriado na resposta multipart
-
Formato de Resposta:
- Sempre retorna
multipart/mixedconforme OData v4 - A ordem das respostas corresponde à ordem das requisições
- Cada resposta inclui status HTTP e corpo (se aplicável)
- Sempre retorna
-
Compatibilidade:
- Implementado conforme OData v4 specification
- Testado com: Postman, curl, e clientes HTTP padrão
- Algumas ferramentas podem ter dificuldade com multipart/mixed complexo
Recomendações de Uso:
// ✅ BOM: Um changeset transacional
Changeset 1: [POST Product, POST Order, PUT Inventory]
// ✅ BOM: Múltiplas leituras independentes
Request 1: GET /Products
Request 2: GET /Categories
Request 3: GET /Orders
// ⚠️ CUIDADO: Múltiplos changesets (não há transação global)
Changeset 1: [POST Product]
Changeset 2: [POST Order] // Se falhar, Changeset 1 já foi commitado
// ❌ EVITAR: Batch muito grande
100+ operações em um único batch // Pode causar timeout
Roadmap Futuro:
- Transações globais entre changesets
- Content-ID cross-changeset
- Operações assíncronas
- Streaming de respostas
- Batch aninhado
Veja o exemplo completo em examples/batch/main.go.
🔧 Operadores Suportados
Comparação
eq- Igualne- Diferentegt- Maior quege- Maior ou iguallt- Menor quele- Menor ou igual
Funções de String
contains(field, 'value')- Contémstartswith(field, 'value')- Inicia comendswith(field, 'value')- Termina comtolower(field)- Converte para minúsculastoupper(field)- Converte para maiúsculas
Funções Matemáticas
round(field)- Arredondafloor(field)- Arredonda para baixoceiling(field)- Arredonda para cima
Lógicos
and- E lógicoor- Ou lógiconot- Negação
📊 Mapeamento de Tipos
| Tipo Go | Tipo OData | Tipo SQL |
|---|---|---|
string |
Edm.String |
VARCHAR |
int, int32 |
Edm.Int32 |
INT |
int64 |
Edm.Int64 |
BIGINT |
float32 |
Edm.Single |
FLOAT |
float64 |
Edm.Double |
DOUBLE |
bool |
Edm.Boolean |
BOOLEAN |
time.Time |
Edm.DateTimeOffset |
TIMESTAMP |
nullable.Int64 |
Edm.Int64 |
BIGINT NULL |
nullable.String |
Edm.String |
VARCHAR NULL |
nullable.Time |
Edm.DateTimeOffset |
TIMESTAMP NULL |
🔧 Execução como Serviço
O GoData possui funcionalidade de serviço integrada transparentemente usando a biblioteca kardianos/service, permitindo execução como serviço nativo no Windows, Linux e macOS sem necessidade de executáveis separados.
🎯 Biblioteca Kardianos Service
O GoData utiliza a biblioteca github.com/kardianos/service que oferece:
- Multi-plataforma: Windows Service, systemd (Linux), launchd (macOS)
- Interface unificada: Mesma API para todas as plataformas
- Logging integrado: Logs direcionados para Event Log/journalctl/Console
- Configuração automática: Dependências e configurações específicas por plataforma
- Controle de ciclo de vida: Install, start, stop, restart, uninstall
🚀 Como Usar
A funcionalidade de serviço está disponível através de métodos do próprio servidor GoData:
package main
import (
"log"
"github.com/fitlcarlos/go-data/odata"
)
func main() {
// Criar servidor (carrega automaticamente configurações do .env)
server := odata.NewServer()
// Registrar entidades
server.RegisterEntity("Users", User{})
// Instalar como serviço
if err := server.Install(); err != nil {
log.Fatal("Erro ao instalar:", err)
}
// Iniciar serviço
if err := server.Start(); err != nil {
log.Fatal("Erro ao iniciar:", err)
}
}
📋 Métodos Disponíveis
// Gerenciamento de serviço (kardianos/service)
server.Install() error // Instala como serviço do sistema
server.Uninstall() error // Remove o serviço
server.Start() error // Inicia (detecta automaticamente se é serviço ou normal)
server.Stop() error // Para o serviço gracefully
server.Restart() error // Reinicia o serviço
server.Status() (service.Status, error) // Verifica status do serviço
// Métodos auxiliares
server.IsRunningAsService() bool // Detecta se está executando como serviço
server.Shutdown() error // Para apenas o servidor HTTP
🔍 Detecção Automática de Serviço
O método Start() detecta automaticamente se deve executar como serviço através de:
-
Argumentos de linha de comando:
./app run # Força execução como serviço ./app --service # Força execução como serviço ./app -service # Força execução como serviço -
Variável de ambiente:
export GODATA_RUN_AS_SERVICE=true ./app -
Contexto do sistema:
- Windows: Detecta execução pelo SCM (Service Control Manager)
- Linux: Detecta
INVOCATION_ID(systemd) ouPPID=1 - macOS: Detecta contexto de execução do launchd
⚙️ Configuração do Serviço
// Configuração automática via .env
server := odata.NewServer()
// As configurações do serviço são carregadas automaticamente do .env:
// SERVICE_NAME=godata-prod
// SERVICE_DISPLAY_NAME=GoData Production
// SERVICE_DESCRIPTION=Servidor GoData OData
// SERVER_HOST=0.0.0.0
// SERVER_PORT=8080
// Instalar e iniciar
server.Install()
server.Start()
🔧 Sobrescrevendo Configurações (Opcional)
Se necessário, ainda é possível sobrescrever as configurações carregadas do .env:
server := odata.NewServer()
// Sobrescrever apenas se necessário
config := server.GetConfig()
config.Name = "godata-customizado"
config.DisplayName = "GoData Personalizado"
config.Description = "Configuração personalizada"
server.Install()
server.Start()
🏗️ Configurações Automáticas por Plataforma (Kardianos)
O GoData configura automaticamente o serviço com otimizações específicas para cada plataforma:
Windows Service
StartType: Automatic
Dependencies: Tcpip, Dhcp
OnFailure: Restart
OnFailureDelayDuration: 5s
OnFailureResetPeriod: 10
Linux systemd
[Unit]
Requires=network.target
After=network-online.target syslog.target
[Service]
Type=notify
Restart=always
RestartSec=5
User=godata
Group=godata
LimitNOFILE=65536
KillMode=mixed
TimeoutStopSec=30
macOS launchd
Configuração automática com propriedades adequadas para execução em background.
🎯 Exemplo Prático
Veja o exemplo completo em examples/service/ que demonstra:
- Como usar os métodos de serviço integrados
- Configuração personalizada de serviço
- Gerenciamento via linha de comando
- Entidades de exemplo (Users e Products)
📊 Monitoramento e Logs (Kardianos)
O kardianos/service integra automaticamente com os sistemas de log nativos:
Linux (systemd + journalctl)
# Status detalhado (use o nome configurado no server.config.Name)
sudo systemctl status meu-godata-service
# Logs em tempo real (integrados via kardianos)
sudo journalctl -u meu-godata-service -f
# Logs específicos do GoData
sudo journalctl -u meu-godata-service --since "1 hour ago"
Windows (Event Log)
# Gerenciador de Serviços (procurar pelo DisplayName)
services.msc
# PowerShell (usar o Name configurado)
Get-Service meu-godata-service
# Event Viewer - logs integrados via kardianos
eventvwr.msc
# Navegar: Windows Logs > Application > Source = "meu-godata-service"
macOS (Console)
# Console.app para logs do sistema
# ou via linha de comando:
log stream --predicate 'subsystem == "meu-godata-service"'
🔒 Configuração de Produção
# Arquivo .env para produção
SERVICE_NAME=godata-prod
SERVICE_DISPLAY_NAME=GoData Production Service
SERVICE_DESCRIPTION=Servidor GoData OData v4 - Produção
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
SERVER_ENABLE_CORS=true
SERVER_ALLOWED_ORIGINS=https://meuapp.com
SERVER_TLS_CERT_FILE=/etc/ssl/certs/server.crt
SERVER_TLS_KEY_FILE=/etc/ssl/private/server.key
JWT_ENABLED=true
JWT_REQUIRE_AUTH=true
JWT_SECRET_KEY=minha-chave-super-secreta-de-producao
// Configuração para produção com kardianos/service
server := odata.NewServer() // Carrega automaticamente do .env
// Instalar e configurar o serviço
log.Fatal(server.Install()) // Instala via kardianos
log.Fatal(server.Start()) // Inicia com detecção automática
📚 Integração com CI/CD
Script de Deploy Automatizado
#!/bin/bash
# deploy-godata.sh
set -e
# Configurações
SERVICE_NAME="godata"
INSTALL_DIR="/opt/godata"
echo "🚀 Iniciando deploy do GoData Service..."
# Parar serviço se estiver rodando
if systemctl is-active --quiet $SERVICE_NAME; then
echo "⏹️ Parando serviço..."
sudo systemctl stop $SERVICE_NAME
fi
# Fazer backup do executável atual
if [ -f "$INSTALL_DIR/godata" ]; then
sudo cp "$INSTALL_DIR/godata" "$INSTALL_DIR/godata.backup"
fi
# Copiar novo executável
sudo cp ./godata $INSTALL_DIR/
sudo chown godata:godata $INSTALL_DIR/godata
sudo chmod +x $INSTALL_DIR/godata
# Instalar/atualizar serviço
sudo $INSTALL_DIR/godata install
# Iniciar serviço
sudo systemctl start $SERVICE_NAME
sudo systemctl enable $SERVICE_NAME
# Verificar status
sleep 2
if systemctl is-active --quiet $SERVICE_NAME; then
echo "✅ Deploy concluído com sucesso!"
sudo systemctl status $SERVICE_NAME
else
echo "❌ Erro no deploy!"
exit 1
fi
GitHub Actions Workflow
name: Deploy GoData Service
on:
push:
tags: ['v*']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Build Service
run: make build-all
- name: Deploy to Production
run: |
# Copiar binário para servidor
scp build/godata-linux-amd64 user@server:/tmp/godata
# Executar deploy no servidor
ssh user@server 'sudo /tmp/deploy-godata.sh'
Para um exemplo completo de uso, consulte: examples/service/
🤝 Contribuindo
Contribuições são bem-vindas! Por favor:
- Faça um fork do projeto
- Crie uma branch para sua feature (
git checkout -b feature/nova-feature) - Commit suas mudanças (
git commit -am 'Adiciona nova feature') - Push para a branch (
git push origin feature/nova-feature) - Abra um Pull Request
Executando Testes
go test ./...
📁 Exemplos
O Go-Data inclui diversos exemplos práticos para demonstrar suas funcionalidades:
🏢 Multi-Tenant
Exemplo completo demonstrando:
- Configuração multi-tenant via .env
- Entidades com isolamento por tenant
- Múltiplos métodos de identificação de tenant
- Endpoints de gerenciamento e monitoramento
- Diferentes tipos de banco por tenant
- Arquivo .env completo com configurações multi-tenant
🔐 JWT Authentication
Demonstra sistema completo de autenticação JWT:
- Configuração JWT com roles e scopes
- Endpoints de login, refresh e logout
- Controle de acesso por entidade
- Middleware de autenticação
- Arquivo .env com JWT habilitado
🔓 Basic Authentication
Demonstra autenticação HTTP Basic:
- Configuração Basic Auth com validação em banco de dados
- Customização de UserValidator com logging
- Entidades protegidas por autenticação
- WWW-Authenticate header automático
- Múltiplos usuários de teste com roles
🎯 Events
Sistema completo de eventos:
- Validações customizadas
- Auditoria e logging
- Cancelamento de operações
- Controle de acesso baseado em contexto
- Arquivo .env com configurações para eventos
🔧 Service
Execução como serviço do sistema:
- Funcionalidade kardianos/service integrada
- Gerenciamento multi-plataforma (Windows/Linux/macOS)
- Detecção automática de contexto de execução
- Configuração de serviço personalizada
- Logging integrado com sistemas nativos
- Arquivo .env completo com configurações de serviço
🎯 Service Operations
Sistema de Service Operations equivalente ao XData:
- ServiceContext otimizado com ObjectManager integrado
- Sintaxe simples similar ao Fiber para registro
- Controle automático de autenticação baseado em JWT
- Suporte completo a multi-tenant
- Service Groups para organização
- Equivalência funcional ao TXDataOperationContext do XData
- Arquivo .env com configurações JWT e multi-tenant
📊 Básico
Exemplo básico de uso:
- Configuração simples
- Entidades e relacionamentos
- Operações CRUD
- Arquivo .env com configurações básicas
🚀 Avançado
Funcionalidades avançadas:
- Configurações personalizadas
- Mapeamento complexo
- Relacionamentos N:N
- Arquivo .env com configurações de produção
⚙️ Config Override
Demonstra configuração programática e sobrescrita de .env:
- Carregamento automático do .env
- Injeção automática de variáveis no
os.Getenv() - Sobrescrita de configurações via código (prioridade sobre .env)
- Uso de variáveis customizadas além das padrões
- Métodos setter fluentes
- Configuração condicional baseada em ambiente
📚 Referências
📄 Licença
Este projeto está licenciado sob a Licença MIT - veja o arquivo LICENSE para detalhes.
📞 Suporte
- GitHub Issues - Para bugs e feature requests
- GitHub Discussions - Para perguntas e discussões
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
advanced
command
|
|
|
batch
command
|
|
|
config_override
command
|
|
|
events
command
|
|
|
jwt
command
|
|
|
jwt_banco
command
|
|
|
multi_tenant
command
|
|
|
service
command
|
|