@@ -214,7 +214,7 @@ func (r *ProjectRepo) AddMemberToProject(ctx context.Context, orgID uuid.UUID, p
214214 }
215215
216216 // Return the created membership
217- return r .FindProjectMembershipByProjectAndID (ctx , orgID , projectID , memberID , membershipType )
217+ return r .FindProjectMembershipDirect (ctx , orgID , projectID , memberID , membershipType )
218218}
219219
220220// RemoveMemberFromProject removes a user or group from a project
@@ -229,7 +229,7 @@ func (r *ProjectRepo) RemoveMemberFromProject(ctx context.Context, orgID uuid.UU
229229 }
230230
231231 // Find the membership to delete
232- m , err := r .queryMembership (orgID , projectID , memberID , membershipType ).Only (ctx )
232+ m , err := r .buildDirectMembershipQuery (orgID , projectID , memberID , membershipType ).Only (ctx )
233233
234234 if err != nil {
235235 if ent .IsNotFound (err ) {
@@ -246,10 +246,10 @@ func (r *ProjectRepo) RemoveMemberFromProject(ctx context.Context, orgID uuid.UU
246246 return nil
247247}
248248
249- // FindProjectMembershipByProjectAndID finds a project membership by project ID and member ID (user or group)
250- func (r * ProjectRepo ) FindProjectMembershipByProjectAndID (ctx context.Context , orgID uuid.UUID , projectID uuid.UUID , memberID uuid.UUID , membershipType authz.MembershipType ) (* biz.ProjectMembership , error ) {
251- // Find the membership
252- m , err := r .queryMembership (orgID , projectID , memberID , membershipType ).Only (ctx )
249+ // FindProjectMembershipDirect finds a project membership by project ID and member ID (user or group)
250+ func (r * ProjectRepo ) FindProjectMembershipDirect (ctx context.Context , orgID uuid.UUID , projectID uuid.UUID , memberID uuid.UUID , membershipType authz.MembershipType ) (* biz.ProjectMembership , error ) {
251+ // Find the membership (direct only)
252+ m , err := r .buildDirectMembershipQuery (orgID , projectID , memberID , membershipType ).Only (ctx )
253253
254254 if err != nil {
255255 if ent .IsNotFound (err ) {
@@ -258,38 +258,29 @@ func (r *ProjectRepo) FindProjectMembershipByProjectAndID(ctx context.Context, o
258258 return nil , fmt .Errorf ("failed to find membership: %w" , err )
259259 }
260260
261- // Build the membership response based on the membership type
262- projectMembership := & biz.ProjectMembership {
263- MembershipType : m .MembershipType ,
264- Role : m .Role ,
265- CreatedAt : & m .CreatedAt ,
266- UpdatedAt : & m .UpdatedAt ,
267- }
268-
261+ // Use centralized converter and fetch associated user/group when needed
269262 switch membershipType {
270263 case authz .MembershipTypeUser :
271- // Fetch the user details for user memberships
272264 u , err := r .data .DB .User .Get (ctx , memberID )
273265 if err != nil {
274266 if ent .IsNotFound (err ) {
275267 return nil , biz .NewErrNotFound ("user" )
276268 }
277269 return nil , fmt .Errorf ("failed to find user: %w" , err )
278270 }
279- projectMembership . User = entUserToBizUser ( u )
271+ return entProjectMembershipToBiz ( m , u , nil ), nil
280272 case authz .MembershipTypeGroup :
281- // Fetch the group details for group memberships
282273 g , err := r .data .DB .Group .Query ().Where (group .ID (memberID ), group .DeletedAtIsNil ()).Only (ctx )
283274 if err != nil {
284275 if ent .IsNotFound (err ) {
285276 return nil , biz .NewErrNotFound ("group" )
286277 }
287278 return nil , fmt .Errorf ("failed to find group: %w" , err )
288279 }
289- projectMembership .Group = entGroupToBiz (g )
280+ return entProjectMembershipToBiz (m , nil , g ), nil
281+ default :
282+ return entProjectMembershipToBiz (m , nil , nil ), nil
290283 }
291-
292- return projectMembership , nil
293284}
294285
295286// UpdateMemberRoleInProject updates the role of a member in a project
@@ -308,7 +299,7 @@ func (r *ProjectRepo) UpdateMemberRoleInProject(ctx context.Context, orgID uuid.
308299 }
309300
310301 // Find the membership to update
311- m , err := r .queryMembership (orgID , projectID , memberID , membershipType ).Only (ctx )
302+ m , err := r .buildDirectMembershipQuery (orgID , projectID , memberID , membershipType ).Only (ctx )
312303
313304 if err != nil {
314305 if ent .IsNotFound (err ) {
@@ -326,8 +317,24 @@ func (r *ProjectRepo) UpdateMemberRoleInProject(ctx context.Context, orgID uuid.
326317 return entProjectMembershipToBiz (m , nil , nil ), nil
327318}
328319
329- // queryMembership is a helper function to build a common membership query
330- func (r * ProjectRepo ) queryMembership (orgID uuid.UUID , projectID uuid.UUID , memberID uuid.UUID , membershipType authz.MembershipType ) * ent.MembershipQuery {
320+ // buildDirectMembershipQuery constructs a query that only considers direct memberships
321+ func (r * ProjectRepo ) buildDirectMembershipQuery (orgID uuid.UUID , projectID uuid.UUID , memberID uuid.UUID , membershipType authz.MembershipType ) * ent.MembershipQuery {
322+ return r .data .DB .Membership .Query ().
323+ Where (
324+ membership .HasOrganizationWith (
325+ organization .ID (orgID ),
326+ ),
327+ membership .MembershipTypeEQ (membershipType ),
328+ membership .MemberID (memberID ),
329+ membership .ResourceTypeEQ (authz .ResourceTypeProject ),
330+ membership .ResourceID (projectID ),
331+ // only direct memberships (parent is nil)
332+ membership .ParentIDIsNil (),
333+ ).WithOrganization ()
334+ }
335+
336+ // buildEffectiveMembershipQuery constructs a query that considers direct and inherited memberships
337+ func (r * ProjectRepo ) buildEffectiveMembershipQuery (orgID uuid.UUID , projectID uuid.UUID , memberID uuid.UUID , membershipType authz.MembershipType ) * ent.MembershipQuery {
331338 return r .data .DB .Membership .Query ().
332339 Where (
333340 membership .HasOrganizationWith (
@@ -337,10 +344,51 @@ func (r *ProjectRepo) queryMembership(orgID uuid.UUID, projectID uuid.UUID, memb
337344 membership .MemberID (memberID ),
338345 membership .ResourceTypeEQ (authz .ResourceTypeProject ),
339346 membership .ResourceID (projectID ),
340- membership .ParentIDIsNil (), // Only top-level memberships
347+ // include both direct (parent nil) and indirect (parent not nil) memberships
348+ membership .Or (
349+ membership .ParentIDIsNil (),
350+ membership .ParentIDNotNil (),
351+ ),
341352 ).WithOrganization ()
342353}
343354
355+ // FindProjectMembershipEffective finds a project membership considering both direct and inherited
356+ // memberships for the member on the project.
357+ func (r * ProjectRepo ) FindProjectMembershipEffective (ctx context.Context , orgID uuid.UUID , projectID uuid.UUID , memberID uuid.UUID , membershipType authz.MembershipType ) (* biz.ProjectMembership , error ) {
358+ m , err := r .buildEffectiveMembershipQuery (orgID , projectID , memberID , membershipType ).Only (ctx )
359+
360+ if err != nil {
361+ if ent .IsNotFound (err ) {
362+ return nil , nil // Return nil when no membership found
363+ }
364+ return nil , fmt .Errorf ("failed to find membership: %w" , err )
365+ }
366+
367+ // Use centralized converter and fetch associated user/group when needed
368+ switch membershipType {
369+ case authz .MembershipTypeUser :
370+ u , err := r .data .DB .User .Get (ctx , memberID )
371+ if err != nil {
372+ if ent .IsNotFound (err ) {
373+ return nil , biz .NewErrNotFound ("user" )
374+ }
375+ return nil , fmt .Errorf ("failed to find user: %w" , err )
376+ }
377+ return entProjectMembershipToBiz (m , u , nil ), nil
378+ case authz .MembershipTypeGroup :
379+ g , err := r .data .DB .Group .Query ().Where (group .ID (memberID ), group .DeletedAtIsNil ()).Only (ctx )
380+ if err != nil {
381+ if ent .IsNotFound (err ) {
382+ return nil , biz .NewErrNotFound ("group" )
383+ }
384+ return nil , fmt .Errorf ("failed to find group: %w" , err )
385+ }
386+ return entProjectMembershipToBiz (m , nil , g ), nil
387+ default :
388+ return entProjectMembershipToBiz (m , nil , nil ), nil
389+ }
390+ }
391+
344392// entProjectToBiz converts an ent.Project to a biz.Project
345393func entProjectToBiz (pro * ent.Project ) * biz.Project {
346394 return & biz.Project {
0 commit comments