package waitlist import ( "context" "encoding/json" "errors" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" ) var errNotFound = errors.New("not found") var errAlreadyExists = errors.New("already exists") type repository interface { create(ctx context.Context, email string, metadata map[string]string) (*WaitlistEntry, error) getAll(ctx context.Context, filter GetWaitlistFilter) ([]*WaitlistEntry, error) getByEmail(ctx context.Context, email string) (*WaitlistEntry, error) markInvited(ctx context.Context, email string) error } type repositoryImpl struct { pool *pgxpool.Pool } func newRepository(pool *pgxpool.Pool) repository { return &repositoryImpl{pool: pool} } func (r *repositoryImpl) create(ctx context.Context, email string, metadata map[string]string) (*WaitlistEntry, error) { metadataJSON, err := json.Marshal(metadata) if err != nil { return nil, err } var entry WaitlistEntry var metadataBytes []byte err = r.pool.QueryRow(ctx, `INSERT INTO waitlist_entries (email, metadata) VALUES ($1, $2) RETURNING id, email, metadata, created_at, invited_at`, email, metadataJSON, ).Scan(&entry.Id, &entry.Email, &metadataBytes, &entry.CreatedAt, &entry.InvitedAt) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23505" { return nil, errAlreadyExists } return nil, err } if err := json.Unmarshal(metadataBytes, &entry.Metadata); err != nil { return nil, err } return &entry, nil } func (r *repositoryImpl) getAll(ctx context.Context, filter GetWaitlistFilter) ([]*WaitlistEntry, error) { query := `SELECT id, email, metadata, created_at, invited_at FROM waitlist_entries` switch filter { case GetWaitlistFilterInvitedOnly: query += ` WHERE invited_at IS NOT NULL` case GetWaitlistFilterUninvitedOnly: query += ` WHERE invited_at IS NULL` } query += ` ORDER BY created_at DESC` rows, err := r.pool.Query(ctx, query) if err != nil { return nil, err } defer rows.Close() var entries []*WaitlistEntry for rows.Next() { var entry WaitlistEntry var metadataBytes []byte if err := rows.Scan(&entry.Id, &entry.Email, &metadataBytes, &entry.CreatedAt, &entry.InvitedAt); err != nil { return nil, err } if err := json.Unmarshal(metadataBytes, &entry.Metadata); err != nil { return nil, err } entries = append(entries, &entry) } return entries, rows.Err() } func (r *repositoryImpl) getByEmail(ctx context.Context, email string) (*WaitlistEntry, error) { var entry WaitlistEntry var metadataBytes []byte err := r.pool.QueryRow(ctx, `SELECT id, email, metadata, created_at, invited_at FROM waitlist_entries WHERE email = $1`, email, ).Scan(&entry.Id, &entry.Email, &metadataBytes, &entry.CreatedAt, &entry.InvitedAt) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, errNotFound } return nil, err } if err := json.Unmarshal(metadataBytes, &entry.Metadata); err != nil { return nil, err } return &entry, nil } func (r *repositoryImpl) markInvited(ctx context.Context, email string) error { result, err := r.pool.Exec(ctx, `UPDATE waitlist_entries SET invited_at = NOW() WHERE email = $1`, email, ) if err != nil { return err } if result.RowsAffected() == 0 { return errNotFound } return nil }