A community based topic aggregation platform built on atproto
1package postgres 2 3import ( 4 "Coves/internal/core/users" 5 "context" 6 "database/sql" 7 "fmt" 8 "strings" 9 10 "github.com/lib/pq" 11) 12 13type postgresUserRepo struct { 14 db *sql.DB 15} 16 17// NewUserRepository creates a new PostgreSQL user repository 18func NewUserRepository(db *sql.DB) users.UserRepository { 19 return &postgresUserRepo{db: db} 20} 21 22// Create inserts a new user into the users table 23func (r *postgresUserRepo) Create(ctx context.Context, user *users.User) (*users.User, error) { 24 query := ` 25 INSERT INTO users (did, handle, pds_url) 26 VALUES ($1, $2, $3) 27 RETURNING did, handle, pds_url, created_at, updated_at` 28 29 err := r.db.QueryRowContext(ctx, query, user.DID, user.Handle, user.PDSURL). 30 Scan(&user.DID, &user.Handle, &user.PDSURL, &user.CreatedAt, &user.UpdatedAt) 31 if err != nil { 32 // Check for unique constraint violations 33 if strings.Contains(err.Error(), "duplicate key") { 34 if strings.Contains(err.Error(), "users_pkey") { 35 return nil, fmt.Errorf("user with DID already exists") 36 } 37 if strings.Contains(err.Error(), "users_handle_key") { 38 return nil, fmt.Errorf("handle already taken") 39 } 40 } 41 return nil, fmt.Errorf("failed to create user: %w", err) 42 } 43 44 return user, nil 45} 46 47// GetByDID retrieves a user by their DID 48func (r *postgresUserRepo) GetByDID(ctx context.Context, did string) (*users.User, error) { 49 user := &users.User{} 50 query := `SELECT did, handle, pds_url, created_at, updated_at FROM users WHERE did = $1` 51 52 err := r.db.QueryRowContext(ctx, query, did). 53 Scan(&user.DID, &user.Handle, &user.PDSURL, &user.CreatedAt, &user.UpdatedAt) 54 55 if err == sql.ErrNoRows { 56 return nil, fmt.Errorf("user not found") 57 } 58 if err != nil { 59 return nil, fmt.Errorf("failed to get user by DID: %w", err) 60 } 61 62 return user, nil 63} 64 65// GetByHandle retrieves a user by their handle 66func (r *postgresUserRepo) GetByHandle(ctx context.Context, handle string) (*users.User, error) { 67 user := &users.User{} 68 query := `SELECT did, handle, pds_url, created_at, updated_at FROM users WHERE handle = $1` 69 70 err := r.db.QueryRowContext(ctx, query, handle). 71 Scan(&user.DID, &user.Handle, &user.PDSURL, &user.CreatedAt, &user.UpdatedAt) 72 73 if err == sql.ErrNoRows { 74 return nil, fmt.Errorf("user not found") 75 } 76 if err != nil { 77 return nil, fmt.Errorf("failed to get user by handle: %w", err) 78 } 79 80 return user, nil 81} 82 83// UpdateHandle updates the handle for a user with the given DID 84func (r *postgresUserRepo) UpdateHandle(ctx context.Context, did, newHandle string) (*users.User, error) { 85 user := &users.User{} 86 query := ` 87 UPDATE users 88 SET handle = $2, updated_at = NOW() 89 WHERE did = $1 90 RETURNING did, handle, pds_url, created_at, updated_at` 91 92 err := r.db.QueryRowContext(ctx, query, did, newHandle). 93 Scan(&user.DID, &user.Handle, &user.PDSURL, &user.CreatedAt, &user.UpdatedAt) 94 95 if err == sql.ErrNoRows { 96 return nil, fmt.Errorf("user not found") 97 } 98 if err != nil { 99 // Check for unique constraint violation on handle 100 if strings.Contains(err.Error(), "duplicate key") && strings.Contains(err.Error(), "users_handle_key") { 101 return nil, fmt.Errorf("handle already taken") 102 } 103 return nil, fmt.Errorf("failed to update handle: %w", err) 104 } 105 106 return user, nil 107} 108 109// GetByDIDs retrieves multiple users by their DIDs in a single query 110// Returns a map of DID -> User for efficient lookups 111// Missing users are not included in the result map (no error for missing users) 112func (r *postgresUserRepo) GetByDIDs(ctx context.Context, dids []string) (map[string]*users.User, error) { 113 if len(dids) == 0 { 114 return make(map[string]*users.User), nil 115 } 116 117 // Build parameterized query with IN clause 118 // Use ANY($1) for PostgreSQL array support with pq.Array() for type conversion 119 query := `SELECT did, handle, pds_url, created_at, updated_at FROM users WHERE did = ANY($1)` 120 121 rows, err := r.db.QueryContext(ctx, query, pq.Array(dids)) 122 if err != nil { 123 return nil, fmt.Errorf("failed to query users by DIDs: %w", err) 124 } 125 defer rows.Close() 126 127 // Build map of results 128 result := make(map[string]*users.User, len(dids)) 129 for rows.Next() { 130 user := &users.User{} 131 err := rows.Scan(&user.DID, &user.Handle, &user.PDSURL, &user.CreatedAt, &user.UpdatedAt) 132 if err != nil { 133 return nil, fmt.Errorf("failed to scan user row: %w", err) 134 } 135 result[user.DID] = user 136 } 137 138 if err = rows.Err(); err != nil { 139 return nil, fmt.Errorf("error iterating user rows: %w", err) 140 } 141 142 return result, nil 143}