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
This commit is contained in:
Jeremy Wharton 2023-06-07 22:30:01 -05:00 committed by Storj Robot
parent 0cbb0ee12e
commit 5b1c22a1e7
6 changed files with 185 additions and 89 deletions

View File

@ -32,13 +32,15 @@ const (
BucketUsageCursorInputType = "bucketUsageCursor" BucketUsageCursorInputType = "bucketUsageCursor"
// BucketUsageType is a graphql type name for bucket usage. // BucketUsageType is a graphql type name for bucket usage.
BucketUsageType = "bucketUsage" BucketUsageType = "bucketUsage"
// BucketUsagePageType is a field name for bucket usage page. // BucketUsagePageType is a graphql type name for bucket usage page.
BucketUsagePageType = "bucketUsagePage" BucketUsagePageType = "bucketUsagePage"
// ProjectMembersPageType is a field name for project members page. // ProjectMembersPageType is a graphql type name for project members page.
ProjectMembersPageType = "projectMembersPage" 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 is a graphql type name for project members.
ProjectMembersCursorInputType = "projectMembersCursor" ProjectMembersCursorInputType = "projectMembersCursor"
// APIKeysPageType is a field name for api keys page. // APIKeysPageType is a graphql type name for api keys page.
APIKeysPageType = "apiKeysPage" APIKeysPageType = "apiKeysPage"
// APIKeysCursorInputType is a graphql type name for api keys. // APIKeysCursorInputType is a graphql type name for api keys.
APIKeysCursorInputType = "apiKeysCursor" APIKeysCursorInputType = "apiKeysCursor"
@ -54,6 +56,8 @@ const (
FieldDescription = "description" FieldDescription = "description"
// FieldMembers is field name for members. // FieldMembers is field name for members.
FieldMembers = "members" FieldMembers = "members"
// FieldMembersAndInvitations is field name for members and invitations.
FieldMembersAndInvitations = "membersAndInvitations"
// FieldAPIKeys is a field name for api keys. // FieldAPIKeys is a field name for api keys.
FieldAPIKeys = "apiKeys" FieldAPIKeys = "apiKeys"
// FieldUsage is a field name for usage rollup. // FieldUsage is a field name for usage rollup.
@ -84,6 +88,8 @@ const (
FieldProjects = "projects" FieldProjects = "projects"
// FieldProjectMembers is a field name for project members. // FieldProjectMembers is a field name for project members.
FieldProjectMembers = "projectMembers" FieldProjectMembers = "projectMembers"
// FieldProjectInvitations is a field name for project member invitations.
FieldProjectInvitations = "projectInvitations"
// CursorArg is an argument name for cursor. // CursorArg is an argument name for cursor.
CursorArg = "cursor" CursorArg = "cursor"
// PageArg ia an argument name for page number. // PageArg ia an argument name for page number.
@ -106,6 +112,48 @@ const (
// graphqlProject creates *graphql.Object type representation of satellite.ProjectInfo. // graphqlProject creates *graphql.Object type representation of satellite.ProjectInfo.
func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Object { 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{ return graphql.NewObject(graphql.ObjectConfig{
Name: ProjectType, Name: ProjectType,
Fields: graphql.Fields{ Fields: graphql.Fields{
@ -130,6 +178,7 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec
FieldMemberCount: &graphql.Field{ FieldMemberCount: &graphql.Field{
Type: graphql.Int, Type: graphql.Int,
}, },
// TODO: Remove once the frontend has been updated to select membersAndInvitations.
FieldMembers: &graphql.Field{ FieldMembers: &graphql.Field{
Type: types.projectMemberPage, Type: types.projectMemberPage,
Args: graphql.FieldConfigArgument{ Args: graphql.FieldConfigArgument{
@ -138,44 +187,18 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec
}, },
}, },
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
project, _ := p.Source.(*console.Project) return resolveMembersAndInvites(p, console.Members)
},
_, err := console.GetUser(p.Context) },
if err != nil { FieldMembersAndInvitations: &graphql.Field{
return nil, err Type: types.projectMembersAndInvitationsPage,
} Args: graphql.FieldConfigArgument{
CursorArg: &graphql.ArgumentConfig{
cursor := cursorArgsToProjectMembersCursor(p.Args[CursorArg].(map[string]interface{})) Type: graphql.NewNonNull(types.projectMembersCursor),
page, err := service.GetProjectMembers(p.Context, project.ID, cursor) },
if err != nil { },
return nil, err Resolve: func(p graphql.ResolveParams) (interface{}, error) {
} return resolveMembersAndInvites(p, console.MembersAndInvitations)
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
}, },
}, },
FieldAPIKeys: &graphql.Field{ FieldAPIKeys: &graphql.Field{

View File

@ -14,6 +14,8 @@ import (
const ( const (
// ProjectMemberType is a graphql type name for project member. // ProjectMemberType is a graphql type name for project member.
ProjectMemberType = "projectMember" ProjectMemberType = "projectMember"
// ProjectInvitationType is a graphql type name for project member invitation.
ProjectInvitationType = "projectInvitation"
// FieldJoinedAt is a field name for joined at timestamp. // FieldJoinedAt is a field name for joined at timestamp.
FieldJoinedAt = "joinedAt" 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 { func graphqlProjectMembersCursor() *graphql.InputObject {
return graphql.NewInputObject(graphql.InputObjectConfig{ return graphql.NewInputObject(graphql.InputObjectConfig{
Name: ProjectMembersCursorInputType, Name: ProjectMembersCursorInputType,
@ -62,40 +79,67 @@ func graphqlProjectMembersCursor() *graphql.InputObject {
} }
func graphqlProjectMembersPage(types *TypeCreator) *graphql.Object { func graphqlProjectMembersPage(types *TypeCreator) *graphql.Object {
return graphql.NewObject(graphql.ObjectConfig{ fields := graphql.Fields{
Name: ProjectMembersPageType, FieldProjectMembers: &graphql.Field{
Fields: graphql.Fields{ Type: graphql.NewList(types.projectMember),
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,
},
}, },
}
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. // projectMember encapsulates User and joinedAt.
type projectMember struct { type projectMember struct {
User *console.User User *console.User
@ -103,7 +147,8 @@ type projectMember struct {
} }
type projectMembersPage struct { type projectMembersPage struct {
ProjectMembers []projectMember ProjectMembers []projectMember
ProjectInvitations []console.ProjectInvitation
Search string Search string
Limit uint Limit uint

View File

@ -16,18 +16,20 @@ type TypeCreator struct {
query *graphql.Object query *graphql.Object
mutation *graphql.Object mutation *graphql.Object
user *graphql.Object user *graphql.Object
reward *graphql.Object reward *graphql.Object
project *graphql.Object project *graphql.Object
projectUsage *graphql.Object projectUsage *graphql.Object
projectsPage *graphql.Object projectsPage *graphql.Object
bucketUsage *graphql.Object bucketUsage *graphql.Object
bucketUsagePage *graphql.Object bucketUsagePage *graphql.Object
projectMember *graphql.Object projectMember *graphql.Object
projectMemberPage *graphql.Object projectInvitation *graphql.Object
apiKeyPage *graphql.Object projectMemberPage *graphql.Object
apiKeyInfo *graphql.Object projectMembersAndInvitationsPage *graphql.Object
createAPIKey *graphql.Object apiKeyPage *graphql.Object
apiKeyInfo *graphql.Object
createAPIKey *graphql.Object
userInput *graphql.InputObject userInput *graphql.InputObject
projectInput *graphql.InputObject projectInput *graphql.InputObject
@ -112,11 +114,21 @@ func (c *TypeCreator) Create(log *zap.Logger, service *console.Service, mailServ
return err return err
} }
c.projectInvitation = graphqlProjectInvitation()
if err := c.projectInvitation.Error(); err != nil {
return err
}
c.projectMemberPage = graphqlProjectMembersPage(c) c.projectMemberPage = graphqlProjectMembersPage(c)
if err := c.projectMemberPage.Error(); err != nil { if err := c.projectMemberPage.Error(); err != nil {
return err return err
} }
c.projectMembersAndInvitationsPage = graphqlProjectMembersAndInvitationsPage(c)
if err := c.projectMembersAndInvitationsPage.Error(); err != nil {
return err
}
c.apiKeyPage = graphqlAPIKeysPage(c) c.apiKeyPage = graphqlAPIKeysPage(c)
if err := c.apiKeyPage.Error(); err != nil { if err := c.apiKeyPage.Error(); err != nil {
return err return err

View File

@ -62,6 +62,18 @@ type ProjectMembersPage struct {
TotalCount uint64 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. // ProjectMemberOrder is used for querying project members in specified order.
type ProjectMemberOrder int8 type ProjectMemberOrder int8

View File

@ -2113,7 +2113,7 @@ func (s *Service) DeleteProjectMembersAndInvitations(ctx context.Context, projec
} }
// GetProjectMembers returns ProjectMembers for given Project. // 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) defer mon.Task()(&ctx)(&err)
user, err := s.getUserAndAuditLog(ctx, "get project members", zap.String("projectID", projectID.String())) 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 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 { if err != nil {
return nil, Error.Wrap(err) return nil, Error.Wrap(err)
} }

View File

@ -289,17 +289,17 @@ func TestService(t *testing.T) {
t.Run("GetProjectMembers", func(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 // 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.NoError(t, err)
require.Len(t, userPage.ProjectMembers, 2) require.Len(t, userPage.ProjectMembers, 2)
// Getting the project members of a foreign project that one is a part of should work // 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.NoError(t, err)
require.Len(t, userPage.ProjectMembers, 2) require.Len(t, userPage.ProjectMembers, 2)
// Getting the project members of a foreign project that one is not a part of should not work // 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.Error(t, err)
require.Nil(t, userPage) require.Nil(t, userPage)
}) })