package invite import ( "context" "database/sql" "log/slog" "time" ) type Queriable interface { ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } type Repository struct { Db *sql.DB q Queriable } func NewRepository(db *sql.DB) (*Repository, error) { return &Repository{Db: db, q: db}, nil } func (r *Repository) WithTx(tx *sql.Tx) *Repository { return &Repository{Db: r.Db, q: tx} } func (r *Repository) CreateInvite(ctx context.Context, token string, quota int, expiresAt time.Time) (*Invite, error) { nowTime := time.Now().UTC() query := "INSERT INTO invites(token, quota, uses, expires_at, created_at) VALUES(?, ?, ?, ?, ?)" invite := &Invite{ Token: token, Quota: quota, Uses: 0, ExpiresAt: expiresAt, CreatedAt: nowTime, } _, err := r.q.ExecContext(ctx, query, invite.Token, invite.Quota, invite.Uses, invite.ExpiresAt, invite.CreatedAt) if err != nil { slog.Error("create invite", slog.String("op", "repo.CreateInvite"), slog.String("token", token), slog.Any("error", err)) return nil, ErrInternal } return invite, nil } func (r *Repository) CountInvites(ctx context.Context) (int64, error) { query := "SELECT COUNT(*) FROM invites" var result int64 err := r.q.QueryRowContext(ctx, query).Scan(&result) if err != nil { slog.Error("count invites", slog.String("op", "repo.CountInvites"), slog.Any("error", err)) return 0, ErrInternal } return result, nil } func (r *Repository) Invites(ctx context.Context, limit, offset int64) ([]*Invite, error) { query := "SELECT token, quota, uses, expires_at, created_at FROM invites" var args []any if limit > 0 { query += " LIMIT ? OFFSET ?" args = append(args, limit, offset) } rows, err := r.Db.QueryContext(ctx, query, args...) if err != nil { slog.Error("list invites", slog.String("op", "repo.Invites"), slog.Any("error", err)) return nil, ErrInternal } defer rows.Close() var invites []*Invite for rows.Next() { invite := &Invite{} err := rows.Scan(&invite.Token, &invite.Quota, &invite.Uses, &invite.ExpiresAt, &invite.CreatedAt) if err != nil { slog.Error("scan invite", slog.String("op", "repo.Invites"), slog.Any("error", err)) return nil, ErrInternal } invites = append(invites, invite) } err = rows.Err() if err != nil { slog.Error("iterate invites", slog.String("op", "repo.Invites"), slog.Any("error", err)) return nil, ErrInternal } return invites, nil } func (r *Repository) InviteByToken(ctx context.Context, token string) (*Invite, error) { query := "SELECT token, quota, uses, expires_at, created_at FROM invites WHERE token = ?" invite := &Invite{} err := r.q.QueryRowContext(ctx, query, token).Scan(&invite.Token, &invite.Quota, &invite.Uses, &invite.ExpiresAt, &invite.CreatedAt) if err != nil { return nil, ErrNotFound } return invite, nil } func (r *Repository) UpdateInvite(ctx context.Context, invite *Invite) error { query := "UPDATE invites SET uses = ? WHERE token = ?" _, err := r.q.ExecContext(ctx, query, invite.Uses, invite.Token) if err != nil { slog.Error("update invite", slog.String("op", "repo.UpdateInvite"), slog.String("token", invite.Token), slog.Any("error", err)) return ErrNotFound } return nil } func (r *Repository) DeleteInvite(ctx context.Context, token string) error { query := "DELETE FROM invites WHERE token = ?" _, err := r.q.ExecContext(ctx, query, token) if err != nil { slog.Error("delete invite", slog.String("op", "repo.DeleteInvite"), slog.String("token", token), slog.Any("error", err)) return ErrNotFound } return nil } func (r *Repository) CleanupInvites(ctx context.Context) error { query := "DELETE FROM invites WHERE expires_at < ? OR uses >= quota" _, err := r.q.ExecContext(ctx, query, time.Now().UTC()) if err != nil { slog.Error("cleanup invites", slog.String("op", "repo.CleanupInvites"), slog.Any("error", err)) return ErrInternal } return nil }