147 lines
3.8 KiB
Go
147 lines
3.8 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/uptrace/bun"
|
|
)
|
|
|
|
// AuditInfo contains metadata for audit logging
|
|
type AuditInfo struct {
|
|
Action string // e.g., "seasons.create", "users.update"
|
|
ResourceType string // e.g., "season", "user"
|
|
ResourceID any // Primary key value (int, string, etc.)
|
|
Details map[string]any // Changed fields or additional metadata
|
|
}
|
|
|
|
// AuditCallback is called after successful database operations to log changes
|
|
type AuditCallback func(ctx context.Context, tx bun.Tx, info *AuditInfo, r *http.Request) error
|
|
|
|
// extractTableName gets the bun table name from a model type using reflection
|
|
// Example: Season with `bun:"table:seasons,alias:s"` returns "seasons"
|
|
func extractTableName[T any]() string {
|
|
var model T
|
|
t := reflect.TypeOf(model)
|
|
|
|
// Handle pointer types
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
|
|
// Look for bun.BaseModel field with table tag
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
if field.Type.Name() == "BaseModel" {
|
|
bunTag := field.Tag.Get("bun")
|
|
if bunTag != "" {
|
|
// Parse tag: "table:seasons,alias:s" -> "seasons"
|
|
parts := strings.Split(bunTag, ",")
|
|
for _, part := range parts {
|
|
if strings.HasPrefix(part, "table:") {
|
|
return strings.TrimPrefix(part, "table:")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: use struct name in lowercase + "s"
|
|
return strings.ToLower(t.Name()) + "s"
|
|
}
|
|
|
|
// extractResourceType converts a table name to singular resource type
|
|
// Example: "seasons" -> "season", "users" -> "user"
|
|
func extractResourceType(tableName string) string {
|
|
// Simple singularization: remove trailing 's'
|
|
if strings.HasSuffix(tableName, "s") && len(tableName) > 1 {
|
|
return tableName[:len(tableName)-1]
|
|
}
|
|
return tableName
|
|
}
|
|
|
|
// buildAction creates a permission-style action string
|
|
// Example: ("season", "create") -> "seasons.create"
|
|
func buildAction(resourceType, operation string) string {
|
|
// Pluralize resource type (simple: add 's')
|
|
plural := resourceType
|
|
if !strings.HasSuffix(plural, "s") {
|
|
plural = plural + "s"
|
|
}
|
|
return plural + "." + operation
|
|
}
|
|
|
|
// extractPrimaryKey uses reflection to find and return the primary key value from a model
|
|
// Returns nil if no primary key is found
|
|
func extractPrimaryKey[T any](model *T) any {
|
|
if model == nil {
|
|
return nil
|
|
}
|
|
|
|
v := reflect.ValueOf(model)
|
|
if v.Kind() == reflect.Ptr {
|
|
v = v.Elem()
|
|
}
|
|
|
|
t := v.Type()
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
bunTag := field.Tag.Get("bun")
|
|
if bunTag != "" && strings.Contains(bunTag, "pk") {
|
|
// Found primary key field
|
|
fieldValue := v.Field(i)
|
|
if fieldValue.IsValid() && fieldValue.CanInterface() {
|
|
return fieldValue.Interface()
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractChangedFields builds a map of field names to their new values
|
|
// Only includes fields specified in the columns list
|
|
func extractChangedFields[T any](model *T, columns []string) map[string]any {
|
|
if model == nil || len(columns) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make(map[string]any)
|
|
v := reflect.ValueOf(model)
|
|
if v.Kind() == reflect.Ptr {
|
|
v = v.Elem()
|
|
}
|
|
|
|
t := v.Type()
|
|
|
|
// Build map of bun column names to field names
|
|
columnToField := make(map[string]int)
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
bunTag := field.Tag.Get("bun")
|
|
if bunTag != "" {
|
|
// Parse bun tag to get column name (first part before comma)
|
|
parts := strings.Split(bunTag, ",")
|
|
if len(parts) > 0 && parts[0] != "" {
|
|
columnToField[parts[0]] = i
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract values for requested columns
|
|
for _, col := range columns {
|
|
if fieldIdx, ok := columnToField[col]; ok {
|
|
fieldValue := v.Field(fieldIdx)
|
|
if fieldValue.IsValid() && fieldValue.CanInterface() {
|
|
result[col] = fieldValue.Interface()
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Note: We don't need getTxFromQuery since we store the tx directly in our helper structs
|