package db import ( "net/http" "reflect" "strings" ) type AuditMeta struct { r *http.Request u *User } func NewAudit(r *http.Request, u *User) *AuditMeta { if u == nil { u = CurrentUser(r.Context()) } return &AuditMeta{r, u} } // 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 } // 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.Pointer { 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" for part := range strings.SplitSeq(bunTag, ",") { part, _ := strings.CutPrefix(part, "table:") return part } } } } // 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.Pointer { 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.Pointer { 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 }