satellite/console: Add ability to list projects by owner ID

Listing projects by owner ID also includes the number of members in each
project.

Change-Id: I53a09674b60c199ef378943851bb0f164e92e4e2
This commit is contained in:
Moby von Briesen 2021-01-11 16:22:58 -05:00 committed by Maximillian von Briesen
parent 424d2787eb
commit c24f84914c
4 changed files with 183 additions and 1 deletions

View File

@ -48,7 +48,7 @@ func TestAPI(t *testing.T) {
t.Run("GetProject", func(t *testing.T) {
require.NoError(t, err)
expected := fmt.Sprintf(
`{"id":"%s","name":"%s","description":"%s","partnerId":"%s","ownerId":"%s","rateLimit":null,"maxBuckets":null,"createdAt":"%s"}`,
`{"id":"%s","name":"%s","description":"%s","partnerId":"%s","ownerId":"%s","rateLimit":null,"maxBuckets":null,"createdAt":"%s","memberCount":0}`,
project.ID.String(),
project.Name,
project.Description,

View File

@ -33,6 +33,8 @@ type Projects interface {
Update(ctx context.Context, project *Project) error
// List returns paginated projects, created before provided timestamp.
List(ctx context.Context, offset int64, limit int, before time.Time) (ProjectsPage, error)
// ListByOwnerID is a method for querying all projects from the database by ownerID. It also includes the number of members for each project.
ListByOwnerID(ctx context.Context, userID uuid.UUID, limit int, offset int64) (ProjectsPage, error)
// UpdateRateLimit is a method for updating projects rate limit.
UpdateRateLimit(ctx context.Context, id uuid.UUID, newLimit int) error
@ -54,6 +56,7 @@ type Project struct {
RateLimit *int `json:"rateLimit"`
MaxBuckets *int `json:"maxBuckets"`
CreatedAt time.Time `json:"createdAt"`
MemberCount int `json:"memberCount"`
}
// ProjectInfo holds data needed to create/update Project.

View File

@ -6,6 +6,8 @@ package console_test
import (
"math/rand"
"sort"
"strconv"
"strings"
"testing"
"time"
@ -15,6 +17,7 @@ import (
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/common/uuid"
"storj.io/storj/satellite"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
@ -238,6 +241,130 @@ func TestProjectsList(t *testing.T) {
})
}
func TestProjectsListByOwner(t *testing.T) {
const (
limit = 5
length = limit*4 - 1 // make length offset from page size so we can test incomplete page at end
)
rateLimit := 100
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
owner1, err := db.Console().Users().Insert(ctx,
&console.User{
ID: testrand.UUID(),
FullName: "Billy H",
Email: "billyh@example.com",
PasswordHash: []byte("example_password"),
Status: 1,
},
)
require.NoError(t, err)
owner2, err := db.Console().Users().Insert(ctx,
&console.User{
ID: testrand.UUID(),
FullName: "James H",
Email: "james@example.com",
PasswordHash: []byte("example_password_2"),
Status: 1,
},
)
require.NoError(t, err)
projectsDB := db.Console().Projects()
projectMembersDB := db.Console().ProjectMembers()
// Create projects
var owner1Projects []console.Project
var owner2Projects []console.Project
for i := 0; i < length; i++ {
proj1, err := projectsDB.Insert(ctx,
&console.Project{
Name: "owner1example" + strconv.Itoa(i),
Description: "example",
OwnerID: owner1.ID,
RateLimit: &rateLimit,
},
)
require.NoError(t, err)
proj2, err := projectsDB.Insert(ctx,
&console.Project{
Name: "owner2example" + strconv.Itoa(i),
Description: "example",
OwnerID: owner2.ID,
RateLimit: &rateLimit,
},
)
require.NoError(t, err)
// insert 0, 1, or 2 project members
numMembers := i % 3
switch numMembers {
case 1:
_, err = projectMembersDB.Insert(ctx, owner1.ID, proj1.ID)
require.NoError(t, err)
_, err = projectMembersDB.Insert(ctx, owner2.ID, proj2.ID)
require.NoError(t, err)
case 2:
_, err = projectMembersDB.Insert(ctx, owner1.ID, proj1.ID)
require.NoError(t, err)
_, err = projectMembersDB.Insert(ctx, owner2.ID, proj1.ID)
require.NoError(t, err)
_, err = projectMembersDB.Insert(ctx, owner1.ID, proj2.ID)
require.NoError(t, err)
_, err = projectMembersDB.Insert(ctx, owner2.ID, proj2.ID)
require.NoError(t, err)
}
proj1.MemberCount = numMembers
proj2.MemberCount = numMembers
owner1Projects = append(owner1Projects, *proj1)
owner2Projects = append(owner2Projects, *proj2)
}
// test listing for each
var testCases = []struct {
id uuid.UUID
originalProjects []console.Project
}{
{id: owner1.ID, originalProjects: owner1Projects},
{id: owner2.ID, originalProjects: owner2Projects},
}
for _, tt := range testCases {
projsPage, err := projectsDB.ListByOwnerID(ctx, tt.id, limit, 0)
require.NoError(t, err)
require.Len(t, projsPage.Projects, limit)
ownerProjectsDB := projsPage.Projects
for projsPage.Next {
projsPage, err = projectsDB.ListByOwnerID(ctx, tt.id, limit, projsPage.NextOffset)
require.NoError(t, err)
// number of projects should not exceed page limit
require.True(t, len(projsPage.Projects) > 0 && len(projsPage.Projects) <= limit)
ownerProjectsDB = append(ownerProjectsDB, projsPage.Projects...)
}
require.False(t, projsPage.Next)
require.Equal(t, int64(0), projsPage.NextOffset)
require.Equal(t, length, len(ownerProjectsDB))
// sort originalProjects by Name in alphabetical order
originalProjects := tt.originalProjects
sort.SliceStable(originalProjects, func(i, j int) bool {
return strings.Compare(originalProjects[i].Name, originalProjects[j].Name) < 0
})
for i, p := range ownerProjectsDB {
// expect response projects to be in alphabetical order
require.Equal(t, originalProjects[i].Name, p.Name)
require.Equal(t, originalProjects[i].MemberCount, p.MemberCount)
}
}
})
}
func TestGetMaxBuckets(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
maxCount := 100

View File

@ -5,6 +5,7 @@ package satellitedb
import (
"context"
"database/sql"
"time"
"github.com/zeebo/errs"
@ -201,6 +202,57 @@ func (projects *projects) List(ctx context.Context, offset int64, limit int, bef
return page, nil
}
// ListByOwnerID is a method for querying all projects from the database by ownerID. It also includes the number of members for each project.
func (projects *projects) ListByOwnerID(ctx context.Context, ownerID uuid.UUID, limit int, offset int64) (_ console.ProjectsPage, err error) {
defer mon.Task()(&ctx)(&err)
var page console.ProjectsPage
rows, err := projects.sdb.Query(ctx, projects.sdb.Rebind(`
SELECT id, name, description, owner_id, rate_limit, max_buckets, created_at,
(SELECT COUNT(*) FROM project_members WHERE project_id = projects.id) AS member_count
FROM projects
WHERE owner_id = ?
ORDER BY name ASC
OFFSET ? ROWS
LIMIT ?
`), ownerID, offset, limit+1) // add 1 to limit to see if there is another page
if err != nil {
return console.ProjectsPage{}, err
}
defer func() { err = errs.Combine(err, rows.Close()) }()
count := 0
projectsToSend := make([]console.Project, 0, limit)
for rows.Next() {
count++
if count == limit+1 {
// we are done with this page; do not include this project
page.Next = true
page.NextOffset = offset + int64(limit)
break
}
var rateLimit, maxBuckets sql.NullInt32
nextProject := &console.Project{}
err = rows.Scan(&nextProject.ID, &nextProject.Name, &nextProject.Description, &nextProject.OwnerID, &rateLimit, &maxBuckets, &nextProject.CreatedAt, &nextProject.MemberCount)
if err != nil {
return console.ProjectsPage{}, err
}
if rateLimit.Valid {
nextProject.RateLimit = new(int)
*nextProject.RateLimit = int(rateLimit.Int32)
}
if maxBuckets.Valid {
nextProject.MaxBuckets = new(int)
*nextProject.MaxBuckets = int(maxBuckets.Int32)
}
projectsToSend = append(projectsToSend, *nextProject)
}
page.Projects = projectsToSend
return page, rows.Err()
}
// projectFromDBX is used for creating Project entity from autogenerated dbx.Project struct.
func projectFromDBX(ctx context.Context, project *dbx.Project) (_ *console.Project, err error) {
defer mon.Task()(&ctx)(&err)