agent
This commit is contained in:
99
repositories/assistantAuditRepository.go
Normal file
99
repositories/assistantAuditRepository.go
Normal file
@@ -0,0 +1,99 @@
|
||||
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() }
|
||||
Reference in New Issue
Block a user