feat: use postgres
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package userstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
pgtable "galaxy/user/internal/adapters/postgres/jet/user/table"
|
||||
"galaxy/user/internal/domain/authblock"
|
||||
"galaxy/user/internal/domain/common"
|
||||
"galaxy/user/internal/ports"
|
||||
|
||||
pg "github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
// blockedEmailSelectColumns is the canonical SELECT list for blocked_emails.
|
||||
var blockedEmailSelectColumns = pg.ColumnList{
|
||||
pgtable.BlockedEmails.Email,
|
||||
pgtable.BlockedEmails.ReasonCode,
|
||||
pgtable.BlockedEmails.BlockedAt,
|
||||
pgtable.BlockedEmails.ActorType,
|
||||
pgtable.BlockedEmails.ActorID,
|
||||
pgtable.BlockedEmails.ResolvedUserID,
|
||||
}
|
||||
|
||||
// GetBlockedEmail returns the blocked-email subject for email.
|
||||
func (store *Store) GetBlockedEmail(ctx context.Context, email common.Email) (authblock.BlockedEmailSubject, error) {
|
||||
if err := email.Validate(); err != nil {
|
||||
return authblock.BlockedEmailSubject{}, fmt.Errorf("get blocked email subject from postgres: %w", err)
|
||||
}
|
||||
|
||||
operationCtx, cancel, err := store.operationContext(ctx, "get blocked email subject from postgres")
|
||||
if err != nil {
|
||||
return authblock.BlockedEmailSubject{}, err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
record, err := scanBlockedEmail(operationCtx, store.db, email, false)
|
||||
switch {
|
||||
case errors.Is(err, ports.ErrNotFound):
|
||||
return authblock.BlockedEmailSubject{}, fmt.Errorf("get blocked email subject %q from postgres: %w", email, ports.ErrNotFound)
|
||||
case err != nil:
|
||||
return authblock.BlockedEmailSubject{}, fmt.Errorf("get blocked email subject %q from postgres: %w", email, err)
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// PutBlockedEmail stores or replaces the blocked-email subject for
|
||||
// record.Email. The schema's PRIMARY KEY on (email) makes this an UPSERT via
|
||||
// `INSERT … ON CONFLICT (email) DO UPDATE`.
|
||||
func (store *Store) PutBlockedEmail(ctx context.Context, record authblock.BlockedEmailSubject) error {
|
||||
if err := record.Validate(); err != nil {
|
||||
return fmt.Errorf("upsert blocked email subject in postgres: %w", err)
|
||||
}
|
||||
|
||||
operationCtx, cancel, err := store.operationContext(ctx, "upsert blocked email subject in postgres")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
if err := upsertBlockedEmail(operationCtx, store.db, record); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// upsertBlockedEmail centralises the UPSERT used by PutBlockedEmail and the
|
||||
// composite block flows. q is a *sql.DB or *sql.Tx so it can run inside an
|
||||
// auth-directory transaction.
|
||||
func upsertBlockedEmail(ctx context.Context, q queryer, record authblock.BlockedEmailSubject) error {
|
||||
stmt := pgtable.BlockedEmails.INSERT(
|
||||
pgtable.BlockedEmails.Email,
|
||||
pgtable.BlockedEmails.ReasonCode,
|
||||
pgtable.BlockedEmails.BlockedAt,
|
||||
pgtable.BlockedEmails.ActorType,
|
||||
pgtable.BlockedEmails.ActorID,
|
||||
pgtable.BlockedEmails.ResolvedUserID,
|
||||
).VALUES(
|
||||
record.Email.String(),
|
||||
record.ReasonCode.String(),
|
||||
record.BlockedAt.UTC(),
|
||||
nullableActorType(record.Actor.Type),
|
||||
nullableActorID(record.Actor.ID),
|
||||
nullableUserID(record.ResolvedUserID),
|
||||
).ON_CONFLICT(pgtable.BlockedEmails.Email).DO_UPDATE(
|
||||
pg.SET(
|
||||
pgtable.BlockedEmails.ReasonCode.SET(pgtable.BlockedEmails.EXCLUDED.ReasonCode),
|
||||
pgtable.BlockedEmails.BlockedAt.SET(pgtable.BlockedEmails.EXCLUDED.BlockedAt),
|
||||
pgtable.BlockedEmails.ActorType.SET(pgtable.BlockedEmails.EXCLUDED.ActorType),
|
||||
pgtable.BlockedEmails.ActorID.SET(pgtable.BlockedEmails.EXCLUDED.ActorID),
|
||||
pgtable.BlockedEmails.ResolvedUserID.SET(pgtable.BlockedEmails.EXCLUDED.ResolvedUserID),
|
||||
),
|
||||
)
|
||||
|
||||
query, args := stmt.Sql()
|
||||
if _, err := q.ExecContext(ctx, query, args...); err != nil {
|
||||
return fmt.Errorf("upsert blocked email subject %q in postgres: %w", record.Email, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanBlockedEmail loads one blocked-email row. forUpdate selects the
|
||||
// `FOR UPDATE` lock variant used inside the auth-directory transaction.
|
||||
func scanBlockedEmail(ctx context.Context, q queryer, email common.Email, forUpdate bool) (authblock.BlockedEmailSubject, error) {
|
||||
stmt := pg.SELECT(blockedEmailSelectColumns).
|
||||
FROM(pgtable.BlockedEmails).
|
||||
WHERE(pgtable.BlockedEmails.Email.EQ(pg.String(email.String())))
|
||||
if forUpdate {
|
||||
stmt = stmt.FOR(pg.UPDATE())
|
||||
}
|
||||
query, args := stmt.Sql()
|
||||
row := q.QueryRowContext(ctx, query, args...)
|
||||
return scanBlockedEmailRow(row)
|
||||
}
|
||||
|
||||
func scanBlockedEmailRow(row *sql.Row) (authblock.BlockedEmailSubject, error) {
|
||||
var (
|
||||
record authblock.BlockedEmailSubject
|
||||
emailValue string
|
||||
reasonCode string
|
||||
blockedAt time.Time
|
||||
actorType *string
|
||||
actorID *string
|
||||
resolvedUserID *string
|
||||
)
|
||||
if err := row.Scan(
|
||||
&emailValue, &reasonCode, &blockedAt,
|
||||
&actorType, &actorID, &resolvedUserID,
|
||||
); err != nil {
|
||||
return authblock.BlockedEmailSubject{}, mapNotFound(err)
|
||||
}
|
||||
|
||||
record.Email = common.Email(emailValue)
|
||||
record.ReasonCode = common.ReasonCode(reasonCode)
|
||||
record.BlockedAt = blockedAt.UTC()
|
||||
if actorType != nil {
|
||||
record.Actor.Type = common.ActorType(*actorType)
|
||||
}
|
||||
if actorID != nil {
|
||||
record.Actor.ID = common.ActorID(*actorID)
|
||||
}
|
||||
if resolvedUserID != nil {
|
||||
record.ResolvedUserID = common.UserID(*resolvedUserID)
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// BlockedEmailStore adapts Store to the BlockedEmailStore port.
|
||||
type BlockedEmailStore struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// BlockedEmails returns one adapter that exposes the blocked-email store
|
||||
// port over Store.
|
||||
func (store *Store) BlockedEmails() *BlockedEmailStore {
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return &BlockedEmailStore{store: store}
|
||||
}
|
||||
|
||||
// GetByEmail returns the blocked-email subject for email.
|
||||
func (adapter *BlockedEmailStore) GetByEmail(ctx context.Context, email common.Email) (authblock.BlockedEmailSubject, error) {
|
||||
return adapter.store.GetBlockedEmail(ctx, email)
|
||||
}
|
||||
|
||||
// Upsert stores or replaces the blocked-email subject for record.Email.
|
||||
func (adapter *BlockedEmailStore) Upsert(ctx context.Context, record authblock.BlockedEmailSubject) error {
|
||||
return adapter.store.PutBlockedEmail(ctx, record)
|
||||
}
|
||||
|
||||
var _ ports.BlockedEmailStore = (*BlockedEmailStore)(nil)
|
||||
Reference in New Issue
Block a user