package db import ( "context" "net/http" "github.com/pkg/errors" "github.com/uptrace/bun" ) type updater[T any] struct { tx bun.Tx q *bun.UpdateQuery model *T columns []string auditCallback AuditCallback auditRequest *http.Request } // Update creates an updater for a model // You must specify which columns to update via .Column() or use .WherePK() func Update[T any](tx bun.Tx, model *T) *updater[T] { if model == nil { panic("model cannot be nil") } return &updater[T]{ tx: tx, q: tx.NewUpdate().Model(model), model: model, } } // UpdateByID creates an updater with an ID where clause // You must still specify which columns to update via .Column() func UpdateByID[T any](tx bun.Tx, id int, model *T) *updater[T] { if id <= 0 { panic("id must be positive") } return Update(tx, model).Where("id = ?", id) } // Column specifies which columns to update // Example: .Column("start_date", "end_date") func (u *updater[T]) Column(columns ...string) *updater[T] { u.columns = append(u.columns, columns...) u.q = u.q.Column(columns...) return u } // Where adds a WHERE clause // Example: .Where("id = ?", 123) func (u *updater[T]) Where(query string, args ...any) *updater[T] { u.q = u.q.Where(query, args...) return u } // WherePK adds a WHERE clause on the primary key // The model must have its primary key field populated func (u *updater[T]) WherePK() *updater[T] { u.q = u.q.WherePK() return u } // Set adds a raw SET clause for complex updates // Example: .Set("updated_at = NOW()") func (u *updater[T]) Set(query string, args ...any) *updater[T] { u.q = u.q.Set(query, args...) return u } // WithAudit enables audit logging for this update operation // The callback will be invoked after successful update with auto-generated audit info // If the callback returns an error, the transaction will be rolled back func (u *updater[T]) WithAudit(r *http.Request, callback AuditCallback) *updater[T] { u.auditRequest = r u.auditCallback = callback return u } // Exec executes the update and optionally logs to audit // Returns an error if update fails or if audit callback fails (triggering rollback) func (u *updater[T]) Exec(ctx context.Context) error { // Build audit details BEFORE update (captures changed fields) var details map[string]any if u.auditCallback != nil && len(u.columns) > 0 { details = extractChangedFields(u.model, u.columns) } // Execute update _, err := u.q.Exec(ctx) if err != nil { return errors.Wrap(err, "bun.UpdateQuery.Exec") } // Handle audit logging if enabled if u.auditCallback != nil && u.auditRequest != nil { tableName := extractTableName[T]() resourceType := extractResourceType(tableName) action := buildAction(resourceType, "update") info := &AuditInfo{ Action: action, ResourceType: resourceType, ResourceID: extractPrimaryKey(u.model), Details: details, // Changed fields only } // Call audit callback - if it fails, return error to trigger rollback if err := u.auditCallback(ctx, u.tx, info, u.auditRequest); err != nil { return errors.Wrap(err, "audit.callback") } } return nil }