feat(workspaces): move project to workspace (#2992)

* feat(workspaces): move project, like this

* fix(workspaces): use new event

* fix(workspaces): add resolver again after merge

* chore(workspaces): lint

* fix(workspaces): works but is a bit illegal

* fix(workspaces): use service update

* chore(workspaces): add unit tests

* fix(workspaces): use transaction

---------

Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
This commit is contained in:
Chuck Driesler 2024-09-18 09:38:27 +01:00 коммит произвёл GitHub
Родитель ce55e5474b
Коммит 56d392424d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
16 изменённых файлов: 751 добавлений и 29 удалений

Просмотреть файл

@ -122,14 +122,14 @@ type WorkspaceMutations {
deleteDomain(input: WorkspaceDomainDeleteInput!): Workspace!
@hasScope(scope: "workspace:update")
invites: WorkspaceInviteMutations!
projects: WorkspaceProjectMutations!
projects: WorkspaceProjectMutations! @hasServerRole(role: SERVER_USER)
}
type WorkspaceProjectMutations {
updateRole(input: ProjectUpdateRoleInput!): Project!
@hasServerRole(role: SERVER_USER)
@hasStreamRole(role: STREAM_OWNER)
@hasWorkspaceRole(role: MEMBER)
moveToWorkspace(projectId: String!, workspaceId: String!): Project!
}
input WorkspaceDomainDeleteInput {

Просмотреть файл

@ -1,6 +1,17 @@
import { ProjectTeamMember } from '@/modules/core/domain/projects/types'
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
import { StreamRoles } from '@speckle/shared'
export type GetProject = (args: { projectId: string }) => Promise<StreamRecord>
export type GetProjectCollaborators = (args: {
projectId: string
}) => Promise<ProjectTeamMember[]>
export type UpdateProject = (args: {
projectUpdate: Pick<StreamRecord, 'id' | 'workspaceId'>
}) => Promise<StreamRecord>
export type UpsertProjectRole = (args: {
projectId: string
userId: string

Просмотреть файл

@ -0,0 +1,6 @@
import { LimitedUserRecord, UserWithRole } from '@/modules/core/helpers/types'
import { StreamRoles } from '@speckle/shared'
export type ProjectTeamMember = UserWithRole<LimitedUserRecord> & {
streamRole: StreamRoles
}

Просмотреть файл

@ -4132,10 +4132,17 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
moveToWorkspace: Project;
updateRole: Project;
};
export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
};
export type WorkspaceProjectMutationsUpdateRoleArgs = {
input: ProjectUpdateRoleInput;
};
@ -6203,6 +6210,7 @@ export type WorkspaceMutationsResolvers<ContextType = GraphQLContext, ParentType
};
export type WorkspaceProjectMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceProjectMutations'] = ResolversParentTypes['WorkspaceProjectMutations']> = {
moveToWorkspace?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsMoveToWorkspaceArgs, 'projectId' | 'workspaceId'>>;
updateRole?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsUpdateRoleArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

Просмотреть файл

@ -50,12 +50,19 @@ import dayjs from 'dayjs'
import cryptoRandomString from 'crypto-random-string'
import { Knex } from 'knex'
import { isProjectCreateInput } from '@/modules/core/helpers/stream'
import { StreamAccessUpdateError } from '@/modules/core/errors/stream'
import {
StreamAccessUpdateError,
StreamNotFoundError,
StreamUpdateError
} from '@/modules/core/errors/stream'
import { metaHelpers } from '@/modules/core/helpers/meta'
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
import { db as defaultKnexInstance } from '@/db/knex'
import {
DeleteProjectRole,
GetProject,
GetProjectCollaborators,
UpdateProject,
GetRolesByUserId,
UpsertProjectRole
} from '@/modules/core/domain/projects/operations'
@ -168,6 +175,19 @@ export async function getStream(
return <Optional<StreamWithOptionalRole>>streams[0]
}
// TODO: Inject db
export const getProjectFactory =
(): GetProject =>
async ({ projectId }) => {
const project = await getStream({ streamId: projectId })
if (!project) {
throw new StreamNotFoundError()
}
return project
}
export type StreamWithCommitId = StreamWithOptionalRole & { commitId: string }
export async function getCommitStreams(params: {
@ -645,6 +665,13 @@ export async function getStreamCollaborators(streamId: string, type?: StreamRole
return items
}
// TODO: Inject db
export const getProjectCollaboratorsFactory =
(): GetProjectCollaborators =>
async ({ projectId }) => {
return await getStreamCollaborators(projectId)
}
type BaseUserStreamsQueryParams = {
/**
* User whose streams we wish to find
@ -904,7 +931,11 @@ const isProjectUpdateInput = (
i: StreamUpdateInput | ProjectUpdateInput
): i is ProjectUpdateInput => has(i, 'visibility')
export async function updateStream(update: StreamUpdateInput | ProjectUpdateInput) {
/** @deprecated Replace all calls with `updateProjectFacotry` */
export async function updateStream(
update: StreamUpdateInput | ProjectUpdateInput,
db?: Knex
) {
const { id: streamId } = update
if (!update.name) update.name = null // to prevent saving name ''
@ -936,7 +967,8 @@ export async function updateStream(update: StreamUpdateInput | ProjectUpdateInpu
if (!Object.keys(validUpdate).length) return null
const [updatedStream] = await Streams.knex()
const [updatedStream] = await tables
.streams(db ?? knex)
.returning('*')
.where({ id: streamId })
.update<StreamRecord[]>({
@ -947,6 +979,18 @@ export async function updateStream(update: StreamUpdateInput | ProjectUpdateInpu
return updatedStream
}
export const updateProjectFactory =
({ db }: { db: Knex }): UpdateProject =>
async ({ projectUpdate }) => {
const updatedStream = await updateStream(projectUpdate, db)
if (!updatedStream) {
throw new StreamUpdateError()
}
return updatedStream
}
export async function markBranchStreamUpdated(branchId: string) {
const q = Streams.knex()
.whereIn(Streams.col.id, (w) => {

Просмотреть файл

@ -4118,10 +4118,17 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
moveToWorkspace: Project;
updateRole: Project;
};
export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
};
export type WorkspaceProjectMutationsUpdateRoleArgs = {
input: ProjectUpdateRoleInput;
};

Просмотреть файл

@ -0,0 +1,14 @@
import { UserRoleData } from '@/modules/shared/domain/rolesAndScopes/types'
import { AvailableRoles } from '@speckle/shared'
import { isUndefined } from 'lodash'
export const orderByWeight = <T extends AvailableRoles>(
roles: T[],
definitions: UserRoleData<T>[]
): UserRoleData<T>[] => {
const roleDefinitions = roles
.map((role) => definitions.find((definition) => definition.name === role))
.filter((definition): definition is UserRoleData<T> => !isUndefined(definition))
return roleDefinitions.sort((a, b) => b.weight - a.weight)
}

Просмотреть файл

@ -0,0 +1,31 @@
import { orderByWeight } from '@/modules/shared/domain/rolesAndScopes/logic'
import coreUserRoles from '@/modules/core/roles'
import { workspaceRoles } from '@/modules/workspaces/roles'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
describe('orderByWeight', () => {
it('should return the highest weighted server role first', () => {
const result = orderByWeight(
[Roles.Server.Guest, Roles.Server.User, Roles.Server.Admin],
coreUserRoles
)
expect(result[0].name).to.equal(Roles.Server.Admin)
})
it('should return the highest weighted stream role first', () => {
const result = orderByWeight(
[Roles.Stream.Reviewer, Roles.Stream.Contributor, Roles.Stream.Owner],
coreUserRoles
)
expect(result[0].name).to.equal(Roles.Stream.Owner)
})
it('should return the highest weighted workspace role first', () => {
const result = orderByWeight(
[Roles.Workspace.Guest, Roles.Workspace.Member, Roles.Workspace.Admin],
workspaceRoles
)
expect(result[0].name).to.equal(Roles.Workspace.Admin)
})
})

Просмотреть файл

@ -132,8 +132,23 @@ export type GetWorkspaceRolesForUser = (
options?: GetWorkspaceRolesForUserOptions
) => Promise<WorkspaceAcl[]>
/** Repository-level change to workspace acl record */
export type UpsertWorkspaceRole = (args: WorkspaceAcl) => Promise<void>
/** Service-level change with protection against invalid role changes */
export type UpdateWorkspaceRole = (
args: Pick<WorkspaceAcl, 'userId' | 'workspaceId' | 'role'> & {
/**
* If this gets triggered from a project role update, we don't want to override that project's role to the default one
*/
skipProjectRoleUpdatesFor?: string[]
/**
* Only add or upgrade role, prevent downgrades
*/
preventRoleDowngrade?: boolean
}
) => Promise<void>
export type GetWorkspaceRoleToDefaultProjectRoleMapping = (args: {
workspaceId: string
}) => Promise<WorkspaceRoleToDefaultProjectRoleMapping>

Просмотреть файл

@ -2,9 +2,13 @@ import { db } from '@/db/knex'
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
import {
getProjectCollaboratorsFactory,
getProjectFactory,
getStream,
getUserStreams,
getUserStreamsCount,
updateProjectFactory,
upsertProjectRoleFactory,
getRolesByUserIdFactory
} from '@/modules/core/repositories/streams'
import { getUser, getUsers } from '@/modules/core/repositories/users'
@ -91,6 +95,8 @@ import {
} from '@/modules/workspaces/services/management'
import {
getWorkspaceProjectsFactory,
getWorkspaceRoleToDefaultProjectRoleMappingFactory,
moveProjectToWorkspaceFactory,
queryAllWorkspaceProjectsFactory
} from '@/modules/workspaces/services/projects'
import {
@ -653,6 +659,50 @@ export = FF_WORKSPACES_MODULE_ENABLED
context.userId!,
context.resourceAccessRules
)
},
moveToWorkspace: async (_parent, args, context) => {
const { projectId, workspaceId } = args
await authorizeResolver(
context.userId,
projectId,
Roles.Stream.Owner,
context.resourceAccessRules
)
await authorizeResolver(
context.userId,
workspaceId,
Roles.Workspace.Admin,
context.resourceAccessRules
)
const trx = await db.transaction()
const moveProjectToWorkspace = moveProjectToWorkspaceFactory({
getProject: getProjectFactory(),
updateProject: updateProjectFactory({ db: trx }),
upsertProjectRole: upsertProjectRoleFactory({ db: trx }),
getProjectCollaborators: getProjectCollaboratorsFactory(),
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
getWorkspaceRoleToDefaultProjectRoleMapping:
getWorkspaceRoleToDefaultProjectRoleMappingFactory({
getWorkspace: getWorkspaceFactory({ db })
}),
updateWorkspaceRole: updateWorkspaceRoleFactory({
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db: trx }),
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({
db: trx
}),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db: trx }),
emitWorkspaceEvent: getEventBus().emit
})
})
return await withTransaction(
moveProjectToWorkspace({ projectId, workspaceId }),
trx
)
}
},
Workspace: {

Просмотреть файл

@ -9,7 +9,8 @@ import {
UpsertWorkspaceRole,
GetWorkspaceWithDomains,
GetWorkspaceDomains,
UpdateWorkspace
UpdateWorkspace,
UpdateWorkspaceRole
} from '@/modules/workspaces/domain/operations'
import {
Workspace,
@ -312,22 +313,13 @@ export const updateWorkspaceRoleFactory =
findVerifiedEmailsByUserId: FindVerifiedEmailsByUserId
upsertWorkspaceRole: UpsertWorkspaceRole
emitWorkspaceEvent: EmitWorkspaceEvent
}) =>
}): UpdateWorkspaceRole =>
async ({
workspaceId,
userId,
role: nextWorkspaceRole,
skipProjectRoleUpdatesFor,
preventRoleDowngrade
}: Pick<WorkspaceAcl, 'userId' | 'workspaceId' | 'role'> & {
/**
* If this gets triggered from a project role update, we don't want to override that project's role to the default one
*/
skipProjectRoleUpdatesFor?: string[]
/**
* Only add or upgrade role, prevent downgrades
*/
preventRoleDowngrade?: boolean
}): Promise<void> => {
const workspaceRoles = await getWorkspaceRoles({ workspaceId })

Просмотреть файл

@ -3,14 +3,26 @@ import { getStreams as serviceGetStreams } from '@/modules/core/services/streams
import { getUserStreams } from '@/modules/core/repositories/streams'
import {
GetWorkspace,
GetWorkspaceRoles,
GetWorkspaceRoleToDefaultProjectRoleMapping,
QueryAllWorkspaceProjects
QueryAllWorkspaceProjects,
UpdateWorkspaceRole
} from '@/modules/workspaces/domain/operations'
import {
WorkspaceInvalidProjectError,
WorkspaceNotFoundError,
WorkspaceQueryError
} from '@/modules/workspaces/errors/workspace'
import { Roles } from '@speckle/shared'
import {
GetProject,
GetProjectCollaborators,
UpdateProject,
UpsertProjectRole
} from '@/modules/core/domain/projects/operations'
import { chunk } from 'lodash'
import { Roles, StreamRoles } from '@speckle/shared'
import { orderByWeight } from '@/modules/shared/domain/rolesAndScopes/logic'
import coreUserRoles from '@/modules/core/roles'
export const queryAllWorkspaceProjectsFactory = ({
getStreams
@ -82,6 +94,89 @@ export const getWorkspaceProjectsFactory =
}
}
type MoveProjectToWorkspaceArgs = {
projectId: string
workspaceId: string
}
export const moveProjectToWorkspaceFactory =
({
getProject,
updateProject,
upsertProjectRole,
getProjectCollaborators,
getWorkspaceRoles,
getWorkspaceRoleToDefaultProjectRoleMapping,
updateWorkspaceRole
}: {
getProject: GetProject
updateProject: UpdateProject
upsertProjectRole: UpsertProjectRole
getProjectCollaborators: GetProjectCollaborators
getWorkspaceRoles: GetWorkspaceRoles
getWorkspaceRoleToDefaultProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
updateWorkspaceRole: UpdateWorkspaceRole
}) =>
async ({
projectId,
workspaceId
}: MoveProjectToWorkspaceArgs): Promise<StreamRecord> => {
const project = await getProject({ projectId })
if (project.workspaceId?.length) {
// We do not currently support moving projects between workspaces
throw new WorkspaceInvalidProjectError(
'Specified project already belongs to a workspace. Moving between workspaces is not yet supported.'
)
}
// Update roles for current project members
const projectTeam = await getProjectCollaborators({ projectId })
const workspaceTeam = await getWorkspaceRoles({ workspaceId })
const defaultProjectRoleMapping = await getWorkspaceRoleToDefaultProjectRoleMapping(
{ workspaceId }
)
for (const projectMembers of chunk(projectTeam, 5)) {
await Promise.all(
projectMembers.map(
async ({ id: userId, role: serverRole, streamRole: currentProjectRole }) => {
// Update workspace role. Prefer existing workspace role if there is one.
const currentWorkspaceRole = workspaceTeam.find(
(role) => role.userId === userId
)
const nextWorkspaceRole = currentWorkspaceRole ?? {
userId,
workspaceId,
role:
serverRole === Roles.Server.Guest
? Roles.Workspace.Guest
: Roles.Workspace.Member,
createdAt: new Date()
}
await updateWorkspaceRole(nextWorkspaceRole)
// Update project role. Prefer default workspace project role if more permissive.
const defaultProjectRole =
defaultProjectRoleMapping[nextWorkspaceRole.role] ?? Roles.Stream.Reviewer
const nextProjectRole = orderByWeight(
[currentProjectRole, defaultProjectRole],
coreUserRoles
)[0]
await upsertProjectRole({
userId,
projectId,
role: nextProjectRole.name as StreamRoles
})
}
)
)
}
// Assign project to workspace
return await updateProject({ projectUpdate: { id: projectId, workspaceId } })
}
export const getWorkspaceRoleToDefaultProjectRoleMappingFactory =
({
getWorkspace

Просмотреть файл

@ -1,4 +1,5 @@
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { grantStreamPermissions } from '@/modules/core/repositories/streams'
import {
BasicTestWorkspace,
createTestWorkspace
@ -11,7 +12,9 @@ import {
import {
ActiveUserProjectsWorkspaceDocument,
CreateWorkspaceProjectDocument,
GetWorkspaceProjectsDocument
GetWorkspaceProjectsDocument,
GetWorkspaceTeamDocument,
MoveProjectToWorkspaceDocument
} from '@/test/graphql/generated/graphql'
import {
createTestContext,
@ -19,6 +22,7 @@ import {
TestApolloServer
} from '@/test/graphqlHelper'
import { beforeEachContext } from '@/test/hooks'
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
@ -32,14 +36,14 @@ describe('Workspace project GQL CRUD', () => {
name: 'My Test Workspace'
}
const testUser: BasicTestUser = {
const serverAdminUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: 'john-speckle-workspace-project-admin@example.org',
role: Roles.Server.Admin
}
const testNonWorkspaceMemberUser: BasicTestUser = {
const serverMemberUser: BasicTestUser = {
id: '',
name: 'John Nobody',
email: 'john-nobody@example.org',
@ -48,19 +52,19 @@ describe('Workspace project GQL CRUD', () => {
before(async () => {
await beforeEachContext()
await createTestUsers([testUser, testNonWorkspaceMemberUser])
const token = await createAuthTokenForUser(testUser.id, AllScopes)
await createTestUsers([serverAdminUser, serverMemberUser])
const token = await createAuthTokenForUser(serverAdminUser.id, AllScopes)
apollo = await testApolloServer({
context: createTestContext({
auth: true,
userId: testUser.id,
userId: serverAdminUser.id,
token,
role: testUser.role,
role: serverAdminUser.role,
scopes: AllScopes
})
})
await createTestWorkspace(workspace, testUser)
await createTestWorkspace(workspace, serverAdminUser)
const workspaceProjects = [
{ name: 'Workspace Project A', workspaceId: workspace.id },
@ -169,4 +173,97 @@ describe('Workspace project GQL CRUD', () => {
.be.true
})
})
describe('when moving a project to a workspace', () => {
const testProject: BasicTestStream = {
id: '',
ownerId: '',
name: 'Test Project',
isPublic: false
}
const targetWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Target Workspace'
}
before(async () => {
await createTestWorkspace(targetWorkspace, serverAdminUser)
})
beforeEach(async () => {
await createTestStream(testProject, serverAdminUser)
await grantStreamPermissions({
streamId: testProject.id,
userId: serverMemberUser.id,
role: Roles.Stream.Contributor
})
})
it('should move the project to the target workspace', async () => {
const res = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: testProject.id,
workspaceId: targetWorkspace.id
})
const { workspaceId } =
res.data?.workspaceMutations.projects.moveToWorkspace ?? {}
expect(res).to.not.haveGraphQLErrors()
expect(workspaceId).to.equal(targetWorkspace.id)
})
it('should preserve project roles for project members', async () => {
const res = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: testProject.id,
workspaceId: targetWorkspace.id
})
const { team } = res.data?.workspaceMutations.projects.moveToWorkspace ?? {}
const adminProjectRole = team?.find((role) => role.id === serverAdminUser.id)
const memberProjectRole = team?.find((role) => role.id === serverMemberUser.id)
expect(res).to.not.haveGraphQLErrors()
expect(adminProjectRole?.role).to.equal(Roles.Stream.Owner)
expect(memberProjectRole?.role).to.equal(Roles.Stream.Contributor)
})
it('should grant workspace roles to project members that are not already in the target workspace', async () => {
const resA = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: testProject.id,
workspaceId: targetWorkspace.id
})
const resB = await apollo.execute(GetWorkspaceTeamDocument, {
workspaceId: targetWorkspace.id
})
const memberWorkspaceRole = resB.data?.workspace.team.items.find(
(role) => role.id === serverMemberUser.id
)
expect(resA).to.not.haveGraphQLErrors()
expect(resB).to.not.haveGraphQLErrors()
expect(memberWorkspaceRole?.role).to.equal(Roles.Workspace.Member)
})
it('should preserve workspace roles for project members that are already in the target workspace', async () => {
const resA = await apollo.execute(MoveProjectToWorkspaceDocument, {
projectId: testProject.id,
workspaceId: targetWorkspace.id
})
const resB = await apollo.execute(GetWorkspaceTeamDocument, {
workspaceId: targetWorkspace.id
})
const adminWorkspaceRole = resB.data?.workspace.team.items.find(
(role) => role.id === serverAdminUser.id
)
expect(resA).to.not.haveGraphQLErrors()
expect(resB).to.not.haveGraphQLErrors()
expect(adminWorkspaceRole?.role).to.equal(Roles.Workspace.Admin)
})
})
})

Просмотреть файл

@ -1,8 +1,21 @@
import { StreamRecord } from '@/modules/core/helpers/types'
import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
import { ProjectTeamMember } from '@/modules/core/domain/projects/types'
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
import {
moveProjectToWorkspaceFactory,
queryAllWorkspaceProjectsFactory
} from '@/modules/workspaces/services/projects'
import { WorkspaceAcl } from '@/modules/workspacesCore/domain/types'
import { expectToThrow } from '@/test/assertionHelper'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
const getWorkspaceRoleToDefaultProjectRoleMapping = async () => ({
'workspace:admin': Roles.Stream.Owner,
'workspace:guest': null,
'workspace:member': Roles.Stream.Contributor
})
describe('Project retrieval services', () => {
describe('queryAllWorkspaceProjectFactory returns a generator, that', () => {
it('returns all streams for a workspace', async () => {
@ -75,3 +88,309 @@ describe('Project retrieval services', () => {
})
})
})
describe('Project management services', () => {
describe('moveProjectToWorkspaceFactory returns a function, that', () => {
it('should throw if attempting to move a project already in a workspace', async () => {
const moveProjectToWorkspace = moveProjectToWorkspaceFactory({
getProject: async () => {
return {
workspaceId: cryptoRandomString({ length: 6 })
} as StreamRecord
},
updateProject: async () => {
expect.fail()
},
upsertProjectRole: async () => {
expect.fail()
},
getProjectCollaborators: async () => {
expect.fail()
},
getWorkspaceRoles: async () => {
expect.fail()
},
getWorkspaceRoleToDefaultProjectRoleMapping: async () => {
expect.fail()
},
updateWorkspaceRole: async () => {
expect.fail()
}
})
await expectToThrow(() =>
moveProjectToWorkspace({
projectId: cryptoRandomString({ length: 6 }),
workspaceId: cryptoRandomString({ length: 6 })
})
)
})
it('should preserve existing workspace roles in target workspace', async () => {
const userId = cryptoRandomString({ length: 6 })
const projectId = cryptoRandomString({ length: 6 })
const workspaceId = cryptoRandomString({ length: 6 })
const updatedRoles: Partial<WorkspaceAcl>[] = []
const moveProjectToWorkspace = moveProjectToWorkspaceFactory({
getProject: async () => {
return {} as StreamRecord
},
updateProject: async () => {
return {} as StreamRecord
},
upsertProjectRole: async () => {
return {} as StreamRecord
},
getProjectCollaborators: async () => {
return [
{
id: userId,
streamRole: Roles.Stream.Contributor
} as unknown as ProjectTeamMember
]
},
getWorkspaceRoles: async () => {
return [
{
userId,
role: Roles.Workspace.Admin,
workspaceId,
createdAt: new Date()
}
]
},
getWorkspaceRoleToDefaultProjectRoleMapping: async () => ({
'workspace:admin': Roles.Stream.Owner,
'workspace:guest': null,
'workspace:member': Roles.Stream.Contributor
}),
updateWorkspaceRole: async (role) => {
updatedRoles.push(role)
}
})
await moveProjectToWorkspace({ projectId, workspaceId })
expect(updatedRoles.length).to.equal(1)
expect(updatedRoles[0].role).to.equal(Roles.Workspace.Admin)
})
it('should set project members as workspace members in target workspace', async () => {
const userId = cryptoRandomString({ length: 6 })
const projectId = cryptoRandomString({ length: 6 })
const workspaceId = cryptoRandomString({ length: 6 })
const updatedRoles: Partial<WorkspaceAcl>[] = []
const moveProjectToWorkspace = moveProjectToWorkspaceFactory({
getProject: async () => {
return {} as StreamRecord
},
updateProject: async () => {
return {} as StreamRecord
},
upsertProjectRole: async () => {
return {} as StreamRecord
},
getProjectCollaborators: async () => {
return [
{
id: userId,
streamRole: Roles.Stream.Contributor
} as unknown as ProjectTeamMember
]
},
getWorkspaceRoles: async () => {
return []
},
getWorkspaceRoleToDefaultProjectRoleMapping,
updateWorkspaceRole: async (role) => {
updatedRoles.push(role)
}
})
await moveProjectToWorkspace({ projectId, workspaceId })
expect(updatedRoles.length).to.equal(1)
expect(updatedRoles[0].role).to.equal(Roles.Workspace.Member)
})
it('should set project members that are server guests as workspace guests in target workspace', async () => {
const userId = cryptoRandomString({ length: 6 })
const projectId = cryptoRandomString({ length: 6 })
const workspaceId = cryptoRandomString({ length: 6 })
const updatedRoles: Partial<WorkspaceAcl>[] = []
const moveProjectToWorkspace = moveProjectToWorkspaceFactory({
getProject: async () => {
return {} as StreamRecord
},
updateProject: async () => {
return {} as StreamRecord
},
upsertProjectRole: async () => {
return {} as StreamRecord
},
getProjectCollaborators: async () => {
return [
{
id: userId,
role: Roles.Server.Guest,
streamRole: Roles.Stream.Contributor
} as unknown as ProjectTeamMember
]
},
getWorkspaceRoles: async () => {
return []
},
getWorkspaceRoleToDefaultProjectRoleMapping,
updateWorkspaceRole: async (role) => {
updatedRoles.push(role)
}
})
await moveProjectToWorkspace({ projectId, workspaceId })
expect(updatedRoles.length).to.equal(1)
expect(updatedRoles[0].role).to.equal(Roles.Workspace.Guest)
})
it('should preserve project roles for project members', async () => {
const userId = cryptoRandomString({ length: 6 })
const projectId = cryptoRandomString({ length: 6 })
const workspaceId = cryptoRandomString({ length: 6 })
const updatedRoles: Partial<StreamAclRecord>[] = []
const moveProjectToWorkspace = moveProjectToWorkspaceFactory({
getProject: async () => {
return {} as StreamRecord
},
updateProject: async () => {
return {} as StreamRecord
},
upsertProjectRole: async (role) => {
updatedRoles.push(role)
return {} as StreamRecord
},
getProjectCollaborators: async () => {
return [
{
id: userId,
role: Roles.Server.User,
streamRole: Roles.Stream.Owner
} as unknown as ProjectTeamMember
]
},
getWorkspaceRoles: async () => {
return []
},
getWorkspaceRoleToDefaultProjectRoleMapping,
updateWorkspaceRole: async () => {}
})
await moveProjectToWorkspace({ projectId, workspaceId })
expect(updatedRoles.length).to.equal(1)
expect(updatedRoles[0].role).to.equal(Roles.Stream.Owner)
})
it('should guarantee that target workspace members get at least the default workspace project role', async () => {
const userId = cryptoRandomString({ length: 6 })
const projectId = cryptoRandomString({ length: 6 })
const workspaceId = cryptoRandomString({ length: 6 })
const updatedRoles: Partial<StreamAclRecord>[] = []
const moveProjectToWorkspace = moveProjectToWorkspaceFactory({
getProject: async () => {
return {} as StreamRecord
},
updateProject: async () => {
return {} as StreamRecord
},
upsertProjectRole: async (role) => {
updatedRoles.push(role)
return {} as StreamRecord
},
getProjectCollaborators: async () => {
return [
{
id: userId,
role: Roles.Server.User,
streamRole: Roles.Stream.Reviewer
} as unknown as ProjectTeamMember
]
},
getWorkspaceRoles: async () => {
return []
},
getWorkspaceRoleToDefaultProjectRoleMapping: async () => ({
[Roles.Workspace.Guest]: null,
[Roles.Workspace.Member]: Roles.Stream.Contributor,
[Roles.Workspace.Admin]: Roles.Stream.Owner
}),
updateWorkspaceRole: async () => {}
})
await moveProjectToWorkspace({ projectId, workspaceId })
expect(updatedRoles.length).to.equal(1)
expect(updatedRoles[0].role).to.equal(Roles.Stream.Contributor)
})
it('should guarantee that target workspace admins become project owners', async () => {
const userId = cryptoRandomString({ length: 6 })
const projectId = cryptoRandomString({ length: 6 })
const workspaceId = cryptoRandomString({ length: 6 })
const updatedRoles: Partial<StreamAclRecord>[] = []
const moveProjectToWorkspace = moveProjectToWorkspaceFactory({
getProject: async () => {
return {} as StreamRecord
},
updateProject: async () => {
return {} as StreamRecord
},
upsertProjectRole: async (role) => {
updatedRoles.push(role)
return {} as StreamRecord
},
getProjectCollaborators: async () => {
return [
{
id: userId,
role: Roles.Server.User,
streamRole: Roles.Stream.Reviewer
} as unknown as ProjectTeamMember
]
},
getWorkspaceRoles: async () => {
return [
{
userId,
workspaceId,
role: Roles.Workspace.Admin,
createdAt: new Date()
}
]
},
getWorkspaceRoleToDefaultProjectRoleMapping: async () => ({
[Roles.Workspace.Guest]: null,
[Roles.Workspace.Member]: Roles.Stream.Contributor,
[Roles.Workspace.Admin]: Roles.Stream.Owner
}),
updateWorkspaceRole: async () => {}
})
await moveProjectToWorkspace({ projectId, workspaceId })
expect(updatedRoles.length).to.equal(1)
expect(updatedRoles[0].role).to.equal(Roles.Stream.Owner)
})
})
})

Просмотреть файл

@ -4119,10 +4119,17 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
moveToWorkspace: Project;
updateRole: Project;
};
export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
};
export type WorkspaceProjectMutationsUpdateRoleArgs = {
input: ProjectUpdateRoleInput;
};
@ -4883,6 +4890,14 @@ export type ActiveUserProjectsWorkspaceQueryVariables = Exact<{ [key: string]: n
export type ActiveUserProjectsWorkspaceQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', projects: { __typename?: 'ProjectCollection', items: Array<{ __typename?: 'Project', id: string, workspace?: { __typename?: 'Workspace', id: string, name: string } | null }> } } | null };
export type MoveProjectToWorkspaceMutationVariables = Exact<{
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
}>;
export type MoveProjectToWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', projects: { __typename?: 'WorkspaceProjectMutations', moveToWorkspace: { __typename?: 'Project', id: string, workspaceId?: string | null, team: Array<{ __typename?: 'ProjectCollaborator', id: string, role: string }> } } } };
export const BasicWorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode<BasicWorkspaceFragment, unknown>;
export const BasicPendingWorkspaceCollaboratorFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<BasicPendingWorkspaceCollaboratorFragment, unknown>;
export const WorkspaceBillingFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceBilling"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"billing"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionsCount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"current"}},{"kind":"Field","name":{"kind":"Name","value":"max"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cost"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subTotal"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"cost"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}},{"kind":"Field","name":{"kind":"Name","value":"discount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode<WorkspaceBillingFragment, unknown>;
@ -4993,4 +5008,5 @@ export const CreateWorkspaceProjectDocument = {"kind":"Document","definitions":[
export const GetWorkspaceProjectsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceProjects"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceProject"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceProject"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceProjectsQuery, GetWorkspaceProjectsQueryVariables>;
export const GetWorkspaceTeamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceTeam"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceTeamFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"team"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceTeamQuery, GetWorkspaceTeamQueryVariables>;
export const ActiveUserLeaveWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ActiveUserLeaveWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"leave"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]}}]} as unknown as DocumentNode<ActiveUserLeaveWorkspaceMutation, ActiveUserLeaveWorkspaceMutationVariables>;
export const ActiveUserProjectsWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserProjectsWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<ActiveUserProjectsWorkspaceQuery, ActiveUserProjectsWorkspaceQueryVariables>;
export const ActiveUserProjectsWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserProjectsWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<ActiveUserProjectsWorkspaceQuery, ActiveUserProjectsWorkspaceQueryVariables>;
export const MoveProjectToWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MoveProjectToWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"moveToWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}},{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<MoveProjectToWorkspaceMutation, MoveProjectToWorkspaceMutationVariables>;

Просмотреть файл

@ -200,3 +200,20 @@ export const getProjectWorkspaceQuery = gql`
}
}
`
export const moveProjectToWorkspaceMutation = gql`
mutation MoveProjectToWorkspace($projectId: String!, $workspaceId: String!) {
workspaceMutations {
projects {
moveToWorkspace(projectId: $projectId, workspaceId: $workspaceId) {
id
workspaceId
team {
id
role
}
}
}
}
}
`