feat: sqlite: implement GetUserByEmail

This commit is contained in:
juancwu 2026-05-06 00:37:47 +00:00
commit 7c9b7aa154
3 changed files with 61 additions and 4 deletions

View file

@ -3,6 +3,7 @@ package store
type Queries struct { type Queries struct {
CreateUser string CreateUser string
GetUserByID string GetUserByID string
GetUserByEmail string
} }
var CanonicalQueries Queries = Queries{ var CanonicalQueries Queries = Queries{
@ -23,11 +24,21 @@ var CanonicalQueries Queries = Queries{
created_at, updated_at created_at, updated_at
FROM pase_users FROM pase_users
WHERE id = ?;`, WHERE id = ?;`,
GetUserByEmail: `
SELECT
id, email, email_verified_at,
username, username_normalized, display_name, profile_image_url,
status, status_reason, status_changed_at, status_expires_at,
failed_login_count, last_failed_login_at,
created_at, updated_at
FROM pase_users
WHERE email = ?;`,
} }
func (q Queries) Rebind(d Dialect) Queries { func (q Queries) Rebind(d Dialect) Queries {
return Queries{ return Queries{
CreateUser: d.Rebind(q.CreateUser), CreateUser: d.Rebind(q.CreateUser),
GetUserByID: d.Rebind(q.GetUserByID), GetUserByID: d.Rebind(q.GetUserByID),
GetUserByEmail: d.Rebind(q.GetUserByEmail),
} }
} }

View file

@ -92,3 +92,31 @@ func (s *Store) GetUserByID(ctx context.Context, id string) (*store.User, error)
} }
return &u, nil return &u, nil
} }
func (s *Store) GetUserByEmail(ctx context.Context, email string) (*store.User, error) {
row := s.db.QueryRowContext(ctx, s.q.GetUserByEmail, email)
u, err := s.scanUser(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("pase/sqlite: get user by email: %w", store.ErrUserNotFound)
}
return nil, fmt.Errorf("pase/sqlite: get user by email: %w", err)
}
return u, nil
}
func (s *Store) scanUser(row *sql.Row) (*store.User, error) {
var u store.User
err := row.Scan(
&u.ID, &u.Email, &u.EmailVerifiedAt,
&u.Username, &u.UsernameNormalized, &u.DisplayName, &u.ProfileImageURL,
&u.Status, &u.StatusReason,
&u.StatusChangedAt, &u.StatusExpiresAt,
&u.FailedLoginCount, &u.LastFailedLoginAt,
&u.CreatedAt, &u.UpdatedAt,
)
if err != nil {
return nil, err
}
return &u, nil
}

View file

@ -28,6 +28,7 @@ import (
type SuiteStore interface { type SuiteStore interface {
CreateUser(ctx context.Context, u *store.User) error CreateUser(ctx context.Context, u *store.User) error
GetUserByID(ctx context.Context, id string) (*store.User, error) GetUserByID(ctx context.Context, id string) (*store.User, error)
GetUserByEmail(ctx context.Context, email string) (*store.User, error)
} }
// Factory returns a fresh, isolated Store. Each call to the factory must // Factory returns a fresh, isolated Store. Each call to the factory must
@ -53,6 +54,7 @@ func RunSuite(t *testing.T, newStore Factory) {
{"GetUserByID_notFound", testGetUserByIDNotFound}, {"GetUserByID_notFound", testGetUserByIDNotFound},
{"CreateUser_duplicateEmail", testCreateUserDuplicateEmail}, {"CreateUser_duplicateEmail", testCreateUserDuplicateEmail},
{"CreateUser_duplicateUsernameNormalized", testCreateUserDuplicateUsernameNormalized}, {"CreateUser_duplicateUsernameNormalized", testCreateUserDuplicateUsernameNormalized},
{"CreateUser_GetUserByEmail_roundTrip", testCreateUserGetUserByEmailRoundTrip},
} }
for _, tc := range cases { for _, tc := range cases {
@ -138,6 +140,22 @@ func testCreateUserDuplicateUsernameNormalized(t *testing.T, s SuiteStore) {
} }
} }
func testCreateUserGetUserByEmailRoundTrip(t *testing.T, s SuiteStore) {
ctx := context.Background()
want := FixedUser()
if err := s.CreateUser(ctx, want); err != nil {
t.Fatalf("CreateUser: %v", err)
}
got, err := s.GetUserByEmail(ctx, want.Email)
if err != nil {
t.Fatalf("GetUserByEmail: %v", err)
}
AssertUserEqual(t, got, want)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Fixtures and helpers. Exported so dialect-specific tests can reuse them // Fixtures and helpers. Exported so dialect-specific tests can reuse them
// for one-off cases that don't fit into the shared suite. // for one-off cases that don't fit into the shared suite.