136 lines
4.1 KiB
Go
136 lines
4.1 KiB
Go
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
|
||
|
|
}
|