satellite/{console,web}: make grapql mutations support publicId

This updates project related graphql mutations and queries to support
project publicId while maintaining support for project ID. The frontend
is updated to use only publicId when using these mutations/queries.

Issues:
https://github.com/storj/storj/issues/5409
https://github.com/storj/storj/issues/5413

Change-Id: Ib6241db157de3b37c86a4a98c9f682bf4a047b62
This commit is contained in:
Wilfred Asomani 2023-01-19 14:54:17 +00:00
parent 0e2fef977f
commit e8cd096eec
9 changed files with 216 additions and 67 deletions

View File

@ -5,6 +5,7 @@ package consoleql
import (
"github.com/graphql-go/graphql"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/common/uuid"
@ -86,7 +87,12 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
Type: types.project,
Args: graphql.FieldConfigArgument{
FieldID: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
Type: graphql.String,
DefaultValue: "",
},
FieldPublicID: &graphql.ArgumentConfig{
Type: graphql.String,
DefaultValue: "",
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
@ -98,7 +104,12 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
Type: types.project,
Args: graphql.FieldConfigArgument{
FieldID: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
Type: graphql.String,
DefaultValue: "",
},
FieldPublicID: &graphql.ArgumentConfig{
Type: graphql.String,
DefaultValue: "",
},
ProjectFields: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(types.projectInput),
@ -113,8 +124,7 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
return nil, err
}
inputID := p.Args[FieldID].(string)
projectID, err := uuid.FromString(inputID)
projectID, err := getProjectID(p)
if err != nil {
return nil, err
}
@ -132,7 +142,12 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
Type: types.project,
Args: graphql.FieldConfigArgument{
FieldProjectID: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
Type: graphql.String,
DefaultValue: "",
},
FieldPublicID: &graphql.ArgumentConfig{
Type: graphql.String,
DefaultValue: "",
},
FieldEmail: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.NewList(graphql.String)),
@ -144,10 +159,9 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
return nil, err
}
pID, _ := p.Args[FieldProjectID].(string)
emails, _ := p.Args[FieldEmail].([]interface{})
projectID, err := uuid.FromString(pID)
projectID, err := getProjectID(p)
if err != nil {
return nil, err
}
@ -162,7 +176,7 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
return nil, err
}
users, err := service.AddProjectMembers(p.Context, projectID, userEmails)
users, err := service.AddProjectMembers(p.Context, project.ID, userEmails)
if err != nil {
return nil, err
}
@ -204,17 +218,21 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
Type: types.project,
Args: graphql.FieldConfigArgument{
FieldProjectID: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
Type: graphql.String,
DefaultValue: "",
},
FieldPublicID: &graphql.ArgumentConfig{
Type: graphql.String,
DefaultValue: "",
},
FieldEmail: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.NewList(graphql.String)),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
pID, _ := p.Args[FieldProjectID].(string)
emails, _ := p.Args[FieldEmail].([]interface{})
projectID, err := uuid.FromString(pID)
projectID, err := getProjectID(p)
if err != nil {
return nil, err
}
@ -242,17 +260,21 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
Type: types.createAPIKey,
Args: graphql.FieldConfigArgument{
FieldProjectID: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
Type: graphql.String,
DefaultValue: "",
},
FieldPublicID: &graphql.ArgumentConfig{
Type: graphql.String,
DefaultValue: "",
},
FieldName: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
projectIDField, _ := p.Args[FieldProjectID].(string)
name, _ := p.Args[FieldName].(string)
projectID, err := uuid.FromString(projectIDField)
projectID, err := getProjectID(p)
if err != nil {
return nil, err
}
@ -328,3 +350,29 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
},
})
}
func getProjectID(p graphql.ResolveParams) (projectID uuid.UUID, err error) {
inputID, _ := p.Args[FieldID].(string)
inputProjectID, _ := p.Args[FieldProjectID].(string)
inputPublicID, _ := p.Args[FieldPublicID].(string)
if inputID != "" {
projectID, err = uuid.FromString(inputID)
if err != nil {
return uuid.UUID{}, err
}
} else if inputProjectID != "" {
projectID, err = uuid.FromString(inputProjectID)
if err != nil {
return uuid.UUID{}, err
}
} else if inputPublicID != "" {
projectID, err = uuid.FromString(inputPublicID)
if err != nil {
return uuid.UUID{}, err
}
} else {
return uuid.UUID{}, errs.New("Project ID was not provided.")
}
return projectID, nil
}

View File

@ -286,14 +286,31 @@ func TestGraphqlMutation(t *testing.T) {
user2.Email = "u2@mail.test"
})
t.Run("Add project members mutation", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {addProjectMembers(projectID:\"%s\",email:[\"%s\",\"%s\"]){id,publicId,name,members(cursor: { limit: 50, search: \"\", page: 1, order: 1, orderDirection: 2 }){projectMembers{joinedAt}}}}",
project.ID.String(),
user1.Email,
user2.Email,
)
regTokenUser3, err := service.CreateRegToken(ctx, 1)
require.NoError(t, err)
user3, err := service.CreateUser(userCtx, console.CreateUser{
FullName: "User3",
Email: "u3@mail.test",
Password: "123a123",
}, regTokenUser3.Secret)
require.NoError(t, err)
t.Run("Activation", func(t *testing.T) {
activationToken3, err := service.GenerateActivationToken(
ctx,
user3.ID,
"u3@mail.test",
)
require.NoError(t, err)
_, err = service.ActivateAccount(ctx, activationToken3)
require.NoError(t, err)
user3.Email = "u3@mail.test"
})
testAdd := func(query string, expectedMembers int) {
result, err := testQuery(t, query)
require.NoError(t, err)
@ -306,15 +323,48 @@ func TestGraphqlMutation(t *testing.T) {
assert.Equal(t, project.ID.String(), proj[consoleql.FieldID])
assert.Equal(t, project.PublicID.String(), proj[consoleql.FieldPublicID])
assert.Equal(t, project.Name, proj[consoleql.FieldName])
assert.Equal(t, 3, len(projectMembers))
assert.Equal(t, expectedMembers, len(projectMembers))
}
t.Run("Add project members mutation", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {addProjectMembers(projectID:\"%s\",email:[\"%s\",\"%s\"]){id,publicId,name,members(cursor: { limit: 50, search: \"\", page: 1, order: 1, orderDirection: 2 }){projectMembers{joinedAt}}}}",
project.ID.String(),
user1.Email,
user2.Email,
)
testAdd(query, 3)
})
t.Run("Add project members mutation with publicId", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {addProjectMembers(publicId:\"%s\",email:[\"%s\"]){id,publicId,name,members(cursor: { limit: 50, search: \"\", page: 1, order: 1, orderDirection: 2 }){projectMembers{joinedAt}}}}",
project.PublicID.String(),
user3.Email,
)
testAdd(query, 4)
})
t.Run("Fail add project members mutation without ID", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {addProjectMembers(email:[\"%s\",\"%s\"]){id,publicId,name,members(cursor: { limit: 50, search: \"\", page: 1, order: 1, orderDirection: 2 }){projectMembers{joinedAt}}}}",
user1.Email,
user2.Email,
)
_, err = testQuery(t, query)
require.Error(t, err)
})
t.Run("Delete project members mutation", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {deleteProjectMembers(projectID:\"%s\",email:[\"%s\",\"%s\"]){id,publicId,name,members(cursor: { limit: 50, search: \"\", page: 1, order: 1, orderDirection: 2 }){projectMembers{user{id}}}}}",
"mutation {deleteProjectMembers(projectID:\"%s\",email:[\"%s\",\"%s\",\"%s\"]){id,publicId,name,members(cursor: { limit: 50, search: \"\", page: 1, order: 1, orderDirection: 2 }){projectMembers{user{id}}}}}",
project.ID.String(),
user1.Email,
user2.Email,
user3.Email,
)
result, err := testQuery(t, query)
@ -392,16 +442,7 @@ func TestGraphqlMutation(t *testing.T) {
const StorageLimit = "100"
const BandwidthLimit = "100"
t.Run("Update project mutation", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {updateProject(id:\"%s\",projectFields:{name:\"%s\",description:\"%s\"},projectLimits:{storageLimit:\"%s\",bandwidthLimit:\"%s\"}){id,publicId,name,description}}",
project.ID.String(),
testName,
testDescription,
StorageLimit,
BandwidthLimit,
)
testUpdate := func(query string) {
result, err := testQuery(t, query)
require.NoError(t, err)
@ -412,6 +453,45 @@ func TestGraphqlMutation(t *testing.T) {
assert.Equal(t, project.PublicID.String(), proj[consoleql.FieldPublicID])
assert.Equal(t, testName, proj[consoleql.FieldName])
assert.Equal(t, testDescription, proj[consoleql.FieldDescription])
}
t.Run("Update project mutation", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {updateProject(id:\"%s\",projectFields:{name:\"%s\",description:\"%s\"},projectLimits:{storageLimit:\"%s\",bandwidthLimit:\"%s\"}){id,publicId,name,description}}",
project.ID.String(),
testName,
testDescription,
StorageLimit,
BandwidthLimit,
)
testUpdate(query)
})
t.Run("Update project mutation with publicId", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {updateProject(publicId:\"%s\",projectFields:{name:\"%s\",description:\"%s\"},projectLimits:{storageLimit:\"%s\",bandwidthLimit:\"%s\"}){id,publicId,name,description}}",
project.PublicID.String(),
testName,
testDescription,
StorageLimit,
BandwidthLimit,
)
testUpdate(query)
})
t.Run("Fail update project mutation without ID", func(t *testing.T) {
query := fmt.Sprintf(
"mutation {updateProject(projectFields:{name:\"%s\",description:\"%s\"},projectLimits:{storageLimit:\"%s\",bandwidthLimit:\"%s\"}){id,publicId,name,description}}",
testName,
testDescription,
StorageLimit,
BandwidthLimit,
)
_, err := testQuery(t, query)
require.Error(t, err)
})
t.Run("Delete project mutation", func(t *testing.T) {

View File

@ -6,7 +6,6 @@ package consoleql
import (
"github.com/graphql-go/graphql"
"storj.io/common/uuid"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/mailservice"
)
@ -31,18 +30,22 @@ func rootQuery(service *console.Service, mailService *mailservice.Service, types
Type: types.project,
Args: graphql.FieldConfigArgument{
FieldID: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
Type: graphql.String,
DefaultValue: "",
},
FieldPublicID: &graphql.ArgumentConfig{
Type: graphql.String,
DefaultValue: "",
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
inputID, _ := p.Args[FieldID].(string)
id, err := uuid.FromString(inputID)
projectID, err := getProjectID(p)
if err != nil {
return nil, err
}
project, err := service.GetProject(p.Context, id)
project, err := service.GetProject(p.Context, projectID)
if err != nil {
return nil, err
}

View File

@ -214,6 +214,20 @@ func TestGraphqlQuery(t *testing.T) {
assert.NoError(t, err)
assert.True(t, createdProject.CreatedAt.Equal(createdAt))
// test getting by publicId
query = fmt.Sprintf(
"query {project(publicId:\"%s\"){id,name,publicId,description,createdAt}}",
createdProject.PublicID.String(),
)
result = testQuery(t, query)
data = result.(map[string]interface{})
project = data[consoleql.ProjectQuery].(map[string]interface{})
assert.Equal(t, createdProject.ID.String(), project[consoleql.FieldID])
assert.Equal(t, createdProject.PublicID.String(), project[consoleql.FieldPublicID])
})
regTokenUser1, err := service.CreateRegToken(ctx, 2)

View File

@ -1383,6 +1383,7 @@ func (s *Service) DeleteAccount(ctx context.Context, password string) (err error
}
// GetProject is a method for querying project by id.
// projectID here may be project.PublicID or project.ID.
func (s *Service) GetProject(ctx context.Context, projectID uuid.UUID) (p *Project, err error) {
defer mon.Task()(&ctx)(&err)
user, err := s.getUserAndAuditLog(ctx, "get project", zap.String("projectID", projectID.String()))
@ -1390,15 +1391,13 @@ func (s *Service) GetProject(ctx context.Context, projectID uuid.UUID) (p *Proje
return nil, Error.Wrap(err)
}
if _, err = s.isProjectMember(ctx, user.ID, projectID); err != nil {
return nil, Error.Wrap(err)
}
p, err = s.store.Projects().Get(ctx, projectID)
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
if err != nil {
return nil, Error.Wrap(err)
}
p = isMember.project
return
}
@ -1681,6 +1680,7 @@ func (s *Service) GenDeleteProject(ctx context.Context, projectID uuid.UUID) (ht
}
// UpdateProject is a method for updating project name and description by id.
// projectID here may be project.PublicID or project.ID.
func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, updatedProject ProjectInfo) (p *Project, err error) {
defer mon.Task()(&ctx)(&err)
@ -1728,7 +1728,7 @@ func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, update
return nil, Error.New("specified bandwidth limit exceeds allowed maximum for current tier")
}
storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, projectID)
storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, project.ID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -1736,7 +1736,7 @@ func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, update
return nil, Error.New("cannot set storage limit below current usage")
}
bandwidthUsed, err := s.projectUsage.GetProjectBandwidthTotals(ctx, projectID)
bandwidthUsed, err := s.projectUsage.GetProjectBandwidthTotals(ctx, project.ID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -1883,6 +1883,7 @@ func (s *Service) GenUpdateProject(ctx context.Context, projectID uuid.UUID, pro
// AddProjectMembers adds users by email to given project.
// Email addresses not belonging to a user are ignored.
// projectID here may be project.PublicID or project.ID.
func (s *Service) AddProjectMembers(ctx context.Context, projectID uuid.UUID, emails []string) (users []*User, err error) {
defer mon.Task()(&ctx)(&err)
user, err := s.getUserAndAuditLog(ctx, "add project members", zap.String("projectID", projectID.String()), zap.Strings("emails", emails))
@ -1890,7 +1891,8 @@ func (s *Service) AddProjectMembers(ctx context.Context, projectID uuid.UUID, em
return nil, Error.Wrap(err)
}
if _, err = s.isProjectMember(ctx, user.ID, projectID); err != nil {
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
if err != nil {
return nil, Error.Wrap(err)
}
@ -1908,7 +1910,7 @@ func (s *Service) AddProjectMembers(ctx context.Context, projectID uuid.UUID, em
// add project members in transaction scope
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
for _, user := range users {
if _, err := tx.ProjectMembers().Insert(ctx, user.ID, projectID); err != nil {
if _, err := tx.ProjectMembers().Insert(ctx, user.ID, isMember.project.ID); err != nil {
return err
}
}
@ -1924,6 +1926,7 @@ func (s *Service) AddProjectMembers(ctx context.Context, projectID uuid.UUID, em
}
// DeleteProjectMembers removes users by email from given project.
// projectID here may be project.PublicID or project.ID.
func (s *Service) DeleteProjectMembers(ctx context.Context, projectID uuid.UUID, emails []string) (err error) {
defer mon.Task()(&ctx)(&err)
user, err := s.getUserAndAuditLog(ctx, "delete project members", zap.String("projectID", projectID.String()), zap.Strings("emails", emails))
@ -2007,6 +2010,7 @@ func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, cu
}
// CreateAPIKey creates new api key.
// projectID here may be project.PublicID or project.ID.
func (s *Service) CreateAPIKey(ctx context.Context, projectID uuid.UUID, name string) (_ *APIKeyInfo, _ *macaroon.APIKey, err error) {
defer mon.Task()(&ctx)(&err)
@ -2015,12 +2019,12 @@ func (s *Service) CreateAPIKey(ctx context.Context, projectID uuid.UUID, name st
return nil, nil, Error.Wrap(err)
}
_, err = s.isProjectMember(ctx, user.ID, projectID)
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
if err != nil {
return nil, nil, Error.Wrap(err)
}
_, err = s.store.APIKeys().GetByNameAndProjectID(ctx, name, projectID)
_, err = s.store.APIKeys().GetByNameAndProjectID(ctx, name, isMember.project.ID)
if err == nil {
return nil, nil, ErrValidation.New(apiKeyWithNameExistsErrMsg)
}
@ -2037,7 +2041,7 @@ func (s *Service) CreateAPIKey(ctx context.Context, projectID uuid.UUID, name st
apikey := APIKeyInfo{
Name: name,
ProjectID: projectID,
ProjectID: isMember.project.ID,
Secret: secret,
UserAgent: user.UserAgent,
}

View File

@ -30,7 +30,7 @@ export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
const query =
`query($projectId: String!, $limit: Int!, $search: String!, $page: Int!, $order: Int!, $orderDirection: Int!) {
project (
id: $projectId,
publicId: $projectId,
) {
apiKeys (
cursor: {
@ -82,7 +82,7 @@ export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
const query =
`mutation($projectId: String!, $name: String!) {
createAPIKey(
projectID: $projectId,
publicId: $projectId,
name: $name
) {
key,

View File

@ -22,7 +22,7 @@ export class BucketsApiGql extends BaseGql implements BucketsApi {
public async get(projectId: string, before: Date, cursor: BucketCursor): Promise<BucketPage> {
const query =
`query($projectId: String!, $before: DateTime!, $limit: Int!, $search: String!, $page: Int!) {
project(id: $projectId) {
project(publicId: $projectId) {
bucketUsages(before: $before, cursor: {
limit: $limit, search: $search, page: $page
}) {

View File

@ -16,9 +16,9 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {
const query =
`mutation($projectId: String!, $emails:[String!]!) {
addProjectMembers(
projectID: $projectId,
publicId: $projectId,
email: $emails
) {id}
) {publicId}
}`;
const variables = {
@ -39,9 +39,9 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {
const query =
`mutation($projectId: String!, $emails:[String!]!) {
deleteProjectMembers(
projectID: $projectId,
publicId: $projectId,
email: $emails
) {id}
) {publicId}
}`;
const variables = {
@ -62,7 +62,7 @@ export class ProjectMembersApiGql extends BaseGql implements ProjectMembersApi {
const query =
`query($projectId: String!, $limit: Int!, $search: String!, $page: Int!, $order: Int!, $orderDirection: Int!) {
project (
id: $projectId,
publicId: $projectId,
) {
members (
cursor: {

View File

@ -33,7 +33,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
name: $name,
description: $description,
}
) {id}
) {publicId}
}`;
const variables = {
@ -43,7 +43,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
const response = await this.mutate(query, variables);
return new Project(response.data.createProject.id, variables.name, variables.description, '', projectFields.ownerId);
return new Project(response.data.createProject.publicId, variables.name, variables.description, '', projectFields.ownerId);
}
/**
@ -56,7 +56,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
const query = `query {
myProjects{
name
id
publicId
description
createdAt
ownerId
@ -65,9 +65,9 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
const response = await this.query(query);
return response.data.myProjects.map((project: Project) => {
return response.data.myProjects.map((project: Project & {publicId: string}) => {
return new Project(
project.id,
project.publicId,
project.name,
project.description,
project.createdAt,
@ -89,7 +89,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
const query =
`mutation($projectId: String!, $name: String!, $description: String!, $storageLimit: String!, $bandwidthLimit: String!) {
updateProject(
id: $projectId,
publicId: $projectId,
projectFields: {
name: $name,
description: $description,
@ -122,7 +122,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
const query =
`mutation($projectId: String!) {
deleteProject(
id: $projectId
publicId: $projectId
) {name}
}`;
@ -245,7 +245,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
`query($limit: Int!, $page: Int!) {
ownedProjects( cursor: { limit: $limit, page: $page } ) {
projects {
id,
publicId,
name,
ownerId,
description,
@ -282,7 +282,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
const projects: Project[] = page.projects.map(key =>
new Project(
key.id,
key.publicId,
key.name,
key.description,
key.createdAt,