100 lines
2.8 KiB
Go
100 lines
2.8 KiB
Go
package repositories
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"time"
|
|
|
|
"nearle/models"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Where the assistant's audit rows land.
|
|
//
|
|
// Deliberately thin: one insert and one read. The registry decides what an entry
|
|
// means; this only has to keep it.
|
|
type AssistantAuditRepository interface {
|
|
Record(entry models.AssistantAudit) error
|
|
// Recent reads the trail back for one tenant, newest first.
|
|
//
|
|
// Scoped by tenant even though this is a review surface, because "who looked
|
|
// at what" is itself a merchant's data — a trail readable across tenants
|
|
// would be a nicer version of the hole the trail exists to detect.
|
|
Recent(tenantID, limit int) ([]models.AssistantAudit, error)
|
|
}
|
|
|
|
type assistantAuditRepository struct{ db *gorm.DB }
|
|
|
|
func NewAssistantAuditRepository(db *gorm.DB) AssistantAuditRepository {
|
|
return &assistantAuditRepository{db: db}
|
|
}
|
|
|
|
func (r *assistantAuditRepository) Record(entry models.AssistantAudit) error {
|
|
if r.db == nil {
|
|
return nil
|
|
}
|
|
return r.db.Create(&entry).Error
|
|
}
|
|
|
|
func (r *assistantAuditRepository) Recent(tenantID, limit int) ([]models.AssistantAudit, error) {
|
|
if r.db == nil {
|
|
return nil, nil
|
|
}
|
|
if limit <= 0 || limit > 500 {
|
|
limit = 100
|
|
}
|
|
var rows []models.AssistantAudit
|
|
err := r.db.Where("tenantid = ?", tenantID).
|
|
Order("at DESC").Limit(limit).Find(&rows).Error
|
|
return rows, err
|
|
}
|
|
|
|
// EncodeAuditArgs renders arguments for storage.
|
|
//
|
|
// Keys sorted, so two identical calls store identical JSON and a query looking
|
|
// for one of them finds both. Go randomises map iteration, and without this the
|
|
// same call would be unsearchable across rows.
|
|
//
|
|
// A value that will not encode becomes a string rather than failing the write:
|
|
// losing the audit row entirely to save one unencodable argument is the wrong
|
|
// trade, and the row is still the record that the call happened.
|
|
func EncodeAuditArgs(args map[string]any) string {
|
|
if len(args) == 0 {
|
|
return "{}"
|
|
}
|
|
ordered := make(map[string]json.RawMessage, len(args))
|
|
keys := make([]string, 0, len(args))
|
|
for key := range args {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
for _, key := range keys {
|
|
raw, err := json.Marshal(args[key])
|
|
if err != nil {
|
|
raw, _ = json.Marshal("<unencodable>")
|
|
}
|
|
ordered[key] = raw
|
|
}
|
|
|
|
// Re-marshalled through an ordered slice of pairs so the output is stable;
|
|
// a map would be re-randomised on the way out.
|
|
var out []byte
|
|
out = append(out, '{')
|
|
for i, key := range keys {
|
|
if i > 0 {
|
|
out = append(out, ',')
|
|
}
|
|
name, _ := json.Marshal(key)
|
|
out = append(out, name...)
|
|
out = append(out, ':')
|
|
out = append(out, ordered[key]...)
|
|
}
|
|
out = append(out, '}')
|
|
return string(out)
|
|
}
|
|
|
|
// AuditDuration converts a duration for storage, rounding to the millisecond.
|
|
func AuditDuration(d time.Duration) int64 { return d.Round(time.Millisecond).Milliseconds() }
|