docs: reorder & testing
This commit is contained in:
+108
-49
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/backend/internal/postgres/jet/backend/model"
|
||||
@@ -72,8 +73,7 @@ type sanctionInsert struct {
|
||||
SanctionCode string
|
||||
Scope string
|
||||
ReasonCode string
|
||||
ActorType string
|
||||
ActorID string
|
||||
Actor ActorRef
|
||||
AppliedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
FlipPermanent bool
|
||||
@@ -85,8 +85,7 @@ type limitInsert struct {
|
||||
LimitCode string
|
||||
Value int32
|
||||
ReasonCode string
|
||||
ActorType string
|
||||
ActorID string
|
||||
Actor ActorRef
|
||||
AppliedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
@@ -113,7 +112,8 @@ func accountColumns() postgres.ColumnList {
|
||||
func snapshotColumns() postgres.ColumnList {
|
||||
s := table.EntitlementSnapshots
|
||||
return postgres.ColumnList{
|
||||
s.UserID, s.Tier, s.IsPaid, s.Source, s.ActorType, s.ActorID,
|
||||
s.UserID, s.Tier, s.IsPaid, s.Source,
|
||||
s.ActorType, s.ActorUserID, s.ActorUsername,
|
||||
s.ReasonCode, s.StartsAt, s.EndsAt, s.MaxRegisteredRaceNames, s.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -275,7 +275,7 @@ func (s *Store) ListActiveSanctions(ctx context.Context, userID uuid.UUID) ([]Ac
|
||||
r := table.SanctionRecords
|
||||
stmt := postgres.SELECT(
|
||||
r.SanctionCode, r.Scope, r.ReasonCode,
|
||||
r.ActorType, r.ActorID,
|
||||
r.ActorType, r.ActorUserID, r.ActorUsername,
|
||||
r.AppliedAt, r.ExpiresAt,
|
||||
).
|
||||
FROM(a.INNER_JOIN(r, r.RecordID.EQ(a.RecordID))).
|
||||
@@ -292,7 +292,7 @@ func (s *Store) ListActiveSanctions(ctx context.Context, userID uuid.UUID) ([]Ac
|
||||
SanctionCode: row.SanctionCode,
|
||||
Scope: row.Scope,
|
||||
ReasonCode: row.ReasonCode,
|
||||
Actor: ActorRef{Type: row.ActorType, ID: derefString(row.ActorID)},
|
||||
Actor: actorFromColumns(row.ActorType, row.ActorUserID, row.ActorUsername),
|
||||
AppliedAt: row.AppliedAt,
|
||||
}
|
||||
if row.ExpiresAt != nil {
|
||||
@@ -311,7 +311,7 @@ func (s *Store) ListActiveLimits(ctx context.Context, userID uuid.UUID) ([]Activ
|
||||
r := table.LimitRecords
|
||||
stmt := postgres.SELECT(
|
||||
r.LimitCode, a.Value, r.ReasonCode,
|
||||
r.ActorType, r.ActorID,
|
||||
r.ActorType, r.ActorUserID, r.ActorUsername,
|
||||
r.AppliedAt, r.ExpiresAt,
|
||||
).
|
||||
FROM(a.INNER_JOIN(r, r.RecordID.EQ(a.RecordID))).
|
||||
@@ -331,7 +331,7 @@ func (s *Store) ListActiveLimits(ctx context.Context, userID uuid.UUID) ([]Activ
|
||||
LimitCode: row.LimitRecords.LimitCode,
|
||||
Value: row.LimitActive.Value,
|
||||
ReasonCode: row.LimitRecords.ReasonCode,
|
||||
Actor: ActorRef{Type: row.LimitRecords.ActorType, ID: derefString(row.LimitRecords.ActorID)},
|
||||
Actor: actorFromColumns(row.LimitRecords.ActorType, row.LimitRecords.ActorUserID, row.LimitRecords.ActorUsername),
|
||||
AppliedAt: row.LimitRecords.AppliedAt,
|
||||
}
|
||||
if row.LimitRecords.ExpiresAt != nil {
|
||||
@@ -395,9 +395,12 @@ func (s *Store) ApplyEntitlementTx(ctx context.Context, snap EntitlementSnapshot
|
||||
if err := s.assertAccountLive(ctx, snap.UserID); err != nil {
|
||||
return EntitlementSnapshot{}, err
|
||||
}
|
||||
err := withTx(ctx, s.db, func(tx *sql.Tx) error {
|
||||
actorUserID, actorUsername, err := actorToColumnArgs(snap.Actor)
|
||||
if err != nil {
|
||||
return EntitlementSnapshot{}, err
|
||||
}
|
||||
err = withTx(ctx, s.db, func(tx *sql.Tx) error {
|
||||
recordID := uuid.New()
|
||||
actorID := nullableString(snap.Actor.ID)
|
||||
var endsAt any
|
||||
if snap.EndsAt != nil {
|
||||
endsAt = *snap.EndsAt
|
||||
@@ -409,20 +412,21 @@ func (s *Store) ApplyEntitlementTx(ctx context.Context, snap EntitlementSnapshot
|
||||
table.EntitlementRecords.IsPaid,
|
||||
table.EntitlementRecords.Source,
|
||||
table.EntitlementRecords.ActorType,
|
||||
table.EntitlementRecords.ActorID,
|
||||
table.EntitlementRecords.ActorUserID,
|
||||
table.EntitlementRecords.ActorUsername,
|
||||
table.EntitlementRecords.ReasonCode,
|
||||
table.EntitlementRecords.StartsAt,
|
||||
table.EntitlementRecords.EndsAt,
|
||||
table.EntitlementRecords.CreatedAt,
|
||||
).VALUES(
|
||||
recordID, snap.UserID, snap.Tier, snap.IsPaid, snap.Source,
|
||||
snap.Actor.Type, actorID, snap.ReasonCode,
|
||||
snap.Actor.Type, actorUserID, actorUsername, snap.ReasonCode,
|
||||
snap.StartsAt, endsAt, snap.UpdatedAt,
|
||||
)
|
||||
if _, err := recordStmt.ExecContext(ctx, tx); err != nil {
|
||||
return fmt.Errorf("insert entitlement record: %w", err)
|
||||
}
|
||||
return upsertSnapshotTx(ctx, tx, snap)
|
||||
return upsertSnapshotTx(ctx, tx, snap, actorUserID, actorUsername)
|
||||
})
|
||||
if err != nil {
|
||||
return EntitlementSnapshot{}, err
|
||||
@@ -437,9 +441,12 @@ func (s *Store) ApplySanctionTx(ctx context.Context, input sanctionInsert) error
|
||||
if err := s.assertAccountLive(ctx, input.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
actorUserID, actorUsername, err := actorToColumnArgs(input.Actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return withTx(ctx, s.db, func(tx *sql.Tx) error {
|
||||
recordID := uuid.New()
|
||||
actorID := nullableString(input.ActorID)
|
||||
var expiresAt any
|
||||
if input.ExpiresAt != nil {
|
||||
expiresAt = *input.ExpiresAt
|
||||
@@ -451,12 +458,13 @@ func (s *Store) ApplySanctionTx(ctx context.Context, input sanctionInsert) error
|
||||
table.SanctionRecords.Scope,
|
||||
table.SanctionRecords.ReasonCode,
|
||||
table.SanctionRecords.ActorType,
|
||||
table.SanctionRecords.ActorID,
|
||||
table.SanctionRecords.ActorUserID,
|
||||
table.SanctionRecords.ActorUsername,
|
||||
table.SanctionRecords.AppliedAt,
|
||||
table.SanctionRecords.ExpiresAt,
|
||||
).VALUES(
|
||||
recordID, input.UserID, input.SanctionCode, input.Scope, input.ReasonCode,
|
||||
input.ActorType, actorID, input.AppliedAt, expiresAt,
|
||||
input.Actor.Type, actorUserID, actorUsername, input.AppliedAt, expiresAt,
|
||||
)
|
||||
if _, err := recordStmt.ExecContext(ctx, tx); err != nil {
|
||||
return fmt.Errorf("insert sanction record: %w", err)
|
||||
@@ -498,9 +506,12 @@ func (s *Store) ApplyLimitTx(ctx context.Context, input limitInsert) error {
|
||||
if err := s.assertAccountLive(ctx, input.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
actorUserID, actorUsername, err := actorToColumnArgs(input.Actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return withTx(ctx, s.db, func(tx *sql.Tx) error {
|
||||
recordID := uuid.New()
|
||||
actorID := nullableString(input.ActorID)
|
||||
var expiresAt any
|
||||
if input.ExpiresAt != nil {
|
||||
expiresAt = *input.ExpiresAt
|
||||
@@ -512,12 +523,13 @@ func (s *Store) ApplyLimitTx(ctx context.Context, input limitInsert) error {
|
||||
table.LimitRecords.Value,
|
||||
table.LimitRecords.ReasonCode,
|
||||
table.LimitRecords.ActorType,
|
||||
table.LimitRecords.ActorID,
|
||||
table.LimitRecords.ActorUserID,
|
||||
table.LimitRecords.ActorUsername,
|
||||
table.LimitRecords.AppliedAt,
|
||||
table.LimitRecords.ExpiresAt,
|
||||
).VALUES(
|
||||
recordID, input.UserID, input.LimitCode, input.Value, input.ReasonCode,
|
||||
input.ActorType, actorID, input.AppliedAt, expiresAt,
|
||||
input.Actor.Type, actorUserID, actorUsername, input.AppliedAt, expiresAt,
|
||||
)
|
||||
if _, err := recordStmt.ExecContext(ctx, tx); err != nil {
|
||||
return fmt.Errorf("insert limit record: %w", err)
|
||||
@@ -547,12 +559,16 @@ func (s *Store) ApplyLimitTx(ctx context.Context, input limitInsert) error {
|
||||
// successful idempotent operation.
|
||||
func (s *Store) SoftDeleteAccount(ctx context.Context, userID uuid.UUID, actor ActorRef, now time.Time) (bool, error) {
|
||||
a := table.Accounts
|
||||
actorIDExpr := nullableStringExpr(actor.ID)
|
||||
actorUserIDExpr, actorUsernameExpr, err := actorToColumnExprs(actor)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
stmt := a.UPDATE().
|
||||
SET(
|
||||
a.DeletedAt.SET(postgres.TimestampzT(now)),
|
||||
a.DeletedActorType.SET(postgres.String(actor.Type)),
|
||||
a.DeletedActorID.SET(actorIDExpr),
|
||||
a.DeletedActorUserID.SET(actorUserIDExpr),
|
||||
a.DeletedActorUsername.SET(actorUsernameExpr),
|
||||
a.UpdatedAt.SET(postgres.TimestampzT(now)),
|
||||
).
|
||||
WHERE(
|
||||
@@ -593,18 +609,23 @@ func (s *Store) assertAccountLive(ctx context.Context, userID uuid.UUID) error {
|
||||
}
|
||||
|
||||
func insertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot) error {
|
||||
actorUserID, actorUsername, err := actorToColumnArgs(snap.Actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es := table.EntitlementSnapshots
|
||||
actorID := nullableString(snap.Actor.ID)
|
||||
var endsAt any
|
||||
if snap.EndsAt != nil {
|
||||
endsAt = *snap.EndsAt
|
||||
}
|
||||
stmt := es.INSERT(
|
||||
es.UserID, es.Tier, es.IsPaid, es.Source, es.ActorType, es.ActorID,
|
||||
es.UserID, es.Tier, es.IsPaid, es.Source,
|
||||
es.ActorType, es.ActorUserID, es.ActorUsername,
|
||||
es.ReasonCode, es.StartsAt, es.EndsAt,
|
||||
es.MaxRegisteredRaceNames, es.UpdatedAt,
|
||||
).VALUES(
|
||||
snap.UserID, snap.Tier, snap.IsPaid, snap.Source, snap.Actor.Type, actorID,
|
||||
snap.UserID, snap.Tier, snap.IsPaid, snap.Source,
|
||||
snap.Actor.Type, actorUserID, actorUsername,
|
||||
snap.ReasonCode, snap.StartsAt, endsAt, snap.MaxRegisteredRaceNames, snap.UpdatedAt,
|
||||
)
|
||||
if _, err := stmt.ExecContext(ctx, tx); err != nil {
|
||||
@@ -613,19 +634,20 @@ func insertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot)
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot) error {
|
||||
func upsertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot, actorUserID, actorUsername any) error {
|
||||
es := table.EntitlementSnapshots
|
||||
actorID := nullableString(snap.Actor.ID)
|
||||
var endsAt any
|
||||
if snap.EndsAt != nil {
|
||||
endsAt = *snap.EndsAt
|
||||
}
|
||||
stmt := es.INSERT(
|
||||
es.UserID, es.Tier, es.IsPaid, es.Source, es.ActorType, es.ActorID,
|
||||
es.UserID, es.Tier, es.IsPaid, es.Source,
|
||||
es.ActorType, es.ActorUserID, es.ActorUsername,
|
||||
es.ReasonCode, es.StartsAt, es.EndsAt,
|
||||
es.MaxRegisteredRaceNames, es.UpdatedAt,
|
||||
).VALUES(
|
||||
snap.UserID, snap.Tier, snap.IsPaid, snap.Source, snap.Actor.Type, actorID,
|
||||
snap.UserID, snap.Tier, snap.IsPaid, snap.Source,
|
||||
snap.Actor.Type, actorUserID, actorUsername,
|
||||
snap.ReasonCode, snap.StartsAt, endsAt, snap.MaxRegisteredRaceNames, snap.UpdatedAt,
|
||||
).
|
||||
ON_CONFLICT(es.UserID).
|
||||
@@ -634,7 +656,8 @@ func upsertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot)
|
||||
es.IsPaid.SET(es.EXCLUDED.IsPaid),
|
||||
es.Source.SET(es.EXCLUDED.Source),
|
||||
es.ActorType.SET(es.EXCLUDED.ActorType),
|
||||
es.ActorID.SET(es.EXCLUDED.ActorID),
|
||||
es.ActorUserID.SET(es.EXCLUDED.ActorUserID),
|
||||
es.ActorUsername.SET(es.EXCLUDED.ActorUsername),
|
||||
es.ReasonCode.SET(es.EXCLUDED.ReasonCode),
|
||||
es.StartsAt.SET(es.EXCLUDED.StartsAt),
|
||||
es.EndsAt.SET(es.EXCLUDED.EndsAt),
|
||||
@@ -680,7 +703,7 @@ func modelToSnapshot(row model.EntitlementSnapshots) EntitlementSnapshot {
|
||||
Tier: row.Tier,
|
||||
IsPaid: row.IsPaid,
|
||||
Source: row.Source,
|
||||
Actor: ActorRef{Type: row.ActorType, ID: derefString(row.ActorID)},
|
||||
Actor: actorFromColumns(row.ActorType, row.ActorUserID, row.ActorUsername),
|
||||
ReasonCode: row.ReasonCode,
|
||||
StartsAt: row.StartsAt,
|
||||
MaxRegisteredRaceNames: row.MaxRegisteredRaceNames,
|
||||
@@ -693,31 +716,67 @@ func modelToSnapshot(row model.EntitlementSnapshots) EntitlementSnapshot {
|
||||
return out
|
||||
}
|
||||
|
||||
// nullableString converts a Go string to the `any` form expected by jet
|
||||
// VALUES: an empty string becomes nil so the column receives NULL.
|
||||
func nullableString(v string) any {
|
||||
if v == "" {
|
||||
return nil
|
||||
// actorToColumnArgs converts an ActorRef into the (actor_user_id,
|
||||
// actor_username) values for jet INSERT VALUES. A nil-typed `any` lands
|
||||
// as SQL NULL through the database/sql driver. Type=="user" parses ID
|
||||
// as a UUID; Type=="admin" stores ID verbatim as the username;
|
||||
// everything else (system, unknown) writes both columns as NULL. An
|
||||
// empty ID is allowed for "user" so synthetic system events that label
|
||||
// themselves as "user" do not fail.
|
||||
func actorToColumnArgs(actor ActorRef) (any, any, error) {
|
||||
switch strings.TrimSpace(actor.Type) {
|
||||
case "user":
|
||||
id := strings.TrimSpace(actor.ID)
|
||||
if id == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
uid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("user store: actor id %q is not a uuid: %w", actor.ID, err)
|
||||
}
|
||||
return uid, nil, nil
|
||||
case "admin":
|
||||
if strings.TrimSpace(actor.ID) == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return nil, actor.ID, nil
|
||||
default:
|
||||
return nil, nil, nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// nullableStringExpr returns a typed jet expression: the empty string
|
||||
// produces NULL, otherwise a String literal. Used by UPDATE SET paths
|
||||
// where jet's SET wants a typed Expression rather than `any`.
|
||||
func nullableStringExpr(v string) postgres.StringExpression {
|
||||
if v == "" {
|
||||
return postgres.StringExp(postgres.NULL)
|
||||
// actorToColumnExprs is the typed-expression analogue of
|
||||
// actorToColumnArgs for the UPDATE SET sites. jet's generated bindings
|
||||
// type uuid columns as ColumnString (the dialect emits an explicit
|
||||
// CAST), so both returned expressions are StringExpression.
|
||||
func actorToColumnExprs(actor ActorRef) (postgres.StringExpression, postgres.StringExpression, error) {
|
||||
uidArg, nameArg, err := actorToColumnArgs(actor)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return postgres.String(v)
|
||||
uidExpr := postgres.StringExp(postgres.NULL)
|
||||
if uid, ok := uidArg.(uuid.UUID); ok {
|
||||
uidExpr = postgres.UUID(uid)
|
||||
}
|
||||
nameExpr := postgres.StringExp(postgres.NULL)
|
||||
if name, ok := nameArg.(string); ok {
|
||||
nameExpr = postgres.String(name)
|
||||
}
|
||||
return uidExpr, nameExpr, nil
|
||||
}
|
||||
|
||||
// derefString returns the empty string when p is nil, otherwise *p.
|
||||
func derefString(p *string) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
// actorFromColumns reconstructs an ActorRef from the (actor_type,
|
||||
// actor_user_id, actor_username) triple read from an audit row. The
|
||||
// non-nil column wins; both nil yields an empty ID.
|
||||
func actorFromColumns(actorType string, userID *uuid.UUID, username *string) ActorRef {
|
||||
out := ActorRef{Type: actorType}
|
||||
switch {
|
||||
case userID != nil:
|
||||
out.ID = userID.String()
|
||||
case username != nil:
|
||||
out.ID = *username
|
||||
}
|
||||
return *p
|
||||
return out
|
||||
}
|
||||
|
||||
// rowsAffectedOrNotFound returns ErrAccountNotFound when the UPDATE
|
||||
|
||||
Reference in New Issue
Block a user