From 0decd1419b07cf1f5447ed225a29b5b3c7d20aa6 Mon Sep 17 00:00:00 2001 From: Bogdan Artemenko Date: Mon, 12 Aug 2019 13:22:32 +0300 Subject: [PATCH] satellite/console: project members refactoring (#2752) --- .../consoleweb/consoleql/mutation_test.go | 14 ++- .../console/consoleweb/consoleql/project.go | 72 ++++++++------ .../consoleweb/consoleql/projectmember.go | 72 ++++++++++++++ .../consoleweb/consoleql/query_test.go | 15 ++- .../consoleweb/consoleql/typecreator.go | 40 +++++--- satellite/console/projectmembers.go | 42 +++++++-- satellite/console/projectmembers_test.go | 49 +++++----- satellite/console/service.go | 8 +- satellite/satellitedb/locked.go | 6 +- satellite/satellitedb/projectmembers.go | 94 ++++++++++++++++--- 10 files changed, 308 insertions(+), 104 deletions(-) diff --git a/satellite/console/consoleweb/consoleql/mutation_test.go b/satellite/console/consoleweb/consoleql/mutation_test.go index 5cac1dcaa..0990f4d3a 100644 --- a/satellite/console/consoleweb/consoleql/mutation_test.go +++ b/satellite/console/consoleweb/consoleql/mutation_test.go @@ -415,7 +415,7 @@ func TestGrapqhlMutation(t *testing.T) { t.Run("Add project members mutation", func(t *testing.T) { query := fmt.Sprintf( - "mutation {addProjectMembers(projectID:\"%s\",email:[\"%s\",\"%s\"]){id,name,members(limit:50,offset:0){joinedAt}}}", + "mutation {addProjectMembers(projectID:\"%s\",email:[\"%s\",\"%s\"]){id,name,members(cursor: { limit: 50, search: \"\", page: 1, order: 1, orderDirection: 2 }){projectMembers{joinedAt}}}}", project.ID.String(), user1.Email, user2.Email, @@ -426,14 +426,17 @@ func TestGrapqhlMutation(t *testing.T) { data := result.(map[string]interface{}) proj := data[consoleql.AddProjectMembersMutation].(map[string]interface{}) + members := proj[consoleql.FieldMembers].(map[string]interface{}) + projectMembers := members[consoleql.FieldProjectMembers].([]interface{}) + assert.Equal(t, project.ID.String(), proj[consoleql.FieldID]) assert.Equal(t, project.Name, proj[consoleql.FieldName]) - assert.Equal(t, 3, len(proj[consoleql.FieldMembers].([]interface{}))) + assert.Equal(t, 3, len(projectMembers)) }) t.Run("Delete project members mutation", func(t *testing.T) { query := fmt.Sprintf( - "mutation {deleteProjectMembers(projectID:\"%s\",email:[\"%s\",\"%s\"]){id,name,members(limit:50,offset:0){user{id}}}}", + "mutation {deleteProjectMembers(projectID:\"%s\",email:[\"%s\",\"%s\"]){id,name,members(cursor: { limit: 50, search: \"\", page: 1, order: 1, orderDirection: 2 }){projectMembers{user{id}}}}}", project.ID.String(), user1.Email, user2.Email, @@ -444,8 +447,9 @@ func TestGrapqhlMutation(t *testing.T) { data := result.(map[string]interface{}) proj := data[consoleql.DeleteProjectMembersMutation].(map[string]interface{}) - members := proj[consoleql.FieldMembers].([]interface{}) - rootMember := members[0].(map[string]interface{})[consoleql.UserType].(map[string]interface{}) + members := proj[consoleql.FieldMembers].(map[string]interface{}) + projectMembers := members[consoleql.FieldProjectMembers].([]interface{}) + rootMember := projectMembers[0].(map[string]interface{})[consoleql.UserType].(map[string]interface{}) assert.Equal(t, project.ID.String(), proj[consoleql.FieldID]) assert.Equal(t, project.Name, proj[consoleql.FieldName]) diff --git a/satellite/console/consoleweb/consoleql/project.go b/satellite/console/consoleweb/consoleql/project.go index c631a0a5e..ff2995b07 100644 --- a/satellite/console/consoleweb/consoleql/project.go +++ b/satellite/console/consoleweb/consoleql/project.go @@ -27,6 +27,10 @@ const ( BucketUsagePageType = "bucketUsagePage" // PaymentMethodType is a field name for payment method PaymentMethodType = "paymentMethod" + // ProjectMembersPageType is a field name for project members page + ProjectMembersPageType = "projectMembersPage" + // ProjectMembersCursorInputType is a graphql type name for project members + ProjectMembersCursorInputType = "projectMembersCursor" // FieldName is a field name for "name" FieldName = "name" // FieldBucketName is a field name for "bucket name" @@ -55,6 +59,8 @@ const ( FieldCurrentPage = "currentPage" // FieldTotalCount is a field name for bucket usage count total FieldTotalCount = "totalCount" + // FieldProjectMembers is a field name for project members + FieldProjectMembers = "projectMembers" // FieldCardBrand is a field name for credit card brand FieldCardBrand = "brand" // FieldCardLastFour is a field name for credit card last four digits @@ -75,6 +81,8 @@ const ( SearchArg = "search" // OrderArg is argument name for order OrderArg = "order" + // OrderDirectionArg is argument name for order direction + OrderDirectionArg = "orderDirection" // SinceArg marks start of the period SinceArg = "since" // BeforeArg marks end of the period @@ -99,48 +107,28 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec Type: graphql.DateTime, }, FieldMembers: &graphql.Field{ - Type: graphql.NewList(types.projectMember), + Type: types.projectMemberPage, Args: graphql.FieldConfigArgument{ - OffsetArg: &graphql.ArgumentConfig{ - Type: graphql.NewNonNull(graphql.Int), - }, - LimitArg: &graphql.ArgumentConfig{ - Type: graphql.NewNonNull(graphql.Int), - }, - SearchArg: &graphql.ArgumentConfig{ - Type: graphql.String, - }, - OrderArg: &graphql.ArgumentConfig{ - Type: graphql.Int, + CursorArg: &graphql.ArgumentConfig{ + Type: graphql.NewNonNull(types.projectMembersCursor), }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { project, _ := p.Source.(*console.Project) - offs, _ := p.Args[OffsetArg].(int) - lim, _ := p.Args[LimitArg].(int) - search, _ := p.Args[SearchArg].(string) - order, _ := p.Args[OrderArg].(int) - - pagination := console.Pagination{ - Limit: lim, - Offset: int64(offs), - Search: search, - Order: console.ProjectMemberOrder(order), - } - - members, err := service.GetProjectMembers(p.Context, project.ID, pagination) + _, err := console.GetAuth(p.Context) if err != nil { return nil, err } - _, err = console.GetAuth(p.Context) + 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 members { + for _, member := range page.ProjectMembers { user, err := service.GetUser(p.Context, member.MemberID) if err != nil { return nil, err @@ -152,7 +140,18 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec }) } - return users, nil + 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{ @@ -421,3 +420,20 @@ func fromMapBucketUsageCursor(args map[string]interface{}) (cursor console.Bucke cursor.Search, _ = args[SearchArg].(string) return } + +func cursorArgsToProjectMembersCursor(args map[string]interface{}) console.ProjectMembersCursor { + limit, _ := args[LimitArg].(int) + page, _ := args[PageArg].(int) + order, _ := args[OrderArg].(int) + orderDirection, _ := args[OrderDirectionArg].(int) + + var cursor console.ProjectMembersCursor + + cursor.Limit = uint(limit) + cursor.Page = uint(page) + cursor.Order = console.ProjectMemberOrder(order) + cursor.OrderDirection = console.ProjectMemberOrderDirection(orderDirection) + cursor.Search, _ = args[SearchArg].(string) + + return cursor +} diff --git a/satellite/console/consoleweb/consoleql/projectmember.go b/satellite/console/consoleweb/consoleql/projectmember.go index 79f4dd1ef..a733a6873 100644 --- a/satellite/console/consoleweb/consoleql/projectmember.go +++ b/satellite/console/consoleweb/consoleql/projectmember.go @@ -38,8 +38,80 @@ func graphqlProjectMember(service *console.Service, types *TypeCreator) *graphql }) } +func graphqlProjectMembersCursor() *graphql.InputObject { + return graphql.NewInputObject(graphql.InputObjectConfig{ + Name: ProjectMembersCursorInputType, + Fields: graphql.InputObjectConfigFieldMap{ + SearchArg: &graphql.InputObjectFieldConfig{ + Type: graphql.NewNonNull(graphql.String), + }, + LimitArg: &graphql.InputObjectFieldConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + PageArg: &graphql.InputObjectFieldConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + OrderArg: &graphql.InputObjectFieldConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + OrderDirectionArg: &graphql.InputObjectFieldConfig{ + Type: graphql.NewNonNull(graphql.Int), + }, + }, + }) +} + +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, + }, + }, + }) +} + // projectMember encapsulates User and joinedAt type projectMember struct { User *console.User JoinedAt time.Time } + +type projectMembersPage struct { + ProjectMembers []projectMember + + Search string + Limit uint + Order int + OrderDirection int + Offset uint64 + + PageCount uint + CurrentPage uint + TotalCount uint64 +} diff --git a/satellite/console/consoleweb/consoleql/query_test.go b/satellite/console/consoleweb/consoleql/query_test.go index cdff514dc..57f53de3e 100644 --- a/satellite/console/consoleweb/consoleql/query_test.go +++ b/satellite/console/consoleweb/consoleql/query_test.go @@ -239,17 +239,22 @@ func TestGraphqlQuery(t *testing.T) { t.Run("Project query team members", func(t *testing.T) { query := fmt.Sprintf( - "query {project(id:\"%s\"){members(offset:0, limit:50){user{id,fullName,shortName,email,createdAt}}}}", + "query {project(id: \"%s\") {members( cursor: { limit: %d, search: \"%s\", page: %d, order: %d, orderDirection: %d } ) { projectMembers{ user { id, fullName, shortName, email, createdAt }, joinedAt }, search, limit, order, offset, pageCount, currentPage, totalCount } } }", createdProject.ID.String(), - ) + 5, + "", + 1, + 1, + 2) result := testQuery(t, query) data := result.(map[string]interface{}) project := data[consoleql.ProjectQuery].(map[string]interface{}) - members := project[consoleql.FieldMembers].([]interface{}) + members := project[consoleql.FieldMembers].(map[string]interface{}) + projectMembers := members[consoleql.FieldProjectMembers].([]interface{}) - assert.Equal(t, 3, len(members)) + assert.Equal(t, 3, len(projectMembers)) testUser := func(t *testing.T, actual map[string]interface{}, expected *console.User) { assert.Equal(t, expected.Email, actual[consoleql.FieldEmail]) @@ -265,7 +270,7 @@ func TestGraphqlQuery(t *testing.T) { var foundRoot, foundU1, foundU2 bool - for _, entry := range members { + for _, entry := range projectMembers { member := entry.(map[string]interface{}) user := member[consoleql.UserType].(map[string]interface{}) diff --git a/satellite/console/consoleweb/consoleql/typecreator.go b/satellite/console/consoleweb/consoleql/typecreator.go index b83c4cc60..09aef46f9 100644 --- a/satellite/console/consoleweb/consoleql/typecreator.go +++ b/satellite/console/consoleweb/consoleql/typecreator.go @@ -18,21 +18,23 @@ type TypeCreator struct { token *graphql.Object - user *graphql.Object - reward *graphql.Object - creditUsage *graphql.Object - project *graphql.Object - projectUsage *graphql.Object - bucketUsage *graphql.Object - bucketUsagePage *graphql.Object - paymentMethod *graphql.Object - projectMember *graphql.Object - apiKeyInfo *graphql.Object - createAPIKey *graphql.Object + user *graphql.Object + reward *graphql.Object + creditUsage *graphql.Object + project *graphql.Object + projectUsage *graphql.Object + bucketUsage *graphql.Object + bucketUsagePage *graphql.Object + paymentMethod *graphql.Object + projectMember *graphql.Object + projectMemberPage *graphql.Object + apiKeyInfo *graphql.Object + createAPIKey *graphql.Object - userInput *graphql.InputObject - projectInput *graphql.InputObject - bucketUsageCursor *graphql.InputObject + userInput *graphql.InputObject + projectInput *graphql.InputObject + bucketUsageCursor *graphql.InputObject + projectMembersCursor *graphql.InputObject } // Create create types and check for error @@ -53,6 +55,11 @@ func (c *TypeCreator) Create(log *zap.Logger, service *console.Service, mailServ return err } + c.projectMembersCursor = graphqlProjectMembersCursor() + if err := c.projectMembersCursor.Error(); err != nil { + return err + } + // entities c.user = graphqlUser() if err := c.user.Error(); err != nil { @@ -104,6 +111,11 @@ func (c *TypeCreator) Create(log *zap.Logger, service *console.Service, mailServ return err } + c.projectMemberPage = graphqlProjectMembersPage(c) + if err := c.projectMemberPage.Error(); err != nil { + return err + } + c.project = graphqlProject(service, c) if err := c.project.Error(); err != nil { return err diff --git a/satellite/console/projectmembers.go b/satellite/console/projectmembers.go index 2b5a4753e..3e6243f0f 100644 --- a/satellite/console/projectmembers.go +++ b/satellite/console/projectmembers.go @@ -14,8 +14,8 @@ import ( type ProjectMembers interface { // GetByMemberID is a method for querying project members from the database by memberID. GetByMemberID(ctx context.Context, memberID uuid.UUID) ([]ProjectMember, error) - // GetByProjectID is a method for querying project members from the database by projectID, offset and limit. - GetByProjectID(ctx context.Context, projectID uuid.UUID, pagination Pagination) ([]ProjectMember, error) + // GetPagedByProjectID is a method for querying project members from the database by projectID and cursor + GetPagedByProjectID(ctx context.Context, projectID uuid.UUID, cursor ProjectMembersCursor) (*ProjectMembersPage, error) // Insert is a method for inserting project member into the database. Insert(ctx context.Context, memberID, projectID uuid.UUID) (*ProjectMember, error) // Delete is a method for deleting project member by memberID and projectID from the database. @@ -32,12 +32,28 @@ type ProjectMember struct { CreatedAt time.Time } -// Pagination defines pagination, filtering and sorting rules -type Pagination struct { - Limit int - Offset int64 - Search string - Order ProjectMemberOrder +// ProjectMembersCursor holds info for project members cursor pagination +type ProjectMembersCursor struct { + Search string + Limit uint + Page uint + Order ProjectMemberOrder + OrderDirection ProjectMemberOrderDirection +} + +// ProjectMembersPage represent project members page result +type ProjectMembersPage struct { + ProjectMembers []ProjectMember + + Search string + Limit uint + Order ProjectMemberOrder + OrderDirection ProjectMemberOrderDirection + Offset uint64 + + PageCount uint + CurrentPage uint + TotalCount uint64 } // ProjectMemberOrder is used for querying project members in specified order @@ -51,3 +67,13 @@ const ( // Created indicates that we should order by created date Created ProjectMemberOrder = 3 ) + +// ProjectMemberOrderDirection is used for querying project members in specific order direction +type ProjectMemberOrderDirection uint8 + +const ( + // Ascending indicates that we should order ascending + Ascending ProjectMemberOrderDirection = 1 + // Descending indicates that we should order descending + Descending ProjectMemberOrderDirection = 2 +) diff --git a/satellite/console/projectmembers_test.go b/satellite/console/projectmembers_test.go index 8c32d2f48..bb50216b0 100644 --- a/satellite/console/projectmembers_test.go +++ b/satellite/console/projectmembers_test.go @@ -84,40 +84,45 @@ func TestProjectMembersRepository(t *testing.T) { t.Run("Get paged", func(t *testing.T) { // sql injection test. F.E '%SomeText%' = > ''%SomeText%' OR 'x' != '%'' will be true - members, err := projectMembers.GetByProjectID(ctx, createdProjects[0].ID, console.Pagination{Limit: 6, Offset: 0, Search: "son%' OR 'x' != '", Order: 2}) - assert.NoError(t, err) - assert.Nil(t, members) - assert.Equal(t, 0, len(members)) - - members, err = projectMembers.GetByProjectID(ctx, createdProjects[0].ID, console.Pagination{Limit: 3, Offset: 0, Search: "", Order: 1}) + members, err := projectMembers.GetPagedByProjectID(ctx, createdProjects[0].ID, console.ProjectMembersCursor{Limit: 6, Search: "son%' OR 'x' != '", Order: 2, Page: 1}) assert.NoError(t, err) assert.NotNil(t, members) - assert.Equal(t, 3, len(members)) + assert.Equal(t, uint64(0), members.TotalCount) + assert.Equal(t, uint(0), members.CurrentPage) + assert.Equal(t, uint(0), members.PageCount) + assert.Equal(t, 0, len(members.ProjectMembers)) - members, err = projectMembers.GetByProjectID(ctx, createdProjects[0].ID, console.Pagination{Limit: 2, Offset: 0, Search: "iam", Order: 2}) // TODO: fix case sensitity issues and change back to "Liam" + members, err = projectMembers.GetPagedByProjectID(ctx, createdProjects[0].ID, console.ProjectMembersCursor{Limit: 3, Search: "", Order: 1, Page: 1}) assert.NoError(t, err) assert.NotNil(t, members) - assert.Equal(t, 2, len(members)) + assert.Equal(t, uint64(5), members.TotalCount) + assert.Equal(t, uint(1), members.CurrentPage) + assert.Equal(t, uint(2), members.PageCount) + assert.Equal(t, 3, len(members.ProjectMembers)) - members, err = projectMembers.GetByProjectID(ctx, createdProjects[0].ID, console.Pagination{Limit: 2, Offset: 0, Search: "iam", Order: 1}) // TODO: fix case sensitity issues and change back to "Liam" + members, err = projectMembers.GetPagedByProjectID(ctx, createdProjects[0].ID, console.ProjectMembersCursor{Limit: 2, Search: "iam", Order: 2, Page: 1}) // TODO: fix case sensitity issues and change back to "Liam" assert.NoError(t, err) assert.NotNil(t, members) - assert.Equal(t, 2, len(members)) + assert.Equal(t, uint64(2), members.TotalCount) + assert.Equal(t, 2, len(members.ProjectMembers)) - members, err = projectMembers.GetByProjectID(ctx, createdProjects[0].ID, console.Pagination{Limit: 6, Offset: 0, Search: "son", Order: 123}) + members, err = projectMembers.GetPagedByProjectID(ctx, createdProjects[0].ID, console.ProjectMembersCursor{Limit: 2, Search: "iam", Order: 1, Page: 1}) // TODO: fix case sensitity issues and change back to "Liam" assert.NoError(t, err) assert.NotNil(t, members) - assert.Equal(t, 5, len(members)) + assert.Equal(t, uint64(2), members.TotalCount) + assert.Equal(t, 2, len(members.ProjectMembers)) - members, err = projectMembers.GetByProjectID(ctx, createdProjects[0].ID, console.Pagination{Limit: 6, Offset: 3, Search: "son", Order: 2}) + members, err = projectMembers.GetPagedByProjectID(ctx, createdProjects[0].ID, console.ProjectMembersCursor{Limit: 6, Search: "son", Order: 123, Page: 1}) assert.NoError(t, err) assert.NotNil(t, members) - assert.Equal(t, 2, len(members)) + assert.Equal(t, uint64(5), members.TotalCount) + assert.Equal(t, 5, len(members.ProjectMembers)) - members, err = projectMembers.GetByProjectID(ctx, createdProjects[0].ID, console.Pagination{Limit: -123, Offset: -14, Search: "son", Order: 2}) - assert.Error(t, err) - assert.Nil(t, members) - assert.Equal(t, 0, len(members)) + members, err = projectMembers.GetPagedByProjectID(ctx, createdProjects[0].ID, console.ProjectMembersCursor{Limit: 6, Search: "son", Order: 2, Page: 1}) + assert.NoError(t, err) + assert.NotNil(t, members) + assert.Equal(t, uint64(5), members.TotalCount) + assert.Equal(t, 5, len(members.ProjectMembers)) }) t.Run("Get member by memberID success", func(t *testing.T) { @@ -140,15 +145,15 @@ func TestProjectMembersRepository(t *testing.T) { err := projectMembers.Delete(ctx, createdUsers[0].ID, createdProjects[0].ID) assert.NoError(t, err) - projMembers, err := projectMembers.GetByProjectID(ctx, createdProjects[0].ID, console.Pagination{ + projMembers, err := projectMembers.GetPagedByProjectID(ctx, createdProjects[0].ID, console.ProjectMembersCursor{ Order: 1, Search: "", - Offset: 0, Limit: 100, + Page: 1, }) assert.NoError(t, err) assert.NotNil(t, projectMembers) - assert.Equal(t, len(projMembers), 4) + assert.Equal(t, len(projMembers.ProjectMembers), 4) }) }) } diff --git a/satellite/console/service.go b/satellite/console/service.go index 24d9637b9..83809158c 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -895,7 +895,7 @@ func (s *Service) DeleteProjectMembers(ctx context.Context, projectID uuid.UUID, } // GetProjectMembers returns ProjectMembers for given Project -func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, pagination Pagination) (pm []ProjectMember, err error) { +func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, cursor ProjectMembersCursor) (pmp *ProjectMembersPage, err error) { defer mon.Task()(&ctx)(&err) auth, err := GetAuth(ctx) if err != nil { @@ -907,11 +907,11 @@ func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, pa return nil, ErrUnauthorized.Wrap(err) } - if pagination.Limit > maxLimit { - pagination.Limit = maxLimit + if cursor.Limit > maxLimit { + cursor.Limit = maxLimit } - pm, err = s.store.ProjectMembers().GetByProjectID(ctx, projectID, pagination) + pmp, err = s.store.ProjectMembers().GetPagedByProjectID(ctx, projectID, cursor) if err != nil { return nil, errs.New(internalErrMsg) } diff --git a/satellite/satellitedb/locked.go b/satellite/satellitedb/locked.go index 0601e0451..e46d2b08d 100644 --- a/satellite/satellitedb/locked.go +++ b/satellite/satellitedb/locked.go @@ -328,11 +328,11 @@ func (m *lockedProjectMembers) GetByMemberID(ctx context.Context, memberID uuid. return m.db.GetByMemberID(ctx, memberID) } -// GetByProjectID is a method for querying project members from the database by projectID, offset and limit. -func (m *lockedProjectMembers) GetByProjectID(ctx context.Context, projectID uuid.UUID, pagination console.Pagination) ([]console.ProjectMember, error) { +// GetPagedByProjectID is a method for querying project members from the database by projectID and cursor +func (m *lockedProjectMembers) GetPagedByProjectID(ctx context.Context, projectID uuid.UUID, cursor console.ProjectMembersCursor) (*console.ProjectMembersPage, error) { m.Lock() defer m.Unlock() - return m.db.GetByProjectID(ctx, projectID, pagination) + return m.db.GetPagedByProjectID(ctx, projectID, cursor) } // Insert is a method for inserting project member into the database. diff --git a/satellite/satellitedb/projectmembers.go b/satellite/satellitedb/projectmembers.go index 86927d6b3..b792033fe 100644 --- a/satellite/satellitedb/projectmembers.go +++ b/satellite/satellitedb/projectmembers.go @@ -32,30 +32,75 @@ func (pm *projectMembers) GetByMemberID(ctx context.Context, memberID uuid.UUID) } // GetByProjectID is a method for querying project members from the database by projectID, offset and limit. -func (pm *projectMembers) GetByProjectID(ctx context.Context, projectID uuid.UUID, pagination console.Pagination) (_ []console.ProjectMember, err error) { +func (pm *projectMembers) GetPagedByProjectID(ctx context.Context, projectID uuid.UUID, cursor console.ProjectMembersCursor) (_ *console.ProjectMembersPage, err error) { defer mon.Task()(&ctx)(&err) - if pagination.Limit < 0 || pagination.Offset < 0 { - return nil, errs.New("invalid pagination argument") + + search := "%" + strings.Replace(cursor.Search, " ", "%", -1) + "%" + + if cursor.Limit > 50 { + cursor.Limit = 50 } - var projectMembers []console.ProjectMember - searchSubQuery := "%" + strings.Replace(pagination.Search, " ", "%", -1) + "%" - //`+getOrder(pagination.Order)+` + if cursor.Page == 0 { + return nil, errs.New("page cannot be 0") + } + page := &console.ProjectMembersPage{ + Search: cursor.Search, + Limit: cursor.Limit, + Offset: uint64((cursor.Page - 1) * cursor.Limit), + Order: cursor.Order, + OrderDirection: cursor.OrderDirection, + } + + countQuery := pm.db.Rebind(` + SELECT COUNT(*) + FROM project_members pm + INNER JOIN users u ON pm.member_id = u.id + WHERE pm.project_id = ? + AND ( u.email LIKE ? OR + u.full_name LIKE ? OR + u.short_name LIKE ? + )`) + + countRow := pm.db.QueryRowContext(ctx, + countQuery, + projectID[:], + search, + search, + search) + + err = countRow.Scan(&page.TotalCount) + if err != nil { + return nil, err + } + if page.TotalCount == 0 { + return page, nil + } + if page.Offset > page.TotalCount-1 { + return nil, errs.New("page is out of range") + } // TODO: LIKE is case-sensitive postgres, however this should be case-insensitive and possibly allow typos reboundQuery := pm.db.Rebind(` SELECT pm.* FROM project_members pm INNER JOIN users u ON pm.member_id = u.id - WHERE pm.project_id = ? - AND ( u.email LIKE ? OR - u.full_name LIKE ? OR - u.short_name LIKE ? ) - ORDER BY ` + sanitizedOrderColumnName(pagination.Order) + ` ASC - LIMIT ? OFFSET ? - `) + WHERE pm.project_id = ? + AND ( u.email LIKE ? OR + u.full_name LIKE ? OR + u.short_name LIKE ? ) + ORDER BY ` + sanitizedOrderColumnName(cursor.Order) + ` + ` + sanitizeOrderDirectionName(page.OrderDirection) + ` + LIMIT ? OFFSET ?`) - rows, err := pm.db.Query(reboundQuery, projectID[:], searchSubQuery, searchSubQuery, searchSubQuery, pagination.Limit, pagination.Offset) + rows, err := pm.db.QueryContext(ctx, + reboundQuery, + projectID[:], + search, + search, + search, + page.Limit, + page.Offset) defer func() { err = errs.Combine(err, rows.Close()) @@ -65,6 +110,7 @@ func (pm *projectMembers) GetByProjectID(ctx context.Context, projectID uuid.UUI return nil, err } + var projectMembers []console.ProjectMember for rows.Next() { pm := console.ProjectMember{} var memberIDBytes, projectIDBytes []uint8 @@ -91,7 +137,17 @@ func (pm *projectMembers) GetByProjectID(ctx context.Context, projectID uuid.UUI projectMembers = append(projectMembers, pm) } - return projectMembers, err + page.ProjectMembers = projectMembers + page.Order = cursor.Order + + page.PageCount = uint(page.TotalCount / uint64(cursor.Limit)) + if page.TotalCount%uint64(cursor.Limit) != 0 { + page.PageCount++ + } + + page.CurrentPage = cursor.Page + + return page, err } // Insert is a method for inserting project member into the database. @@ -155,6 +211,14 @@ func sanitizedOrderColumnName(pmo console.ProjectMemberOrder) string { } } +func sanitizeOrderDirectionName(pmo console.ProjectMemberOrderDirection) string { + if pmo == 2 { + return "DESC" + } + + return "ASC" +} + // projectMembersFromDbxSlice is used for creating []ProjectMember entities from autogenerated []*dbx.ProjectMember struct func projectMembersFromDbxSlice(ctx context.Context, projectMembersDbx []*dbx.ProjectMember) (_ []console.ProjectMember, err error) { defer mon.Task()(&ctx)(&err)