From 5b1c22a1e7728b423aa3b08b0fbc63155c86408e Mon Sep 17 00:00:00 2001 From: Jeremy Wharton Date: Wed, 7 Jun 2023 22:30:01 -0500 Subject: [PATCH] satellite/console/consoleweb/consoleql: include invites in member pages Project member invitations may now be requested through GraphQL queries. This is necessary for the satellite frontend to display invitations in the Team page. References #5855 Change-Id: Ibc8526ba768fd82c1b1890201004ef0f066df2fc --- .../console/consoleweb/consoleql/project.go | 105 ++++++++++------- .../consoleweb/consoleql/projectmember.go | 107 +++++++++++++----- .../consoleweb/consoleql/typecreator.go | 36 ++++-- satellite/console/projectmembers.go | 12 ++ satellite/console/service.go | 8 +- satellite/console/service_test.go | 6 +- 6 files changed, 185 insertions(+), 89 deletions(-) diff --git a/satellite/console/consoleweb/consoleql/project.go b/satellite/console/consoleweb/consoleql/project.go index 950c52f63..cc3b875aa 100644 --- a/satellite/console/consoleweb/consoleql/project.go +++ b/satellite/console/consoleweb/consoleql/project.go @@ -32,13 +32,15 @@ const ( BucketUsageCursorInputType = "bucketUsageCursor" // BucketUsageType is a graphql type name for bucket usage. BucketUsageType = "bucketUsage" - // BucketUsagePageType is a field name for bucket usage page. + // BucketUsagePageType is a graphql type name for bucket usage page. BucketUsagePageType = "bucketUsagePage" - // ProjectMembersPageType is a field name for project members page. + // ProjectMembersPageType is a graphql type name for project members page. ProjectMembersPageType = "projectMembersPage" + // ProjectMembersAndInvitationsPageType is a graphql type name for a page of project members and invitations. + ProjectMembersAndInvitationsPageType = "projectMembersAndInvitationsPage" // ProjectMembersCursorInputType is a graphql type name for project members. ProjectMembersCursorInputType = "projectMembersCursor" - // APIKeysPageType is a field name for api keys page. + // APIKeysPageType is a graphql type name for api keys page. APIKeysPageType = "apiKeysPage" // APIKeysCursorInputType is a graphql type name for api keys. APIKeysCursorInputType = "apiKeysCursor" @@ -54,6 +56,8 @@ const ( FieldDescription = "description" // FieldMembers is field name for members. FieldMembers = "members" + // FieldMembersAndInvitations is field name for members and invitations. + FieldMembersAndInvitations = "membersAndInvitations" // FieldAPIKeys is a field name for api keys. FieldAPIKeys = "apiKeys" // FieldUsage is a field name for usage rollup. @@ -84,6 +88,8 @@ const ( FieldProjects = "projects" // FieldProjectMembers is a field name for project members. FieldProjectMembers = "projectMembers" + // FieldProjectInvitations is a field name for project member invitations. + FieldProjectInvitations = "projectInvitations" // CursorArg is an argument name for cursor. CursorArg = "cursor" // PageArg ia an argument name for page number. @@ -106,6 +112,48 @@ const ( // graphqlProject creates *graphql.Object type representation of satellite.ProjectInfo. func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Object { + resolveMembersAndInvites := func(p graphql.ResolveParams, pagingType console.ProjectMembersPagingType) (interface{}, error) { + project, _ := p.Source.(*console.Project) + + _, err := console.GetUser(p.Context) + if err != nil { + return nil, err + } + + cursor := cursorArgsToProjectMembersCursor(p.Args[CursorArg].(map[string]interface{})) + page, err := service.GetProjectMembers(p.Context, project.ID, cursor, pagingType) + if err != nil { + return nil, err + } + + var users []projectMember + for _, member := range page.ProjectMembers { + user, err := service.GetUser(p.Context, member.MemberID) + if err != nil { + return nil, err + } + + users = append(users, projectMember{ + User: user, + JoinedAt: member.CreatedAt, + }) + } + + projectMembersPage := projectMembersPage{ + ProjectMembers: users, + ProjectInvitations: page.ProjectInvitations, + TotalCount: page.TotalCount, + Offset: page.Offset, + Limit: page.Limit, + Order: int(page.Order), + OrderDirection: int(page.OrderDirection), + Search: page.Search, + CurrentPage: page.CurrentPage, + PageCount: page.PageCount, + } + return projectMembersPage, nil + } + return graphql.NewObject(graphql.ObjectConfig{ Name: ProjectType, Fields: graphql.Fields{ @@ -130,6 +178,7 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec FieldMemberCount: &graphql.Field{ Type: graphql.Int, }, + // TODO: Remove once the frontend has been updated to select membersAndInvitations. FieldMembers: &graphql.Field{ Type: types.projectMemberPage, Args: graphql.FieldConfigArgument{ @@ -138,44 +187,18 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { - project, _ := p.Source.(*console.Project) - - _, err := console.GetUser(p.Context) - if err != nil { - return nil, err - } - - cursor := cursorArgsToProjectMembersCursor(p.Args[CursorArg].(map[string]interface{})) - page, err := service.GetProjectMembers(p.Context, project.ID, cursor) - if err != nil { - return nil, err - } - - var users []projectMember - for _, member := range page.ProjectMembers { - user, err := service.GetUser(p.Context, member.MemberID) - if err != nil { - return nil, err - } - - users = append(users, projectMember{ - User: user, - JoinedAt: member.CreatedAt, - }) - } - - projectMembersPage := projectMembersPage{ - ProjectMembers: users, - TotalCount: page.TotalCount, - Offset: page.Offset, - Limit: page.Limit, - Order: int(page.Order), - OrderDirection: int(page.OrderDirection), - Search: page.Search, - CurrentPage: page.CurrentPage, - PageCount: page.PageCount, - } - return projectMembersPage, nil + return resolveMembersAndInvites(p, console.Members) + }, + }, + FieldMembersAndInvitations: &graphql.Field{ + Type: types.projectMembersAndInvitationsPage, + Args: graphql.FieldConfigArgument{ + CursorArg: &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(types.projectMembersCursor), + }, + }, + Resolve: func(p graphql.ResolveParams) (interface{}, error) { + return resolveMembersAndInvites(p, console.MembersAndInvitations) }, }, FieldAPIKeys: &graphql.Field{ diff --git a/satellite/console/consoleweb/consoleql/projectmember.go b/satellite/console/consoleweb/consoleql/projectmember.go index e3573f4d9..f05544cde 100644 --- a/satellite/console/consoleweb/consoleql/projectmember.go +++ b/satellite/console/consoleweb/consoleql/projectmember.go @@ -14,6 +14,8 @@ import ( const ( // ProjectMemberType is a graphql type name for project member. ProjectMemberType = "projectMember" + // ProjectInvitationType is a graphql type name for project member invitation. + ProjectInvitationType = "projectInvitation" // FieldJoinedAt is a field name for joined at timestamp. FieldJoinedAt = "joinedAt" ) @@ -38,6 +40,21 @@ func graphqlProjectMember(service *console.Service, types *TypeCreator) *graphql }) } +// graphqlProjectInvitation creates projectInvitation type. +func graphqlProjectInvitation() *graphql.Object { + return graphql.NewObject(graphql.ObjectConfig{ + Name: ProjectInvitationType, + Fields: graphql.Fields{ + FieldEmail: &graphql.Field{ + Type: graphql.String, + }, + FieldCreatedAt: &graphql.Field{ + Type: graphql.DateTime, + }, + }, + }) +} + func graphqlProjectMembersCursor() *graphql.InputObject { return graphql.NewInputObject(graphql.InputObjectConfig{ Name: ProjectMembersCursorInputType, @@ -62,40 +79,67 @@ func graphqlProjectMembersCursor() *graphql.InputObject { } func graphqlProjectMembersPage(types *TypeCreator) *graphql.Object { - return graphql.NewObject(graphql.ObjectConfig{ - Name: ProjectMembersPageType, - Fields: graphql.Fields{ - FieldProjectMembers: &graphql.Field{ - Type: graphql.NewList(types.projectMember), - }, - SearchArg: &graphql.Field{ - Type: graphql.String, - }, - LimitArg: &graphql.Field{ - Type: graphql.Int, - }, - OrderArg: &graphql.Field{ - Type: graphql.Int, - }, - OrderDirectionArg: &graphql.Field{ - Type: graphql.Int, - }, - OffsetArg: &graphql.Field{ - Type: graphql.Int, - }, - FieldPageCount: &graphql.Field{ - Type: graphql.Int, - }, - FieldCurrentPage: &graphql.Field{ - Type: graphql.Int, - }, - FieldTotalCount: &graphql.Field{ - Type: graphql.Int, - }, + fields := graphql.Fields{ + FieldProjectMembers: &graphql.Field{ + Type: graphql.NewList(types.projectMember), }, + } + for k, v := range commonProjectMembersPageFields() { + fields[k] = v + } + return graphql.NewObject(graphql.ObjectConfig{ + Name: ProjectMembersPageType, + Fields: fields, }) } +func graphqlProjectMembersAndInvitationsPage(types *TypeCreator) *graphql.Object { + fields := graphql.Fields{ + FieldProjectMembers: &graphql.Field{ + Type: graphql.NewList(types.projectMember), + }, + FieldProjectInvitations: &graphql.Field{ + Type: graphql.NewList(types.projectInvitation), + }, + } + for k, v := range commonProjectMembersPageFields() { + fields[k] = v + } + return graphql.NewObject(graphql.ObjectConfig{ + Name: ProjectMembersAndInvitationsPageType, + Fields: fields, + }) +} + +func commonProjectMembersPageFields() graphql.Fields { + return graphql.Fields{ + SearchArg: &graphql.Field{ + Type: graphql.String, + }, + LimitArg: &graphql.Field{ + Type: graphql.Int, + }, + OrderArg: &graphql.Field{ + Type: graphql.Int, + }, + OrderDirectionArg: &graphql.Field{ + Type: graphql.Int, + }, + OffsetArg: &graphql.Field{ + Type: graphql.Int, + }, + FieldPageCount: &graphql.Field{ + Type: graphql.Int, + }, + FieldCurrentPage: &graphql.Field{ + Type: graphql.Int, + }, + FieldTotalCount: &graphql.Field{ + Type: graphql.Int, + }, + } +} + // projectMember encapsulates User and joinedAt. type projectMember struct { User *console.User @@ -103,7 +147,8 @@ type projectMember struct { } type projectMembersPage struct { - ProjectMembers []projectMember + ProjectMembers []projectMember + ProjectInvitations []console.ProjectInvitation Search string Limit uint diff --git a/satellite/console/consoleweb/consoleql/typecreator.go b/satellite/console/consoleweb/consoleql/typecreator.go index a594bf5f8..060d1edcf 100644 --- a/satellite/console/consoleweb/consoleql/typecreator.go +++ b/satellite/console/consoleweb/consoleql/typecreator.go @@ -16,18 +16,20 @@ type TypeCreator struct { query *graphql.Object mutation *graphql.Object - user *graphql.Object - reward *graphql.Object - project *graphql.Object - projectUsage *graphql.Object - projectsPage *graphql.Object - bucketUsage *graphql.Object - bucketUsagePage *graphql.Object - projectMember *graphql.Object - projectMemberPage *graphql.Object - apiKeyPage *graphql.Object - apiKeyInfo *graphql.Object - createAPIKey *graphql.Object + user *graphql.Object + reward *graphql.Object + project *graphql.Object + projectUsage *graphql.Object + projectsPage *graphql.Object + bucketUsage *graphql.Object + bucketUsagePage *graphql.Object + projectMember *graphql.Object + projectInvitation *graphql.Object + projectMemberPage *graphql.Object + projectMembersAndInvitationsPage *graphql.Object + apiKeyPage *graphql.Object + apiKeyInfo *graphql.Object + createAPIKey *graphql.Object userInput *graphql.InputObject projectInput *graphql.InputObject @@ -112,11 +114,21 @@ func (c *TypeCreator) Create(log *zap.Logger, service *console.Service, mailServ return err } + c.projectInvitation = graphqlProjectInvitation() + if err := c.projectInvitation.Error(); err != nil { + return err + } + c.projectMemberPage = graphqlProjectMembersPage(c) if err := c.projectMemberPage.Error(); err != nil { return err } + c.projectMembersAndInvitationsPage = graphqlProjectMembersAndInvitationsPage(c) + if err := c.projectMembersAndInvitationsPage.Error(); err != nil { + return err + } + c.apiKeyPage = graphqlAPIKeysPage(c) if err := c.apiKeyPage.Error(); err != nil { return err diff --git a/satellite/console/projectmembers.go b/satellite/console/projectmembers.go index 17d58f7aa..af9b408fb 100644 --- a/satellite/console/projectmembers.go +++ b/satellite/console/projectmembers.go @@ -62,6 +62,18 @@ type ProjectMembersPage struct { TotalCount uint64 } +// ProjectMembersPagingType determines what types of results should be returned +// in a project members page. +type ProjectMembersPagingType int + +const ( + // Members indicates that only project members should be returned in a project members page. + Members ProjectMembersPagingType = iota + // MembersAndInvitations indicates that project members and project invitations should be + // returned in a project members page. + MembersAndInvitations +) + // ProjectMemberOrder is used for querying project members in specified order. type ProjectMemberOrder int8 diff --git a/satellite/console/service.go b/satellite/console/service.go index 35a42a7a8..821165544 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -2113,7 +2113,7 @@ func (s *Service) DeleteProjectMembersAndInvitations(ctx context.Context, projec } // GetProjectMembers returns ProjectMembers for given Project. -func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, cursor ProjectMembersCursor) (pmp *ProjectMembersPage, err error) { +func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, cursor ProjectMembersCursor, pagingType ProjectMembersPagingType) (pmp *ProjectMembersPage, err error) { defer mon.Task()(&ctx)(&err) user, err := s.getUserAndAuditLog(ctx, "get project members", zap.String("projectID", projectID.String())) @@ -2130,7 +2130,11 @@ func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, cu cursor.Limit = maxLimit } - pmp, err = s.store.ProjectMembers().GetPagedByProjectID(ctx, projectID, cursor) + if pagingType == MembersAndInvitations { + pmp, err = s.store.ProjectMembers().GetPagedWithInvitationsByProjectID(ctx, projectID, cursor) + } else { + pmp, err = s.store.ProjectMembers().GetPagedByProjectID(ctx, projectID, cursor) + } if err != nil { return nil, Error.Wrap(err) } diff --git a/satellite/console/service_test.go b/satellite/console/service_test.go index 5414cad4c..9a54420bd 100644 --- a/satellite/console/service_test.go +++ b/satellite/console/service_test.go @@ -289,17 +289,17 @@ func TestService(t *testing.T) { t.Run("GetProjectMembers", func(t *testing.T) { // Getting the project members of an own project that one is a part of should work - userPage, err := service.GetProjectMembers(userCtx1, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10}) + userPage, err := service.GetProjectMembers(userCtx1, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10}, console.Members) require.NoError(t, err) require.Len(t, userPage.ProjectMembers, 2) // Getting the project members of a foreign project that one is a part of should work - userPage, err = service.GetProjectMembers(userCtx2, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10}) + userPage, err = service.GetProjectMembers(userCtx2, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10}, console.Members) require.NoError(t, err) require.Len(t, userPage.ProjectMembers, 2) // Getting the project members of a foreign project that one is not a part of should not work - userPage, err = service.GetProjectMembers(userCtx1, up2Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10}) + userPage, err = service.GetProjectMembers(userCtx1, up2Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10}, console.Members) require.Error(t, err) require.Nil(t, userPage) })