feat: user service
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
// Package redisstate defines the frozen Redis logical keyspace and pagination
|
||||
// helpers used by future User Service storage adapters.
|
||||
package redisstate
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/user/internal/domain/account"
|
||||
"galaxy/user/internal/domain/common"
|
||||
"galaxy/user/internal/domain/entitlement"
|
||||
"galaxy/user/internal/domain/policy"
|
||||
)
|
||||
|
||||
const defaultPrefix = "user:"
|
||||
|
||||
// Keyspace builds the frozen Redis logical keys used by future storage
|
||||
// adapters. The package intentionally exposes key construction only and does
|
||||
// not depend on any Redis client.
|
||||
type Keyspace struct {
|
||||
// Prefix stores the namespace prefix applied to every key. The zero value
|
||||
// uses `user:`.
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Account returns the primary user-account key for userID.
|
||||
func (k Keyspace) Account(userID common.UserID) string {
|
||||
return k.prefix() + "account:" + encodeKeyComponent(userID.String())
|
||||
}
|
||||
|
||||
// EmailLookup returns the exact normalized e-mail lookup key.
|
||||
func (k Keyspace) EmailLookup(email common.Email) string {
|
||||
return k.prefix() + "lookup:email:" + encodeKeyComponent(email.String())
|
||||
}
|
||||
|
||||
// RaceNameLookup returns the exact stored race-name lookup key.
|
||||
func (k Keyspace) RaceNameLookup(raceName common.RaceName) string {
|
||||
return k.prefix() + "lookup:race-name:" + encodeKeyComponent(raceName.String())
|
||||
}
|
||||
|
||||
// RaceNameReservation returns the replaceable canonical race-name reservation
|
||||
// key.
|
||||
func (k Keyspace) RaceNameReservation(key account.RaceNameCanonicalKey) string {
|
||||
return k.prefix() + "reservation:race-name:" + encodeKeyComponent(key.String())
|
||||
}
|
||||
|
||||
// BlockedEmailSubject returns the dedicated blocked-email-subject key.
|
||||
func (k Keyspace) BlockedEmailSubject(email common.Email) string {
|
||||
return k.prefix() + "blocked-email:" + encodeKeyComponent(email.String())
|
||||
}
|
||||
|
||||
// EntitlementRecord returns the primary entitlement history-record key.
|
||||
func (k Keyspace) EntitlementRecord(recordID entitlement.EntitlementRecordID) string {
|
||||
return k.prefix() + "entitlement:record:" + encodeKeyComponent(recordID.String())
|
||||
}
|
||||
|
||||
// EntitlementHistory returns the per-user entitlement-history index key.
|
||||
func (k Keyspace) EntitlementHistory(userID common.UserID) string {
|
||||
return k.prefix() + "entitlement:history:" + encodeKeyComponent(userID.String())
|
||||
}
|
||||
|
||||
// EntitlementSnapshot returns the current entitlement-snapshot key.
|
||||
func (k Keyspace) EntitlementSnapshot(userID common.UserID) string {
|
||||
return k.prefix() + "entitlement:snapshot:" + encodeKeyComponent(userID.String())
|
||||
}
|
||||
|
||||
// SanctionRecord returns the primary sanction history-record key.
|
||||
func (k Keyspace) SanctionRecord(recordID policy.SanctionRecordID) string {
|
||||
return k.prefix() + "sanction:record:" + encodeKeyComponent(recordID.String())
|
||||
}
|
||||
|
||||
// SanctionHistory returns the per-user sanction-history index key.
|
||||
func (k Keyspace) SanctionHistory(userID common.UserID) string {
|
||||
return k.prefix() + "sanction:history:" + encodeKeyComponent(userID.String())
|
||||
}
|
||||
|
||||
// ActiveSanction returns the per-user active-sanction slot for one sanction
|
||||
// code. The slot guarantees at most one active sanction per `user_id +
|
||||
// sanction_code`.
|
||||
func (k Keyspace) ActiveSanction(userID common.UserID, code policy.SanctionCode) string {
|
||||
return k.prefix() + "sanction:active:" + encodeKeyComponent(userID.String()) + ":" + encodeKeyComponent(string(code))
|
||||
}
|
||||
|
||||
// LimitRecord returns the primary limit history-record key.
|
||||
func (k Keyspace) LimitRecord(recordID policy.LimitRecordID) string {
|
||||
return k.prefix() + "limit:record:" + encodeKeyComponent(recordID.String())
|
||||
}
|
||||
|
||||
// LimitHistory returns the per-user limit-history index key.
|
||||
func (k Keyspace) LimitHistory(userID common.UserID) string {
|
||||
return k.prefix() + "limit:history:" + encodeKeyComponent(userID.String())
|
||||
}
|
||||
|
||||
// ActiveLimit returns the per-user active-limit slot for one limit code. The
|
||||
// slot guarantees at most one active limit per `user_id + limit_code`.
|
||||
func (k Keyspace) ActiveLimit(userID common.UserID, code policy.LimitCode) string {
|
||||
return k.prefix() + "limit:active:" + encodeKeyComponent(userID.String()) + ":" + encodeKeyComponent(string(code))
|
||||
}
|
||||
|
||||
// CreatedAtIndex returns the deterministic newest-first user-ordering index.
|
||||
func (k Keyspace) CreatedAtIndex() string {
|
||||
return k.prefix() + "index:created-at"
|
||||
}
|
||||
|
||||
// PaidStateIndex returns the coarse free-versus-paid index key.
|
||||
func (k Keyspace) PaidStateIndex(state entitlement.PaidState) string {
|
||||
return k.prefix() + "index:paid-state:" + encodeKeyComponent(string(state))
|
||||
}
|
||||
|
||||
// FinitePaidExpiryIndex returns the finite paid-expiry index key. Lifetime
|
||||
// plans intentionally do not participate in this index.
|
||||
func (k Keyspace) FinitePaidExpiryIndex() string {
|
||||
return k.prefix() + "index:paid-expiry:finite"
|
||||
}
|
||||
|
||||
// DeclaredCountryIndex returns the current declared-country reverse-lookup
|
||||
// index key.
|
||||
func (k Keyspace) DeclaredCountryIndex(code common.CountryCode) string {
|
||||
return k.prefix() + "index:declared-country:" + encodeKeyComponent(code.String())
|
||||
}
|
||||
|
||||
// ActiveSanctionCodeIndex returns the reverse-lookup index key for users with
|
||||
// an active sanction code.
|
||||
func (k Keyspace) ActiveSanctionCodeIndex(code policy.SanctionCode) string {
|
||||
return k.prefix() + "index:active-sanction:" + encodeKeyComponent(string(code))
|
||||
}
|
||||
|
||||
// ActiveLimitCodeIndex returns the reverse-lookup index key for users with an
|
||||
// active limit code.
|
||||
func (k Keyspace) ActiveLimitCodeIndex(code policy.LimitCode) string {
|
||||
return k.prefix() + "index:active-limit:" + encodeKeyComponent(string(code))
|
||||
}
|
||||
|
||||
// EligibilityMarkerIndex returns the reverse-lookup index key for one derived
|
||||
// eligibility marker boolean.
|
||||
func (k Keyspace) EligibilityMarkerIndex(marker policy.EligibilityMarker, value bool) string {
|
||||
return fmt.Sprintf("%sindex:eligibility:%s:%t", k.prefix(), encodeKeyComponent(string(marker)), value)
|
||||
}
|
||||
|
||||
// CreatedAtScore returns the frozen ZSET score representation for created-at
|
||||
// ordering and deterministic pagination.
|
||||
func CreatedAtScore(createdAt time.Time) float64 {
|
||||
return float64(createdAt.UTC().UnixMicro())
|
||||
}
|
||||
|
||||
// ExpiryScore returns the frozen ZSET score representation for finite paid
|
||||
// expiry ordering.
|
||||
func ExpiryScore(expiresAt time.Time) float64 {
|
||||
return float64(expiresAt.UTC().UnixMicro())
|
||||
}
|
||||
|
||||
// PageCursor identifies the last seen `(created_at, user_id)` tuple used by
|
||||
// deterministic newest-first pagination.
|
||||
type PageCursor struct {
|
||||
// CreatedAt stores the created-at component of the last seen row.
|
||||
CreatedAt time.Time
|
||||
|
||||
// UserID stores the user-id tiebreaker component of the last seen row.
|
||||
UserID common.UserID
|
||||
}
|
||||
|
||||
// Validate reports whether PageCursor contains a complete cursor tuple.
|
||||
func (cursor PageCursor) Validate() error {
|
||||
if err := common.ValidateTimestamp("page cursor created at", cursor.CreatedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cursor.UserID.Validate(); err != nil {
|
||||
return fmt.Errorf("page cursor user id: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComparePageOrder compares two listing positions using the frozen ordering:
|
||||
// `created_at desc`, then `user_id desc`.
|
||||
func ComparePageOrder(left PageCursor, right PageCursor) int {
|
||||
switch {
|
||||
case left.CreatedAt.After(right.CreatedAt):
|
||||
return -1
|
||||
case left.CreatedAt.Before(right.CreatedAt):
|
||||
return 1
|
||||
default:
|
||||
return -strings.Compare(left.UserID.String(), right.UserID.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (k Keyspace) prefix() string {
|
||||
prefix := strings.TrimSpace(k.Prefix)
|
||||
if prefix == "" {
|
||||
return defaultPrefix
|
||||
}
|
||||
|
||||
return prefix
|
||||
}
|
||||
|
||||
func encodeKeyComponent(value string) string {
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(value))
|
||||
}
|
||||
Reference in New Issue
Block a user