feat: add user search
This commit is contained in:
@@ -152,6 +152,60 @@ func (r *UserRepo) UpdatePassword(ctx context.Context, id, newHash string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search returns users whose name or handle matches `q` (ILIKE
|
||||
// substring). Leading "@" is stripped — callers can pass either
|
||||
// "@foo" or "foo". Ordering bubbles handle-prefix matches above
|
||||
// name-prefix above the rest, then alphabetic by name as the
|
||||
// tiebreaker.
|
||||
//
|
||||
// Postgres ILIKE is fine at our user-table scale (low six figures
|
||||
// max). When the table grows enough to warrant fuzzy / trigram
|
||||
// matching, add a pg_trgm GIN index and switch to similarity().
|
||||
func (r *UserRepo) Search(ctx context.Context, q string, limit int) ([]model.User, error) {
|
||||
cleaned := strings.TrimPrefix(strings.TrimSpace(q), "@")
|
||||
if cleaned == "" {
|
||||
return []model.User{}, nil
|
||||
}
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 20
|
||||
}
|
||||
substring := "%" + cleaned + "%"
|
||||
prefix := cleaned + "%"
|
||||
|
||||
const sql = `
|
||||
SELECT id, email, handle, password_hash, name, avatar_url, role, plan,
|
||||
email_verified_at, token_version, created_at, updated_at
|
||||
FROM users
|
||||
WHERE name ILIKE $1 OR handle::text ILIKE $1
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN handle::text ILIKE $2 THEN 0
|
||||
WHEN name ILIKE $2 THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
name
|
||||
LIMIT $3`
|
||||
|
||||
rows, err := r.pg.Pool.Query(ctx, sql, substring, prefix, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search users: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []model.User{}
|
||||
for rows.Next() {
|
||||
var u model.User
|
||||
if err := rows.Scan(
|
||||
&u.ID, &u.Email, &u.Handle, &u.PasswordHash, &u.Name, &u.AvatarURL,
|
||||
&u.Role, &u.Plan, &u.EmailVerifiedAt, &u.TokenVersion,
|
||||
&u.CreatedAt, &u.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan user: %w", err)
|
||||
}
|
||||
out = append(out, u)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GetReadingPrefs returns the raw JSONB bytes for the user's reading
|
||||
// prefs (empty object when never set). The frontend owns the shape, so
|
||||
// we round-trip the payload verbatim — no server-side schema enforced.
|
||||
|
||||
Reference in New Issue
Block a user