package postgres import ( "context" "database/sql" "errors" "github.com/google/uuid" "github.com/lib/pq" "github.com/rwadurian/mpc-system/services/account/domain/entities" "github.com/rwadurian/mpc-system/services/account/domain/repositories" "github.com/rwadurian/mpc-system/services/account/domain/value_objects" ) // AccountPostgresRepo implements AccountRepository using PostgreSQL type AccountPostgresRepo struct { db *sql.DB } // NewAccountPostgresRepo creates a new AccountPostgresRepo func NewAccountPostgresRepo(db *sql.DB) repositories.AccountRepository { return &AccountPostgresRepo{db: db} } // Create creates a new account func (r *AccountPostgresRepo) Create(ctx context.Context, account *entities.Account) error { query := ` INSERT INTO accounts (id, username, email, phone, public_key, keygen_session_id, threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) ` _, err := r.db.ExecContext(ctx, query, account.ID.UUID(), account.Username, account.Email, account.Phone, account.PublicKey, account.KeygenSessionID, account.ThresholdN, account.ThresholdT, account.Status.String(), account.CreatedAt, account.UpdatedAt, account.LastLoginAt, pq.Array(account.SigningParties), ) return err } // GetByID retrieves an account by ID func (r *AccountPostgresRepo) GetByID(ctx context.Context, id value_objects.AccountID) (*entities.Account, error) { query := ` SELECT id, username, email, phone, public_key, keygen_session_id, threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties FROM accounts WHERE id = $1 ` return r.scanAccount(r.db.QueryRowContext(ctx, query, id.UUID())) } // GetByUsername retrieves an account by username func (r *AccountPostgresRepo) GetByUsername(ctx context.Context, username string) (*entities.Account, error) { query := ` SELECT id, username, email, phone, public_key, keygen_session_id, threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties FROM accounts WHERE username = $1 ` return r.scanAccount(r.db.QueryRowContext(ctx, query, username)) } // GetByEmail retrieves an account by email func (r *AccountPostgresRepo) GetByEmail(ctx context.Context, email string) (*entities.Account, error) { query := ` SELECT id, username, email, phone, public_key, keygen_session_id, threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties FROM accounts WHERE email = $1 ` return r.scanAccount(r.db.QueryRowContext(ctx, query, email)) } // GetByPublicKey retrieves an account by public key func (r *AccountPostgresRepo) GetByPublicKey(ctx context.Context, publicKey []byte) (*entities.Account, error) { query := ` SELECT id, username, email, phone, public_key, keygen_session_id, threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties FROM accounts WHERE public_key = $1 ` return r.scanAccount(r.db.QueryRowContext(ctx, query, publicKey)) } // Update updates an existing account func (r *AccountPostgresRepo) Update(ctx context.Context, account *entities.Account) error { query := ` UPDATE accounts SET username = $2, email = $3, phone = $4, public_key = $5, keygen_session_id = $6, threshold_n = $7, threshold_t = $8, status = $9, updated_at = $10, last_login_at = $11, signing_parties = $12 WHERE id = $1 ` result, err := r.db.ExecContext(ctx, query, account.ID.UUID(), account.Username, account.Email, account.Phone, account.PublicKey, account.KeygenSessionID, account.ThresholdN, account.ThresholdT, account.Status.String(), account.UpdatedAt, account.LastLoginAt, pq.Array(account.SigningParties), ) if err != nil { return err } rowsAffected, err := result.RowsAffected() if err != nil { return err } if rowsAffected == 0 { return entities.ErrAccountNotFound } return nil } // Delete deletes an account func (r *AccountPostgresRepo) Delete(ctx context.Context, id value_objects.AccountID) error { query := `DELETE FROM accounts WHERE id = $1` result, err := r.db.ExecContext(ctx, query, id.UUID()) if err != nil { return err } rowsAffected, err := result.RowsAffected() if err != nil { return err } if rowsAffected == 0 { return entities.ErrAccountNotFound } return nil } // ExistsByUsername checks if username exists func (r *AccountPostgresRepo) ExistsByUsername(ctx context.Context, username string) (bool, error) { query := `SELECT EXISTS(SELECT 1 FROM accounts WHERE username = $1)` var exists bool err := r.db.QueryRowContext(ctx, query, username).Scan(&exists) return exists, err } // ExistsByEmail checks if email exists func (r *AccountPostgresRepo) ExistsByEmail(ctx context.Context, email string) (bool, error) { query := `SELECT EXISTS(SELECT 1 FROM accounts WHERE email = $1)` var exists bool err := r.db.QueryRowContext(ctx, query, email).Scan(&exists) return exists, err } // List lists accounts with pagination func (r *AccountPostgresRepo) List(ctx context.Context, offset, limit int) ([]*entities.Account, error) { query := ` SELECT id, username, email, phone, public_key, keygen_session_id, threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties FROM accounts ORDER BY created_at DESC LIMIT $1 OFFSET $2 ` rows, err := r.db.QueryContext(ctx, query, limit, offset) if err != nil { return nil, err } defer rows.Close() var accounts []*entities.Account for rows.Next() { account, err := r.scanAccountFromRows(rows) if err != nil { return nil, err } accounts = append(accounts, account) } return accounts, rows.Err() } // Count returns the total number of accounts func (r *AccountPostgresRepo) Count(ctx context.Context) (int64, error) { query := `SELECT COUNT(*) FROM accounts` var count int64 err := r.db.QueryRowContext(ctx, query).Scan(&count) return count, err } // scanAccount scans a single account row func (r *AccountPostgresRepo) scanAccount(row *sql.Row) (*entities.Account, error) { var ( id uuid.UUID username string email sql.NullString phone sql.NullString publicKey []byte keygenSessionID uuid.UUID thresholdN int thresholdT int status string signingParties pq.StringArray account entities.Account ) err := row.Scan( &id, &username, &email, &phone, &publicKey, &keygenSessionID, &thresholdN, &thresholdT, &status, &account.CreatedAt, &account.UpdatedAt, &account.LastLoginAt, &signingParties, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, entities.ErrAccountNotFound } return nil, err } account.ID = value_objects.AccountIDFromUUID(id) account.Username = username if email.Valid { account.Email = &email.String } if phone.Valid { account.Phone = &phone.String } account.PublicKey = publicKey account.KeygenSessionID = keygenSessionID account.ThresholdN = thresholdN account.ThresholdT = thresholdT account.Status = value_objects.AccountStatus(status) account.SigningParties = signingParties return &account, nil } // scanAccountFromRows scans account from rows func (r *AccountPostgresRepo) scanAccountFromRows(rows *sql.Rows) (*entities.Account, error) { var ( id uuid.UUID username string email sql.NullString phone sql.NullString publicKey []byte keygenSessionID uuid.UUID thresholdN int thresholdT int status string signingParties pq.StringArray account entities.Account ) err := rows.Scan( &id, &username, &email, &phone, &publicKey, &keygenSessionID, &thresholdN, &thresholdT, &status, &account.CreatedAt, &account.UpdatedAt, &account.LastLoginAt, &signingParties, ) if err != nil { return nil, err } account.ID = value_objects.AccountIDFromUUID(id) account.Username = username if email.Valid { account.Email = &email.String } if phone.Valid { account.Phone = &phone.String } account.PublicKey = publicKey account.KeygenSessionID = keygenSessionID account.ThresholdN = thresholdN account.ThresholdT = thresholdT account.Status = value_objects.AccountStatus(status) account.SigningParties = signingParties return &account, nil }