fixed some migration issues and added generics for update and insert
This commit is contained in:
146
internal/db/audit.go
Normal file
146
internal/db/audit.go
Normal file
@@ -0,0 +1,146 @@
|
||||
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
|
||||
Reference in New Issue
Block a user