@@ -69,131 +69,98 @@ func resolveAchievedAt(ctx context.Context, r *Resolver, achievementID string) (
6969 return & scalars.DateTime {Time : * ts }, nil
7070}
7171
72- // computeLeaderboardTags computes tags for a leaderboard entry based on entity type and viewer context
72+ // viewerContext holds pre-loaded viewer data to avoid N+1 queries in tag computation
73+ type viewerContext struct {
74+ teamIDs map [string ]bool // Viewer's team IDs in the current project
75+ superTeamIDs map [string ]bool // Viewer's superteam IDs in the current project
76+ churchID string // Viewer's church ID
77+ }
78+
79+ // computeLeaderboardTags computes tags for a leaderboard entry based on entity type and viewer context.
80+ // This function does NO database lookups - all data is pre-loaded into viewerCtx.
81+ // Note: ADMIN and TEAM_LEAD tags have been removed from project/event leaderboards.
82+ // TEAM_LEAD is only shown on team member leaderboards (handled separately).
7383func computeLeaderboardTags (
74- ctx context.Context ,
7584 entityType model.LeaderboardEntityType ,
7685 entityID string ,
77- projectID string ,
7886 currentUserID string ,
79- ldrs * loaders. Loaders ,
80- ) ( []model.LeaderboardEntryTag , error ) {
87+ viewerCtx * viewerContext ,
88+ ) []model.LeaderboardEntryTag {
8189 tags := []model.LeaderboardEntryTag {}
8290
8391 switch entityType {
8492 case model .LeaderboardEntityTypePersons :
85- // Check ME tag
93+ // ME tag - simple ID comparison
8694 if entityID == currentUserID {
8795 tags = append (tags , model .LeaderboardEntryTagMe )
8896 }
8997
90- // Check ADMIN tag
91- userRoles , ok := ctx .Value (middleware .UserRolesKey ).([]string )
92- if ok {
93- for _ , role := range userRoles {
94- if role == "superadmin" || role == "admin" || role == "project_admin" {
95- // Check if the entity (person in leaderboard) has admin roles
96- personRolesThunk := ldrs .RolesByUserLoader .Load (ctx , entityID )
97- personRoles , err := personRolesThunk ()
98- if err == nil {
99- for _ , personRole := range personRoles {
100- if personRole .Role == model .RoleTypeSuperadmin ||
101- personRole .Role == model .RoleTypeAdmin ||
102- personRole .Role == model .RoleTypeProjectAdmin {
103- tags = append (tags , model .LeaderboardEntryTagAdmin )
104- goto checkTeamLead // Break out of nested loops
105- }
106- }
107- }
108- }
109- }
110- }
111-
112- checkTeamLead:
113- // Check TEAM_LEAD tag
114- // Load viewer's teams
115- viewerTeamsThunk := ldrs .TeamsByUserLoader .Load (ctx , currentUserID )
116- allViewerTeams , err := viewerTeamsThunk ()
117- if err != nil {
118- return tags , fmt .Errorf ("failed to load viewer teams: %w" , err )
119- }
120-
121- // Filter teams by project and get team IDs
122- viewerTeamIDs := make (map [string ]bool )
123- for _ , team := range allViewerTeams {
124- if team .ProjectID == projectID {
125- viewerTeamIDs [team .ID ] = true
126- }
98+ case model .LeaderboardEntityTypeTeams :
99+ // ME tag - check if this is one of viewer's teams
100+ if viewerCtx .teamIDs [entityID ] {
101+ tags = append (tags , model .LeaderboardEntryTagMe )
127102 }
128103
129- // Load person's roles
130- personRolesThunk := ldrs .RolesByUserLoader .Load (ctx , entityID )
131- personRoles , err := personRolesThunk ()
132- if err != nil {
133- return tags , fmt .Errorf ("failed to load person roles: %w" , err )
104+ case model .LeaderboardEntityTypeSuperteams :
105+ // ME tag - check if this is viewer's superteam
106+ if viewerCtx .superTeamIDs [entityID ] {
107+ tags = append (tags , model .LeaderboardEntryTagMe )
134108 }
135109
136- // Check if person is TEAM_LEAD of any of viewer's teams
137- for _ , role := range personRoles {
138- if role .Role == model .RoleTypeTeamLead && role .Scope != nil {
139- if role .Scope .Type == model .ScopeTypeTeam {
140- if viewerTeamIDs [role .Scope .ID ] {
141- tags = append (tags , model .LeaderboardEntryTagTeamLead )
142- break
143- }
144- }
145- }
110+ case model .LeaderboardEntityTypeChurches :
111+ // ME tag - check if this is viewer's church
112+ if entityID == viewerCtx .churchID {
113+ tags = append (tags , model .LeaderboardEntryTagMe )
146114 }
115+ }
147116
148- case model .LeaderboardEntityTypeTeams :
149- // For teams, only check ME tag
150- // Load viewer's teams
151- viewerTeamsThunk := ldrs .TeamsByUserLoader .Load (ctx , currentUserID )
152- allViewerTeams , err := viewerTeamsThunk ()
153- if err != nil {
154- return tags , fmt .Errorf ("failed to load viewer teams: %w" , err )
155- }
117+ return tags
118+ }
156119
157- // Filter by project and check if entityID matches
158- for _ , team := range allViewerTeams {
159- if team .ProjectID == projectID && team .ID == entityID {
160- tags = append (tags , model .LeaderboardEntryTagMe )
161- break
162- }
163- }
120+ // preloadViewerContext loads all viewer-specific data needed for tag computation.
121+ // This is called ONCE before iterating over leaderboard entries.
122+ func preloadViewerContext (
123+ ctx context.Context ,
124+ currentUserID string ,
125+ projectID string ,
126+ entityType model.LeaderboardEntityType ,
127+ ldrs * loaders.Loaders ,
128+ ) (* viewerContext , error ) {
129+ viewerCtx := & viewerContext {
130+ teamIDs : make (map [string ]bool ),
131+ superTeamIDs : make (map [string ]bool ),
132+ }
164133
165- case model .LeaderboardEntityTypeSuperteams :
166- // For superteams, only check ME tag
167- // Load viewer's teams
168- viewerTeamsThunk := ldrs .TeamsByUserLoader .Load (ctx , currentUserID )
169- allViewerTeams , err := viewerTeamsThunk ()
134+ // Load viewer's teams for TEAMS and SUPERTEAMS entity types
135+ if entityType == model .LeaderboardEntityTypeTeams || entityType == model .LeaderboardEntityTypeSuperteams {
136+ teamsThunk := ldrs .TeamsByUserLoader .Load (ctx , currentUserID )
137+ teams , err := teamsThunk ()
170138 if err != nil {
171- return tags , fmt .Errorf ("failed to load viewer teams: %w" , err )
139+ return viewerCtx , fmt .Errorf ("failed to load viewer teams: %w" , err )
172140 }
173-
174- // Filter by project and check if any team belongs to this superteam
175- for _ , team := range allViewerTeams {
176- if team .ProjectID == projectID && team . SuperTeam != nil && team . SuperTeam . ID == entityID {
177- tags = append ( tags , model . LeaderboardEntryTagMe )
178- break
141+ for _ , team := range teams {
142+ if team . ProjectID == projectID {
143+ viewerCtx . teamIDs [ team . ID ] = true
144+ if team .SuperTeam != nil {
145+ viewerCtx . superTeamIDs [ team . SuperTeam . ID ] = true
146+ }
179147 }
180148 }
149+ }
181150
182- case model .LeaderboardEntityTypeChurches :
183- // For churches, only check ME tag
184- // Load viewer's user data
185- viewerThunk := ldrs .UserByIDLoader .Load (ctx , currentUserID )
186- viewer , err := viewerThunk ()
151+ // Load viewer's church for CHURCHES entity type
152+ if entityType == model .LeaderboardEntityTypeChurches {
153+ userThunk := ldrs .UserByIDLoader .Load (ctx , currentUserID )
154+ user , err := userThunk ()
187155 if err != nil {
188- return tags , fmt .Errorf ("failed to load viewer: %w" , err )
156+ return viewerCtx , fmt .Errorf ("failed to load viewer: %w" , err )
189157 }
190-
191- if viewer .Church .ID == entityID {
192- tags = append (tags , model .LeaderboardEntryTagMe )
158+ if user != nil && user .Church != nil {
159+ viewerCtx .churchID = user .Church .ID
193160 }
194161 }
195162
196- return tags , nil
163+ return viewerCtx , nil
197164}
198165
199166// buildLeaderboardConnection builds a GraphQL connection from leaderboard entries
@@ -227,14 +194,17 @@ func buildLeaderboardConnection(
227194 entries = entries [:requestedLimit ]
228195 }
229196
230- // Build edges
197+ // Pre-load viewer context ONCE (outside the loop) to avoid N+1 queries
198+ viewerCtx , err := preloadViewerContext (ctx , currentUserID , projectID , entityType , ldrs )
199+ if err != nil {
200+ return nil , fmt .Errorf ("failed to preload viewer context: %w" , err )
201+ }
202+
203+ // Build edges - NO dataloader calls in this loop
231204 edges := make ([]model.LeaderboardEdge , len (entries ))
232205 for i , entry := range entries {
233- // Compute tags based on entity type and viewer context
234- tags , err := computeLeaderboardTags (ctx , entityType , entry .EntityID , projectID , currentUserID , ldrs )
235- if err != nil {
236- return nil , fmt .Errorf ("failed to compute tags for entry %s: %w" , entry .EntityID , err )
237- }
206+ // Compute tags using pre-loaded viewer context (no DB lookups)
207+ tags := computeLeaderboardTags (entityType , entry .EntityID , currentUserID , viewerCtx )
238208
239209 edges [i ] = model.LeaderboardEdge {
240210 Cursor : fmt .Sprintf ("%d" , entry .Rank ),
@@ -266,14 +236,10 @@ func buildLeaderboardConnection(
266236 EndCursor : endCursor ,
267237 }
268238
269- // Build "me" entry
239+ // Build "me" entry using same pre-loaded context
270240 var me * model.LeaderboardEntry
271241 if meEntry != nil {
272- // Compute tags for "me" entry
273- meTags , err := computeLeaderboardTags (ctx , entityType , meEntry .EntityID , projectID , currentUserID , ldrs )
274- if err != nil {
275- return nil , fmt .Errorf ("failed to compute tags for me entry: %w" , err )
276- }
242+ meTags := computeLeaderboardTags (entityType , meEntry .EntityID , currentUserID , viewerCtx )
277243
278244 me = & model.LeaderboardEntry {
279245 ID : meEntry .EntityID ,
0 commit comments