Skip to content

Commit 4b97560

Browse files
authored
fix(group): Calculate member count as a whole (#2257)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent df71cee commit 4b97560

File tree

7 files changed

+109
-47
lines changed

7 files changed

+109
-47
lines changed

app/controlplane/cmd/wire_gen.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/pkg/biz/group.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ type GroupRepo interface {
5555
ListPendingInvitationsByGroup(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, paginationOpts *pagination.OffsetPaginationOpts) ([]*OrgInvitation, int, error)
5656
// ListProjectsByGroup retrieves a list of projects that a group is a member of with pagination.
5757
ListProjectsByGroup(ctx context.Context, orgID uuid.UUID, groupID uuid.UUID, visibleProjectIDs []uuid.UUID, paginationOpts *pagination.OffsetPaginationOpts) ([]*GroupProjectInfo, int, error)
58+
// UpdateGroupMemberCount updates the member count of a group.
59+
UpdateGroupMemberCount(ctx context.Context, groupID uuid.UUID) error
5860
}
5961

6062
// GroupMembership represents a membership of a user in a group.

app/controlplane/pkg/biz/testhelpers/wire_gen.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Fix group member counts by calculating the correct number of members for each group
2+
-- This migration addresses any inconsistencies in the member_count field of the groups table
3+
-- by counting the actual number of non-deleted memberships for each group.
4+
5+
-- Update the member_count in groups table based on a count of active memberships
6+
UPDATE "groups" g
7+
SET "member_count" = (
8+
SELECT COUNT(*)
9+
FROM "group_memberships" gm
10+
WHERE gm."group_id" = g."id"
11+
AND gm."deleted_at" IS NULL
12+
); -- Apply to all groups

app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
h1:WasPyGuTE05lx75h+qqmoAknSDJPoN50E8hqbhNFVQs=
1+
h1:bxjg7J7Xk0+87+PhS0+F/c4AcHJZ3pkf0FnPMZa39Q0=
22
20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M=
33
20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g=
44
20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI=
@@ -97,3 +97,4 @@ h1:WasPyGuTE05lx75h+qqmoAknSDJPoN50E8hqbhNFVQs=
9797
20250704090359.sql h1:a0ksfjy2dtzviJL16HbC4eT1xBxy2qFH5mNFOpYlUrA=
9898
20250710105502.sql h1:EA6Ta1qsZcrNoOrO5zUNgiweHDtjl0HUlobukRuruko=
9999
20250714172256.sql h1:S0ImNk0sMjWVVZvS6VVHn2h96/nx8GOf4aVxELbJAcg=
100+
20250715100956.sql h1:y9eOaPMpQTlcJppjaGzeuHBTNDwe6sGbxSVU8e7LL1o=

app/controlplane/pkg/data/group.go

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,6 @@ import (
2020
"fmt"
2121
"time"
2222

23-
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion"
24-
25-
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project"
26-
27-
"entgo.io/ent/dialect/sql/sqljson"
28-
29-
"entgo.io/ent/dialect/sql"
3023
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
3124
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
3225
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent"
@@ -36,10 +29,14 @@ import (
3629
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization"
3730
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/orginvitation"
3831
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate"
32+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project"
33+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion"
3934
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/user"
4035
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow"
4136
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination"
4237

38+
"entgo.io/ent/dialect/sql"
39+
"entgo.io/ent/dialect/sql/sqljson"
4340
"github.com/go-kratos/kratos/v2/log"
4441
"github.com/google/uuid"
4542
)
@@ -231,13 +228,9 @@ func (g GroupRepo) Create(ctx context.Context, orgID uuid.UUID, opts *biz.Create
231228
SetDescription(opts.Description).
232229
SetOrganizationID(orgID)
233230

234-
// Only set the member count and add member if userID is provided
231+
// Add member if userID is provided
235232
if opts.UserID != nil {
236-
builder = builder.
237-
AddMemberIDs(*opts.UserID).
238-
SetMemberCount(1)
239-
} else {
240-
builder = builder.SetMemberCount(0)
233+
builder = builder.AddMemberIDs(*opts.UserID)
241234
}
242235

243236
gr, err := builder.Save(ctx)
@@ -287,6 +280,11 @@ func (g GroupRepo) Create(ctx context.Context, orgID uuid.UUID, opts *biz.Create
287280
return nil, fmt.Errorf("failed to create group: %w", err)
288281
}
289282

283+
// Update the member count based on actual query
284+
if err := g.UpdateGroupMemberCount(ctx, entGroup.ID); err != nil {
285+
g.log.Warnf("failed to update member count for newly created group %s: %v", entGroup.ID, err)
286+
}
287+
290288
return g.FindByOrgAndID(ctx, orgID, entGroup.ID)
291289
}
292290

@@ -474,16 +472,16 @@ func (g GroupRepo) AddMemberToGroup(ctx context.Context, orgID uuid.UUID, groupI
474472
}
475473
}
476474

477-
// Increment the member count of the group
478-
if err := tx.Group.UpdateOneID(groupID).AddMemberCount(1).Exec(ctx); err != nil {
479-
return fmt.Errorf("failed to increment group member count: %w", err)
480-
}
481-
482475
return nil
483476
}); err != nil {
484477
return nil, fmt.Errorf("failed to add member to group: %w", err)
485478
}
486479

480+
// Update the member count based on actual query after transaction
481+
if err := g.UpdateGroupMemberCount(ctx, groupID); err != nil {
482+
g.log.Warnf("failed to update member count after adding member to group %s: %v", groupID, err)
483+
}
484+
487485
// Return the newly created membership
488486
return g.FindGroupMembershipByGroupAndID(ctx, groupID, userID)
489487
}
@@ -527,18 +525,18 @@ func (g GroupRepo) RemoveMemberFromGroup(ctx context.Context, orgID uuid.UUID, g
527525
}
528526
}
529527

530-
// Decrement the member count of the group
531-
if err := tx.Group.UpdateOneID(groupID).AddMemberCount(-1).Exec(ctx); err != nil {
532-
return fmt.Errorf("failed to increment group member count: %w", err)
533-
}
534-
535528
return nil
536529
})
537530

538531
if err != nil {
539532
return err
540533
}
541534

535+
// Update the member count based on actual query after transaction
536+
if err := g.UpdateGroupMemberCount(ctx, groupID); err != nil {
537+
g.log.Warnf("failed to update member count after removing member from group %s: %v", groupID, err)
538+
}
539+
542540
return nil
543541
}
544542

@@ -758,3 +756,27 @@ func entGroupMembershipToBiz(gu *ent.GroupMembership) *biz.GroupMembership {
758756
DeletedAt: toTimePtr(gu.DeletedAt),
759757
}
760758
}
759+
760+
// UpdateGroupMemberCount updates the member count of a group based on an actual count query
761+
// This should be called after membership changes have been committed.
762+
func (g GroupRepo) UpdateGroupMemberCount(ctx context.Context, groupID uuid.UUID) error {
763+
// Count active members in the group
764+
count, err := g.data.DB.GroupMembership.Query().
765+
Where(
766+
groupmembership.GroupIDEQ(groupID),
767+
groupmembership.DeletedAtIsNil(),
768+
).
769+
Count(ctx)
770+
if err != nil {
771+
return fmt.Errorf("failed to count group members: %w", err)
772+
}
773+
774+
// Update the group's member count to the actual count
775+
if _, err := g.data.DB.Group.UpdateOneID(groupID).
776+
SetMemberCount(count).
777+
Save(ctx); err != nil {
778+
return fmt.Errorf("failed to update group member count: %w", err)
779+
}
780+
781+
return nil
782+
}

app/controlplane/pkg/data/membership.go

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ import (
3434
)
3535

3636
type MembershipRepo struct {
37-
data *Data
38-
log *log.Helper
37+
data *Data
38+
log *log.Helper
39+
groupRepo biz.GroupRepo
3940
}
4041

41-
func NewMembershipRepo(data *Data, logger log.Logger) biz.MembershipRepo {
42+
func NewMembershipRepo(data *Data, groupRepo biz.GroupRepo, logger log.Logger) biz.MembershipRepo {
4243
return &MembershipRepo{
43-
data: data,
44-
log: log.NewHelper(logger),
44+
data: data,
45+
groupRepo: groupRepo,
46+
log: log.NewHelper(logger),
4547
}
4648
}
4749

@@ -253,7 +255,10 @@ func (r *MembershipRepo) Delete(ctx context.Context, id uuid.UUID) error {
253255
return fmt.Errorf("failed to get membership: %w", err)
254256
}
255257

256-
return WithTx(ctx, r.data.DB, func(tx *ent.Tx) error {
258+
// Prepare a slice to hold group IDs that need to be updated
259+
var groupIDs []uuid.UUID
260+
261+
if trErr := WithTx(ctx, r.data.DB, func(tx *ent.Tx) error {
257262
// Delete the specific membership
258263
if err := tx.Membership.DeleteOneID(id).Exec(ctx); err != nil {
259264
return fmt.Errorf("failed to delete membership: %w", err)
@@ -279,6 +284,21 @@ func (r *MembershipRepo) Delete(ctx context.Context, id uuid.UUID) error {
279284
// Remove the user from all groups in the organization by soft-deleting group memberships
280285
now := time.Now()
281286

287+
// Find all group IDs where this user is a member in this organization
288+
groupMemberships, grpMemErr := tx.GroupMembership.Query().Where(
289+
groupmembership.UserID(userID),
290+
groupmembership.DeletedAtIsNil(),
291+
groupmembership.HasGroupWith(group.OrganizationID(orgID)),
292+
).Select(groupmembership.FieldGroupID).All(ctx)
293+
if grpMemErr != nil {
294+
return fmt.Errorf("failed to fetch group IDs for user %s in organization %s: %w", userID, orgID, err)
295+
}
296+
297+
// Collect group IDs to update member counts later
298+
for _, gm := range groupMemberships {
299+
groupIDs = append(groupIDs, gm.GroupID)
300+
}
301+
282302
// Soft delete all group memberships for this user in this organization
283303
if _, err := tx.GroupMembership.Update().Where(
284304
groupmembership.UserID(userID),
@@ -287,22 +307,27 @@ func (r *MembershipRepo) Delete(ctx context.Context, id uuid.UUID) error {
287307
).SetDeletedAt(now).SetUpdatedAt(now).Save(ctx); err != nil {
288308
return fmt.Errorf("failed to delete group memberships for user %s in organization %s: %w", userID, orgID, err)
289309
}
290-
291-
// Decrement the member count of each group this user was a member of
292-
// We don't need a separate check for groups the user was a maintainer of
293-
// because all memberships were already deleted above
294-
if _, err := tx.Group.Update().
295-
Where(
296-
group.HasMembersWith(user.ID(userID)),
297-
group.HasOrganizationWith(organization.ID(orgID)),
298-
).
299-
AddMemberCount(-1).Save(ctx); err != nil {
300-
return fmt.Errorf("failed to decrement group member count: %w", err)
301-
}
302310
}
303311

304312
return nil
305-
})
313+
}); trErr != nil {
314+
return trErr
315+
}
316+
317+
// For each affected group, update the member count based on actual query
318+
updated := map[uuid.UUID]struct{}{}
319+
for _, gid := range groupIDs {
320+
if _, seen := updated[gid]; seen {
321+
// deduplicate group IDs
322+
continue
323+
}
324+
updated[gid] = struct{}{}
325+
if err := r.groupRepo.UpdateGroupMemberCount(ctx, gid); err != nil {
326+
return fmt.Errorf("failed to update group member count for group %s: %w", gid, err)
327+
}
328+
}
329+
330+
return nil
306331
}
307332

308333
// RBAC methods

0 commit comments

Comments
 (0)