Files
oslstats/internal/db/update.go
2026-02-15 12:27:36 +11:00

123 lines
3.2 KiB
Go

package db
import (
"context"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
type updater[T any] struct {
tx bun.Tx
q *bun.UpdateQuery
model *T
columns []string
audit *AuditMeta
auditInfo *AuditInfo
}
// 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
// If the provided *AuditInfo is nil, will use reflection to automatically work out the details
func (u *updater[T]) WithAudit(meta *AuditMeta, info *AuditInfo) *updater[T] {
u.audit = meta
u.auditInfo = info
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.audit != nil && len(u.columns) > 0 {
details = extractChangedFields(u.model, u.columns)
}
// Execute update
result, err := u.q.Exec(ctx)
if err != nil {
return errors.Wrap(err, "bun.UpdateQuery.Exec")
}
rows, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "result.RowsAffected")
}
if rows == 0 {
resource := extractResourceType(extractTableName[T]())
return BadRequestNotFound(resource, "id", extractPrimaryKey(u.model))
}
// Handle audit logging if enabled
if u.audit != nil {
if u.auditInfo == nil {
tableName := extractTableName[T]()
resourceType := extractResourceType(tableName)
action := buildAction(resourceType, "update")
u.auditInfo = &AuditInfo{
Action: action,
ResourceType: resourceType,
ResourceID: extractPrimaryKey(u.model),
Details: details, // Changed fields only
}
}
err = LogSuccess(ctx, u.tx, u.audit, u.auditInfo)
if err != nil {
return errors.Wrap(err, "LogSuccess")
}
}
return nil
}