Files
oslstats/internal/db/update.go

116 lines
3.1 KiB
Go

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
}