package postgres import ( "context" "database/sql" "fmt" "git.gobha.me/xcaliber/chat-switchboard/models" ) // ── GroupStore ────────────────────────────── type GroupStore struct{} func NewGroupStore() *GroupStore { return &GroupStore{} } func (s *GroupStore) Create(ctx context.Context, g *models.Group) error { return DB.QueryRowContext(ctx, ` INSERT INTO groups (name, description, scope, team_id, created_by) VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at, updated_at`, g.Name, g.Description, g.Scope, models.NullString(g.TeamID), g.CreatedBy, ).Scan(&g.ID, &g.CreatedAt, &g.UpdatedAt) } func (s *GroupStore) GetByID(ctx context.Context, id string) (*models.Group, error) { var g models.Group var teamID sql.NullString err := DB.QueryRowContext(ctx, ` SELECT g.id, g.name, g.description, g.scope, g.team_id, g.created_by, g.created_at, g.updated_at, (SELECT COUNT(*) FROM group_members WHERE group_id = g.id) AS member_count FROM groups g WHERE g.id = $1`, id).Scan( &g.ID, &g.Name, &g.Description, &g.Scope, &teamID, &g.CreatedBy, &g.CreatedAt, &g.UpdatedAt, &g.MemberCount, ) if err != nil { return nil, err } g.TeamID = NullableStringPtr(teamID) return &g, nil } func (s *GroupStore) Update(ctx context.Context, id string, name, description *string) error { b := NewUpdate("groups") if name != nil { b.Set("name", *name) } if description != nil { b.Set("description", *description) } if !b.HasSets() { return nil } b.Where("id", id) res, err := b.Exec(DB) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return sql.ErrNoRows } return nil } func (s *GroupStore) Delete(ctx context.Context, id string) error { res, err := DB.ExecContext(ctx, "DELETE FROM groups WHERE id = $1", id) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return sql.ErrNoRows } return nil } // ── Scoped Listing ────────────────────────── func (s *GroupStore) ListAll(ctx context.Context) ([]models.Group, error) { return queryGroups(ctx, ` SELECT g.id, g.name, g.description, g.scope, g.team_id, g.created_by, g.created_at, g.updated_at, (SELECT COUNT(*) FROM group_members WHERE group_id = g.id) FROM groups g ORDER BY g.scope, g.name`) } func (s *GroupStore) ListForTeam(ctx context.Context, teamID string) ([]models.Group, error) { return queryGroups(ctx, ` SELECT g.id, g.name, g.description, g.scope, g.team_id, g.created_by, g.created_at, g.updated_at, (SELECT COUNT(*) FROM group_members WHERE group_id = g.id) FROM groups g WHERE g.scope = 'team' AND g.team_id = $1 ORDER BY g.name`, teamID) } func (s *GroupStore) ListForUser(ctx context.Context, userID string) ([]models.Group, error) { return queryGroups(ctx, ` SELECT g.id, g.name, g.description, g.scope, g.team_id, g.created_by, g.created_at, g.updated_at, (SELECT COUNT(*) FROM group_members WHERE group_id = g.id) FROM groups g WHERE g.id IN (SELECT group_id FROM group_members WHERE user_id = $1) ORDER BY g.scope, g.name`, userID) } // ── Members ───────────────────────────────── func (s *GroupStore) AddMember(ctx context.Context, groupID, userID, addedBy string) error { _, err := DB.ExecContext(ctx, ` INSERT INTO group_members (group_id, user_id, added_by) VALUES ($1, $2, $3) ON CONFLICT (group_id, user_id) DO NOTHING`, groupID, userID, addedBy) return err } func (s *GroupStore) RemoveMember(ctx context.Context, groupID, userID string) error { res, err := DB.ExecContext(ctx, "DELETE FROM group_members WHERE group_id = $1 AND user_id = $2", groupID, userID) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return sql.ErrNoRows } return nil } func (s *GroupStore) ListMembers(ctx context.Context, groupID string) ([]models.GroupMember, error) { rows, err := DB.QueryContext(ctx, ` SELECT gm.id, gm.group_id, gm.user_id, gm.added_by, gm.added_at, u.username, u.email, COALESCE(u.display_name, '') FROM group_members gm JOIN users u ON u.id = gm.user_id WHERE gm.group_id = $1 ORDER BY u.username`, groupID) if err != nil { return nil, err } defer rows.Close() var result []models.GroupMember for rows.Next() { var m models.GroupMember if err := rows.Scan(&m.ID, &m.GroupID, &m.UserID, &m.AddedBy, &m.AddedAt, &m.Username, &m.Email, &m.DisplayName); err != nil { return nil, err } result = append(result, m) } return result, rows.Err() } func (s *GroupStore) IsMember(ctx context.Context, groupID, userID string) (bool, error) { var exists bool err := DB.QueryRowContext(ctx, ` SELECT EXISTS(SELECT 1 FROM group_members WHERE group_id = $1 AND user_id = $2)`, groupID, userID).Scan(&exists) return exists, err } func (s *GroupStore) GetUserGroupIDs(ctx context.Context, userID string) ([]string, error) { rows, err := DB.QueryContext(ctx, "SELECT group_id FROM group_members WHERE user_id = $1", userID) if err != nil { return nil, err } defer rows.Close() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } ids = append(ids, id) } return ids, rows.Err() } // ── Scanners ──────────────────────────────── func queryGroups(ctx context.Context, q string, args ...interface{}) ([]models.Group, error) { rows, err := DB.QueryContext(ctx, q, args...) if err != nil { return nil, fmt.Errorf("queryGroups: %w", err) } defer rows.Close() var result []models.Group for rows.Next() { var g models.Group var teamID sql.NullString if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.Scope, &teamID, &g.CreatedBy, &g.CreatedAt, &g.UpdatedAt, &g.MemberCount); err != nil { return nil, err } g.TeamID = NullableStringPtr(teamID) result = append(result, g) } return result, rows.Err() }