-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
853 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package db | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/RMI/pacta/pacta" | ||
) | ||
|
||
type Cursor string | ||
|
||
type PageInfo struct { | ||
Cursor Cursor | ||
HasNextPage bool | ||
} | ||
|
||
type AuditLogQuerySortBy string | ||
|
||
const ( | ||
AuditLogQuerySortBy_CreatedAt AuditLogQuerySortBy = "created_at" | ||
AuditLogQuerySortBy_ActorType AuditLogQuerySortBy = "actor_type" | ||
AuditLogQuerySortBy_ActorID AuditLogQuerySortBy = "actor_id" | ||
AuditLogQuerySortBy_ActorOwnerID AuditLogQuerySortBy = "actor_owner_id" | ||
AuditLogQuerySortBy_PrimaryTargetID AuditLogQuerySortBy = "primary_target_id" | ||
AuditLogQuerySortBy_PrimaryTargetType AuditLogQuerySortBy = "primary_target_type" | ||
AuditLogQuerySortBy_PrimaryTargetOwnerID AuditLogQuerySortBy = "primary_target_owner_id" | ||
AuditLogQuerySortBy_SecondaryTargetID AuditLogQuerySortBy = "secondary_target_id" | ||
AuditLogQuerySortBy_SecondaryTargetType AuditLogQuerySortBy = "secondary_target_type" | ||
AuditLogQuerySortBy_SecondaryTargetOwnerID AuditLogQuerySortBy = "secondary_target_owner_id" | ||
) | ||
|
||
type AuditLogQuerySort struct { | ||
By AuditLogQuerySortBy | ||
Ascending bool | ||
} | ||
|
||
type AuditLogQueryWhere struct { | ||
InID []pacta.AuditLogID | ||
MinCreatedAt time.Time | ||
MaxCreatedAt time.Time | ||
InActionType []pacta.AuditLogAction | ||
InActorType []pacta.AuditLogActorType | ||
InActorID []string | ||
InActorOwnerID []pacta.OwnerID | ||
InTargetType []pacta.AuditLogTargetType | ||
InTargetID []string | ||
InTargetOwnerID []pacta.OwnerID | ||
} | ||
|
||
type AuditLogQuery struct { | ||
Cursor Cursor | ||
Limit int | ||
Wheres []*AuditLogQueryWhere | ||
Sorts []*AuditLogQuerySort | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
package sqldb | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/RMI/pacta/db" | ||
"github.com/RMI/pacta/pacta" | ||
"github.com/jackc/pgx/v5" | ||
"github.com/jackc/pgx/v5/pgtype" | ||
) | ||
|
||
const auditLogIDNamespace = "al" | ||
const auditLogSelectColumns = ` | ||
audit_log.id, | ||
audit_log.action, | ||
audit_log.actor_type, | ||
audit_log.actor_id, | ||
audit_log.actor_owner_id, | ||
audit_log.primary_target_type, | ||
audit_log.primary_target_id, | ||
audit_log.primary_target_owner_id, | ||
audit_log.secondary_target_type, | ||
audit_log.secondary_target_id, | ||
audit_log.secondary_target_owner_id, | ||
audit_log.created_at | ||
` | ||
|
||
func (d *DB) AuditLogs(tx db.Tx, q *db.AuditLogQuery) ([]*pacta.AuditLog, *db.PageInfo, error) { | ||
if q.Limit <= 0 { | ||
return nil, nil, fmt.Errorf("limit must be greater than 0, was %d", q.Limit) | ||
} | ||
offset, err := offsetFromCursor(q.Cursor) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("converting cursor to offset: %w", err) | ||
} | ||
sql, args, err := auditLogQuery(q) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("building audit_log query: %w", err) | ||
} | ||
rows, err := d.query(tx, sql, args...) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("executing audit_log query: %w", err) | ||
} | ||
als, err := rowsToAuditLogs(rows) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("getting audit_logs from rows: %w", err) | ||
} | ||
// This will incorrectly say "yes there are more results" if we happen to hit the actual limit, but | ||
// that's a pretty small performance loss. | ||
hasNextPage := len(als) == q.Limit | ||
cursor := offsetToCursor(offset + len(als)) | ||
return als, &db.PageInfo{HasNextPage: hasNextPage, Cursor: db.Cursor(cursor)}, nil | ||
} | ||
|
||
func (d *DB) CreateAuditLog(tx db.Tx, a *pacta.AuditLog) (pacta.AuditLogID, error) { | ||
if err := validateAuditLogForCreation(a); err != nil { | ||
return "", fmt.Errorf("validating audit_log for creation: %w", err) | ||
} | ||
id := pacta.AuditLogID(d.randomID(auditLogIDNamespace)) | ||
ownerFn := func(o *pacta.Owner) any { | ||
if o == nil { | ||
return pgtype.Text{} | ||
} | ||
return o.ID | ||
} | ||
var stt *string | ||
if a.SecondaryTargetType != "" { | ||
s := string(a.SecondaryTargetType) | ||
stt = &s | ||
} | ||
err := d.exec(tx, ` | ||
INSERT INTO audit_log | ||
( | ||
id, action, actor_type, actor_id, actor_owner_id, | ||
primary_target_type, primary_target_id, primary_target_owner_id, | ||
secondary_target_type, secondary_target_id, secondary_target_owner_id | ||
) | ||
VALUES | ||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11); | ||
`, id, a.Action, a.ActorType, a.ActorID, ownerFn(a.ActorOwner), | ||
a.PrimaryTargetType, a.PrimaryTargetID, ownerFn(a.PrimaryTargetOwner), | ||
stt, a.SecondaryTargetID, ownerFn(a.SecondaryTargetOwner)) | ||
if err != nil { | ||
return "", fmt.Errorf("creating audit_log row: %w", err) | ||
} | ||
return id, nil | ||
} | ||
|
||
func rowsToAuditLogs(rows pgx.Rows) ([]*pacta.AuditLog, error) { | ||
return allRows("auditLog", rows, rowToAuditLog) | ||
} | ||
|
||
func rowToAuditLog(row rowScanner) (*pacta.AuditLog, error) { | ||
a := &pacta.AuditLog{} | ||
var actorType, primaryType string | ||
var actorOwner, primaryOwner pacta.OwnerID | ||
var secondaryType, secondaryOwner pgtype.Text | ||
err := row.Scan( | ||
&a.ID, &a.Action, &actorType, &a.ActorID, &actorOwner, | ||
&primaryType, &a.PrimaryTargetID, &primaryOwner, | ||
&secondaryType, &a.SecondaryTargetID, &secondaryOwner, | ||
&a.CreatedAt, | ||
) | ||
if err != nil { | ||
return nil, fmt.Errorf("scanning into audit_log: %w", err) | ||
} | ||
if t, err := pacta.ParseAuditLogActorType(actorType); err != nil { | ||
return nil, fmt.Errorf("parsing audit_log actor_type: %w", err) | ||
} else { | ||
a.ActorType = t | ||
} | ||
if t, err := pacta.ParseAuditLogTargetType(primaryType); err != nil { | ||
return nil, fmt.Errorf("parsing audit_log primary_target_type: %w", err) | ||
} else { | ||
a.PrimaryTargetType = t | ||
} | ||
if secondaryType.Valid { | ||
if t, err := pacta.ParseAuditLogTargetType(secondaryType.String); err != nil { | ||
return nil, fmt.Errorf("parsing audit_log secondary_target_type: %w", err) | ||
} else { | ||
a.SecondaryTargetType = t | ||
} | ||
} | ||
if secondaryOwner.Valid { | ||
a.SecondaryTargetOwner = &pacta.Owner{ID: pacta.OwnerID(secondaryOwner.String)} | ||
} | ||
if actorOwner != "" { | ||
a.ActorOwner = &pacta.Owner{ID: actorOwner} | ||
} | ||
if primaryOwner != "" { | ||
a.PrimaryTargetOwner = &pacta.Owner{ID: primaryOwner} | ||
} | ||
return a, nil | ||
} | ||
|
||
func validateAuditLogForCreation(a *pacta.AuditLog) error { | ||
if a.ID != "" { | ||
return fmt.Errorf("audit log already has an ID") | ||
} | ||
if !a.CreatedAt.IsZero() { | ||
return fmt.Errorf("audit log already has a CreatedAt") | ||
} | ||
if a.Action == "" { | ||
return fmt.Errorf("audit log missing required action") | ||
} | ||
if a.ActorType == "" { | ||
return fmt.Errorf("audit log missing ActorType") | ||
} | ||
if a.ActorID == "" { | ||
return fmt.Errorf("audit log missing ActorID") | ||
} | ||
if a.ActorOwner == nil { | ||
return fmt.Errorf("audit log ActorOwner is nil") | ||
} | ||
if a.ActorOwner.ID == "" { | ||
return fmt.Errorf("audit log ActorOwnerID is empty") | ||
} | ||
if a.PrimaryTargetType == "" { | ||
return fmt.Errorf("audit log missing PrimaryTargetType") | ||
} | ||
if a.PrimaryTargetID == "" { | ||
return fmt.Errorf("audit log missing PrimaryTargetID") | ||
} | ||
if a.PrimaryTargetOwner == nil { | ||
return fmt.Errorf("audit log PrimaryTargetOwner is nil") | ||
} | ||
if a.PrimaryTargetOwner.ID == "" { | ||
return fmt.Errorf("audit log PrimaryTargetOwnerID is empty") | ||
} | ||
return nil | ||
} | ||
|
||
func auditLogQuery(q *db.AuditLogQuery) (string, []any, error) { | ||
args := &queryArgs{} | ||
selectFrom := `SELECT ` + auditLogSelectColumns + ` FROM audit_log ` | ||
where := auditLogQueryWheresToSQL(q.Wheres, args) | ||
if where == "" { | ||
return "", nil, errors.New("where clause cannot be empty in audit_log query") | ||
} | ||
order := auditLogQuerySortsToSQL(q.Sorts) | ||
limit := fmt.Sprintf("LIMIT %d", q.Limit) | ||
offset := "" | ||
if q.Cursor != "" { | ||
o, err := offsetFromCursor(q.Cursor) | ||
if err != nil { | ||
return "", nil, fmt.Errorf("extracting offset from cursor in audit-log query: %w", err) | ||
} | ||
offset = fmt.Sprintf("OFFSET %d", o) | ||
} | ||
sql := fmt.Sprintf("%s %s %s %s %s;", selectFrom, where, order, limit, offset) | ||
return sql, args.values, nil | ||
} | ||
|
||
func auditLogQuerySortsToSQL(ss []*db.AuditLogQuerySort) string { | ||
sorts := []string{} | ||
for _, s := range ss { | ||
v := " DESC" | ||
if s.Ascending { | ||
v = " ASC" | ||
} | ||
sorts = append(sorts, fmt.Sprintf("audit_log.%s %s", s.By, v)) | ||
} | ||
// Forces a deterministic sort for pagination. | ||
sorts = append(sorts, "audit_log.id ASC") | ||
return "ORDER BY " + strings.Join(sorts, ", ") | ||
} | ||
|
||
func auditLogQueryWheresToSQL(qs []*db.AuditLogQueryWhere, args *queryArgs) string { | ||
wheres := []string{} | ||
for _, q := range qs { | ||
if len(q.InID) > 0 { | ||
wheres = append(wheres, eqOrIn("audit_log.id", q.InID, args)) | ||
} | ||
if len(q.InActionType) > 0 { | ||
wheres = append(wheres, eqOrIn("audit_log.action", q.InActionType, args)) | ||
} | ||
if !q.MinCreatedAt.IsZero() { | ||
wheres = append(wheres, "audit_log.created_at >= "+args.add(q.MinCreatedAt)) | ||
} | ||
if !q.MaxCreatedAt.IsZero() { | ||
wheres = append(wheres, "audit_log.created_at <= "+args.add(q.MaxCreatedAt)) | ||
} | ||
if len(q.InActorType) > 0 { | ||
wheres = append(wheres, eqOrIn("audit_log.actor_type", q.InActorType, args)) | ||
} | ||
if len(q.InActorID) > 0 { | ||
wheres = append(wheres, eqOrIn("audit_log.actor_id", q.InActorID, args)) | ||
} | ||
if len(q.InActorOwnerID) > 0 { | ||
wheres = append(wheres, eqOrIn("audit_log.actor_owner_id", q.InActorOwnerID, args)) | ||
} | ||
if len(q.InTargetType) > 0 { | ||
or := fmt.Sprintf("(%s OR %s)", | ||
eqOrIn("audit_log.primary_target_type", q.InTargetType, args), | ||
eqOrIn("audit_log.secondary_target_type", q.InTargetType, args), | ||
) | ||
wheres = append(wheres, or) | ||
} | ||
if len(q.InTargetID) > 0 { | ||
or := fmt.Sprintf("(%s OR %s)", | ||
eqOrIn("audit_log.primary_target_id", q.InTargetID, args), | ||
eqOrIn("audit_log.secondary_target_id", q.InTargetID, args), | ||
) | ||
wheres = append(wheres, or) | ||
} | ||
if len(q.InTargetOwnerID) > 0 { | ||
or := fmt.Sprintf("(%s OR %s)", | ||
eqOrIn("audit_log.primary_target_owner_id", q.InTargetOwnerID, args), | ||
eqOrIn("audit_log.secondary_target_owner_id", q.InTargetOwnerID, args), | ||
) | ||
wheres = append(wheres, or) | ||
} | ||
} | ||
if len(wheres) == 0 { | ||
return "" | ||
} | ||
return "WHERE " + strings.Join(wheres, " AND ") | ||
} |
Oops, something went wrong.