package group import ( "context" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) type Repository interface { List(ctx context.Context) ([]Group, error) Create(ctx context.Context, input CreateRequest) (Group, error) Update(ctx context.Context, groupID uuid.UUID, input UpdateRequest) (Group, error) Delete(ctx context.Context, groupID uuid.UUID) error } type PGRepository struct { db *pgxpool.Pool } func NewPGRepository(db *pgxpool.Pool) *PGRepository { return &PGRepository{db: db} } func (r *PGRepository) List(ctx context.Context) ([]Group, error) { rows, err := r.db.Query(ctx, ` select id, name, description from groups where deleted_at is null order by created_at desc `) if err != nil { return nil, err } defer rows.Close() var items []Group for rows.Next() { var item Group if err := rows.Scan(&item.ID, &item.Name, &item.Description); err != nil { return nil, err } members, err := r.listMembers(ctx, item.ID) if err != nil { return nil, err } item.Members = members for _, member := range members { item.UserIDs = append(item.UserIDs, member.ID) } items = append(items, item) } return items, rows.Err() } func (r *PGRepository) Create(ctx context.Context, input CreateRequest) (Group, error) { tx, err := r.db.Begin(ctx) if err != nil { return Group{}, err } defer tx.Rollback(ctx) groupID := uuid.New() if _, err := tx.Exec(ctx, ` insert into groups (id, name, description) values ($1, $2, $3) `, groupID, input.Name, input.Description); err != nil { return Group{}, err } for _, userID := range input.UserIDs { if _, err := tx.Exec(ctx, ` insert into group_memberships (id, group_id, user_id) values ($1, $2, $3) `, uuid.New(), groupID, userID); err != nil { return Group{}, err } } if err := tx.Commit(ctx); err != nil { return Group{}, err } return r.getByID(ctx, groupID) } func (r *PGRepository) Update(ctx context.Context, groupID uuid.UUID, input UpdateRequest) (Group, error) { tx, err := r.db.Begin(ctx) if err != nil { return Group{}, err } defer tx.Rollback(ctx) if _, err := tx.Exec(ctx, ` update groups set name = coalesce($2, name), description = coalesce($3, description), updated_at = now() where id = $1 and deleted_at is null `, groupID, input.Name, input.Description); err != nil { return Group{}, err } if input.UserIDs != nil { if _, err := tx.Exec(ctx, `delete from group_memberships where group_id = $1`, groupID); err != nil { return Group{}, err } for _, userID := range input.UserIDs { if _, err := tx.Exec(ctx, ` insert into group_memberships (id, group_id, user_id) values ($1, $2, $3) `, uuid.New(), groupID, userID); err != nil { return Group{}, err } } } if err := tx.Commit(ctx); err != nil { return Group{}, err } return r.getByID(ctx, groupID) } func (r *PGRepository) Delete(ctx context.Context, groupID uuid.UUID) error { _, err := r.db.Exec(ctx, ` update groups set deleted_at = now(), updated_at = now() where id = $1 and deleted_at is null `, groupID) return err } func (r *PGRepository) getByID(ctx context.Context, groupID uuid.UUID) (Group, error) { row := r.db.QueryRow(ctx, ` select id, name, description from groups where id = $1 and deleted_at is null `, groupID) var item Group if err := row.Scan(&item.ID, &item.Name, &item.Description); err != nil { return Group{}, err } members, err := r.listMembers(ctx, item.ID) if err != nil { return Group{}, err } item.Members = members for _, member := range members { item.UserIDs = append(item.UserIDs, member.ID) } return item, nil } func (r *PGRepository) listMembers(ctx context.Context, groupID uuid.UUID) ([]Member, error) { rows, err := r.db.Query(ctx, ` select u.id, u.username, u.display_name from group_memberships gm join users u on u.id = gm.user_id where gm.group_id = $1 and u.deleted_at is null order by u.username asc `, groupID) if err != nil { return nil, err } defer rows.Close() var items []Member for rows.Next() { var item Member if err := rows.Scan(&item.ID, &item.Username, &item.DisplayName); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() }