package projectsrepo import ( "context" "database/sql" ) type Repo struct { db *sql.DB } type Project struct { ID int64 GuildID int64 Name string Description string IsActive bool CreatedBy int64 } type ProjectWithMembers struct { Project *Project Creator int64 Helpers []int64 } func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} } func (r *Repo) CreateProject(ctx context.Context, guildID, creatorID int64, name, description string) (int64, error) { const q = ` INSERT INTO projects (guild_id, name, description, created_by) VALUES ($1, $2, $3, $4) RETURNING id ` var id int64 if err := r.db.QueryRowContext(ctx, q, guildID, name, description, creatorID).Scan(&id); err != nil { return 0, err } const qMember = ` INSERT INTO project_members (project_id, user_id, is_creator) VALUES ($1, $2, TRUE) ON CONFLICT (project_id, user_id) DO NOTHING ` if _, err := r.db.ExecContext(ctx, qMember, id, creatorID); err != nil { return 0, err } return id, nil } func (r *Repo) AddHelper(ctx context.Context, projectID, userID int64) error { const q = ` INSERT INTO project_members (project_id, user_id, is_creator) VALUES ($1, $2, FALSE) ON CONFLICT (project_id, user_id) DO NOTHING ` _, err := r.db.ExecContext(ctx, q, projectID, userID) return err } func (r *Repo) ProjectExistsInGuild(ctx context.Context, guildID, projectID int64) (bool, error) { const q = ` SELECT 1 FROM projects WHERE guild_id = $1 AND id = $2 ` row := r.db.QueryRowContext(ctx, q, guildID, projectID) var one int switch err := row.Scan(&one); err { case nil: return true, nil case sql.ErrNoRows: return false, nil default: return false, err } } func (r *Repo) ListActiveProjectsWithMembers(ctx context.Context, guildID int64, limit int) ([]*ProjectWithMembers, error) { const qProjects = ` SELECT id, name, description, created_by FROM projects WHERE guild_id = $1 AND is_active = TRUE ORDER BY created_at DESC LIMIT $2 ` rows, err := r.db.QueryContext(ctx, qProjects, guildID, limit) if err != nil { return nil, err } defer rows.Close() var out []*ProjectWithMembers for rows.Next() { p := &Project{GuildID: guildID, IsActive: true} if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.CreatedBy); err != nil { return nil, err } out = append(out, &ProjectWithMembers{ Project: p, Creator: p.CreatedBy, }) } if err := rows.Err(); err != nil { return nil, err } if len(out) == 0 { return out, nil } // Load members per project. const qMembers = ` SELECT project_id, user_id, is_creator FROM project_members WHERE project_id = ANY($1) ` ids := make([]int64, len(out)) for i, p := range out { ids[i] = p.Project.ID } rowsM, err := r.db.QueryContext(ctx, qMembers, ids) if err != nil { return nil, err } defer rowsM.Close() index := make(map[int64]*ProjectWithMembers, len(out)) for _, p := range out { index[p.Project.ID] = p } for rowsM.Next() { var pid, uid int64 var isCreator bool if err := rowsM.Scan(&pid, &uid, &isCreator); err != nil { return nil, err } item, ok := index[pid] if !ok { continue } if isCreator { item.Creator = uid } else { item.Helpers = append(item.Helpers, uid) } } if err := rowsM.Err(); err != nil { return nil, err } return out, nil }