7bb40134eb
* chore(workspaces): add some tests for new filters * fix(workspaces): args style
569 lines
19 KiB
TypeScript
569 lines
19 KiB
TypeScript
import { db } from '@/db/knex'
|
|
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
|
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
|
import {
|
|
getStream,
|
|
grantStreamPermissions,
|
|
revokeStreamPermissions
|
|
} from '@/modules/core/repositories/streams'
|
|
import { getUser, getUsers } from '@/modules/core/repositories/users'
|
|
import { getStreams } from '@/modules/core/services/streams'
|
|
import { InviteCreateValidationError } from '@/modules/serverinvites/errors'
|
|
import {
|
|
deleteAllResourceInvitesFactory,
|
|
deleteInviteFactory,
|
|
deleteInvitesByTargetFactory,
|
|
findInviteFactory,
|
|
findUserByTargetFactory,
|
|
insertInviteAndDeleteOldFactory,
|
|
queryAllResourceInvitesFactory,
|
|
queryAllUserResourceInvitesFactory
|
|
} from '@/modules/serverinvites/repositories/serverInvites'
|
|
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
|
|
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
|
|
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
|
|
import {
|
|
cancelResourceInviteFactory,
|
|
finalizeResourceInviteFactory
|
|
} from '@/modules/serverinvites/services/processing'
|
|
import { createProjectInviteFactory } from '@/modules/serverinvites/services/projectInviteManagement'
|
|
import { getInvitationTargetUsersFactory } from '@/modules/serverinvites/services/retrieval'
|
|
import { authorizeResolver } from '@/modules/shared'
|
|
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
|
import { getEventBus } from '@/modules/shared/services/eventBus'
|
|
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
|
|
import {
|
|
WorkspaceInvalidRoleError,
|
|
WorkspaceNotFoundError,
|
|
WorkspacesNotAuthorizedError,
|
|
WorkspacesNotYetImplementedError
|
|
} from '@/modules/workspaces/errors/workspace'
|
|
import { isWorkspaceRole } from '@/modules/workspaces/helpers/roles'
|
|
import {
|
|
deleteWorkspaceFactory as repoDeleteWorkspaceFactory,
|
|
deleteWorkspaceRoleFactory as repoDeleteWorkspaceRoleFactory,
|
|
getWorkspaceCollaboratorsFactory,
|
|
getWorkspaceFactory,
|
|
getWorkspaceRolesFactory,
|
|
getWorkspaceRolesForUserFactory,
|
|
upsertWorkspaceFactory,
|
|
upsertWorkspaceRoleFactory,
|
|
workspaceInviteValidityFilter
|
|
} from '@/modules/workspaces/repositories/workspaces'
|
|
import {
|
|
buildWorkspaceInviteEmailContentsFactory,
|
|
collectAndValidateWorkspaceTargetsFactory,
|
|
createWorkspaceInviteFactory,
|
|
getPendingWorkspaceCollaboratorsFactory,
|
|
getUserPendingWorkspaceInviteFactory,
|
|
getUserPendingWorkspaceInvitesFactory,
|
|
processFinalizedWorkspaceInviteFactory,
|
|
validateWorkspaceInviteBeforeFinalizationFactory
|
|
} from '@/modules/workspaces/services/invites'
|
|
import {
|
|
createWorkspaceFactory,
|
|
deleteWorkspaceFactory,
|
|
deleteWorkspaceRoleFactory,
|
|
updateWorkspaceFactory,
|
|
updateWorkspaceRoleFactory
|
|
} from '@/modules/workspaces/services/management'
|
|
import {
|
|
getWorkspaceProjectsFactory,
|
|
queryAllWorkspaceProjectsFactory
|
|
} from '@/modules/workspaces/services/projects'
|
|
import { getWorkspacesForUserFactory } from '@/modules/workspaces/services/retrieval'
|
|
import { Roles, WorkspaceRoles, removeNullOrUndefinedKeys } from '@speckle/shared'
|
|
import { chunk } from 'lodash'
|
|
import { deleteStream } from '@/modules/core/repositories/streams'
|
|
|
|
const buildCreateAndSendServerOrProjectInvite = () =>
|
|
createAndSendInviteFactory({
|
|
findUserByTarget: findUserByTargetFactory(),
|
|
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
|
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
|
|
getStream
|
|
}),
|
|
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
|
|
getStream
|
|
}),
|
|
emitEvent: ({ eventName, payload }) =>
|
|
getEventBus().emit({
|
|
eventName,
|
|
payload
|
|
})
|
|
})
|
|
|
|
const buildCreateAndSendWorkspaceInvite = () =>
|
|
createAndSendInviteFactory({
|
|
findUserByTarget: findUserByTargetFactory(),
|
|
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
|
collectAndValidateResourceTargets: collectAndValidateWorkspaceTargetsFactory({
|
|
getStream,
|
|
getWorkspace: getWorkspaceFactory({ db })
|
|
}),
|
|
buildInviteEmailContents: buildWorkspaceInviteEmailContentsFactory({
|
|
getStream,
|
|
getWorkspace: getWorkspaceFactory({ db })
|
|
}),
|
|
emitEvent: ({ eventName, payload }) =>
|
|
getEventBus().emit({
|
|
eventName,
|
|
payload
|
|
})
|
|
})
|
|
|
|
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
|
|
|
export = FF_WORKSPACES_MODULE_ENABLED
|
|
? ({
|
|
Query: {
|
|
workspace: async (_parent, args, ctx) => {
|
|
const workspace = await ctx.loaders.workspaces!.getWorkspace.load(args.id)
|
|
if (!workspace) {
|
|
throw new WorkspaceNotFoundError()
|
|
}
|
|
|
|
await authorizeResolver(
|
|
ctx.userId,
|
|
args.id,
|
|
Roles.Workspace.Guest,
|
|
ctx.resourceAccessRules
|
|
)
|
|
|
|
return workspace
|
|
},
|
|
workspaceInvite: async (_parent, args, ctx) => {
|
|
const getPendingInvite = getUserPendingWorkspaceInviteFactory({
|
|
findInvite: findInviteFactory({
|
|
db,
|
|
filterQuery: workspaceInviteValidityFilter
|
|
}),
|
|
getUser
|
|
})
|
|
|
|
return await getPendingInvite({
|
|
userId: ctx.userId!,
|
|
token: args.token,
|
|
workspaceId: args.workspaceId
|
|
})
|
|
}
|
|
},
|
|
Mutation: {
|
|
workspaceMutations: () => ({})
|
|
},
|
|
ProjectInviteMutations: {
|
|
async createForWorkspace(_parent, args, ctx) {
|
|
await authorizeResolver(
|
|
ctx.userId,
|
|
args.projectId,
|
|
Roles.Stream.Owner,
|
|
ctx.resourceAccessRules
|
|
)
|
|
|
|
const inviteCount = args.inputs.length
|
|
if (inviteCount > 10 && ctx.role !== Roles.Server.Admin) {
|
|
throw new InviteCreateValidationError(
|
|
'Maximum 10 invites can be sent at once by non admins'
|
|
)
|
|
}
|
|
|
|
const createProjectInvite = createProjectInviteFactory({
|
|
createAndSendInvite: buildCreateAndSendServerOrProjectInvite()
|
|
})
|
|
|
|
const inputBatches = chunk(args.inputs, 10)
|
|
for (const batch of inputBatches) {
|
|
await Promise.all(
|
|
batch.map((i) => {
|
|
const workspaceRole = i.workspaceRole
|
|
if (
|
|
workspaceRole &&
|
|
!(Object.values(Roles.Workspace) as string[]).includes(workspaceRole)
|
|
) {
|
|
throw new InviteCreateValidationError(
|
|
'Invalid workspace role specified: ' + workspaceRole
|
|
)
|
|
}
|
|
|
|
return createProjectInvite({
|
|
input: {
|
|
...i,
|
|
projectId: args.projectId
|
|
},
|
|
inviterId: ctx.userId!,
|
|
inviterResourceAccessRules: ctx.resourceAccessRules,
|
|
secondaryResourceRoles: workspaceRole
|
|
? {
|
|
[WorkspaceInviteResourceType]: workspaceRole as WorkspaceRoles
|
|
}
|
|
: undefined
|
|
})
|
|
})
|
|
)
|
|
}
|
|
return ctx.loaders.streams.getStream.load(args.projectId)
|
|
}
|
|
},
|
|
WorkspaceMutations: {
|
|
create: async (_parent, args, context) => {
|
|
const { name, description } = args.input
|
|
|
|
const createWorkspace = createWorkspaceFactory({
|
|
upsertWorkspace: upsertWorkspaceFactory({ db }),
|
|
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
|
|
emitWorkspaceEvent: getEventBus().emit
|
|
})
|
|
|
|
const workspace = await createWorkspace({
|
|
userId: context.userId!,
|
|
workspaceInput: {
|
|
name,
|
|
description: description || null,
|
|
logo: null
|
|
},
|
|
userResourceAccessLimits: context.resourceAccessRules
|
|
})
|
|
|
|
return workspace
|
|
},
|
|
delete: async (_parent, args, context) => {
|
|
const { workspaceId } = args
|
|
|
|
await authorizeResolver(
|
|
context.userId!,
|
|
workspaceId,
|
|
Roles.Workspace.Admin,
|
|
context.resourceAccessRules
|
|
)
|
|
|
|
// Delete workspace and associated resources (i.e. invites)
|
|
const deleteWorkspace = deleteWorkspaceFactory({
|
|
deleteWorkspace: repoDeleteWorkspaceFactory({ db }),
|
|
deleteProject: deleteStream,
|
|
deleteAllResourceInvites: deleteAllResourceInvitesFactory({ db }),
|
|
queryAllWorkspaceProjects: queryAllWorkspaceProjectsFactory({ getStreams })
|
|
})
|
|
|
|
await deleteWorkspace({ workspaceId })
|
|
|
|
return true
|
|
},
|
|
update: async (_parent, args, context) => {
|
|
const { id: workspaceId, ...workspaceInput } = args.input
|
|
|
|
await authorizeResolver(
|
|
context.userId!,
|
|
workspaceId,
|
|
Roles.Workspace.Admin,
|
|
context.resourceAccessRules
|
|
)
|
|
|
|
const updateWorkspace = updateWorkspaceFactory({
|
|
getWorkspace: getWorkspaceFactory({ db }),
|
|
upsertWorkspace: upsertWorkspaceFactory({ db }),
|
|
emitWorkspaceEvent: getEventBus().emit
|
|
})
|
|
|
|
const workspace = await updateWorkspace({
|
|
workspaceId,
|
|
workspaceInput
|
|
})
|
|
|
|
return workspace
|
|
},
|
|
updateRole: async (_parent, args, context) => {
|
|
const { userId, workspaceId, role } = args.input
|
|
|
|
authorizeResolver(
|
|
context.userId,
|
|
workspaceId,
|
|
Roles.Workspace.Admin,
|
|
context.resourceAccessRules
|
|
)
|
|
|
|
const getWorkspaceRoles = getWorkspaceRolesFactory({ db })
|
|
const emitWorkspaceEvent = getEventBus().emit
|
|
|
|
if (!role) {
|
|
const deleteWorkspaceRole = deleteWorkspaceRoleFactory({
|
|
deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db }),
|
|
getWorkspaceRoles,
|
|
emitWorkspaceEvent,
|
|
getStreams,
|
|
revokeStreamPermissions
|
|
})
|
|
|
|
await deleteWorkspaceRole(args.input)
|
|
} else {
|
|
if (!isWorkspaceRole(role)) {
|
|
throw new WorkspaceInvalidRoleError()
|
|
}
|
|
|
|
const updateWorkspaceRole = updateWorkspaceRoleFactory({
|
|
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
|
|
getWorkspaceRoles,
|
|
emitWorkspaceEvent,
|
|
getStreams,
|
|
grantStreamPermissions
|
|
})
|
|
|
|
await updateWorkspaceRole({ userId, workspaceId, role })
|
|
}
|
|
|
|
return await getWorkspaceFactory({ db })({ workspaceId })
|
|
},
|
|
invites: () => ({})
|
|
},
|
|
WorkspaceInviteMutations: {
|
|
create: async (_parent, args, ctx) => {
|
|
const createInvite = createWorkspaceInviteFactory({
|
|
createAndSendInvite: buildCreateAndSendWorkspaceInvite()
|
|
})
|
|
await createInvite({
|
|
workspaceId: args.workspaceId,
|
|
input: args.input,
|
|
inviterId: ctx.userId!,
|
|
inviterResourceAccessRules: ctx.resourceAccessRules
|
|
})
|
|
|
|
return ctx.loaders.workspaces!.getWorkspace.load(args.workspaceId)
|
|
},
|
|
batchCreate: async (_parent, args, ctx) => {
|
|
const inviteCount = args.input.length
|
|
if (inviteCount > 10 && ctx.role !== Roles.Server.Admin) {
|
|
throw new InviteCreateValidationError(
|
|
'Maximum 10 invites can be sent at once by non admins'
|
|
)
|
|
}
|
|
|
|
const createInvite = createWorkspaceInviteFactory({
|
|
createAndSendInvite: buildCreateAndSendWorkspaceInvite()
|
|
})
|
|
|
|
const inputBatches = chunk(args.input, 10)
|
|
for (const batch of inputBatches) {
|
|
await Promise.all(
|
|
batch.map((i) =>
|
|
createInvite({
|
|
workspaceId: args.workspaceId,
|
|
input: i,
|
|
inviterId: ctx.userId!,
|
|
inviterResourceAccessRules: ctx.resourceAccessRules
|
|
})
|
|
)
|
|
)
|
|
}
|
|
|
|
return ctx.loaders.workspaces!.getWorkspace.load(args.workspaceId)
|
|
},
|
|
use: async (_parent, args, ctx) => {
|
|
const finalizeInvite = finalizeResourceInviteFactory({
|
|
findInvite: findInviteFactory({ db }),
|
|
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
|
|
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
|
emitEvent: ({ eventName, payload }) =>
|
|
getEventBus().emit({
|
|
eventName,
|
|
payload
|
|
}),
|
|
validateInvite: validateWorkspaceInviteBeforeFinalizationFactory({
|
|
getWorkspace: getWorkspaceFactory({ db })
|
|
}),
|
|
processInvite: processFinalizedWorkspaceInviteFactory({
|
|
getWorkspace: getWorkspaceFactory({ db }),
|
|
updateWorkspaceRole: updateWorkspaceRoleFactory({
|
|
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
|
|
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
|
|
emitWorkspaceEvent: ({ eventName, payload }) =>
|
|
getEventBus().emit({
|
|
eventName,
|
|
payload
|
|
}),
|
|
getStreams,
|
|
grantStreamPermissions
|
|
})
|
|
})
|
|
})
|
|
|
|
await finalizeInvite({
|
|
finalizerUserId: ctx.userId!,
|
|
finalizerResourceAccessLimits: ctx.resourceAccessRules,
|
|
token: args.input.token,
|
|
accept: args.input.accept,
|
|
resourceType: WorkspaceInviteResourceType
|
|
})
|
|
|
|
return true
|
|
},
|
|
cancel: async (_parent, args, ctx) => {
|
|
await authorizeResolver(
|
|
ctx.userId,
|
|
args.workspaceId,
|
|
Roles.Workspace.Admin,
|
|
ctx.resourceAccessRules
|
|
)
|
|
|
|
const cancelInvite = cancelResourceInviteFactory({
|
|
findInvite: findInviteFactory({ db }),
|
|
deleteInvite: deleteInviteFactory({ db }),
|
|
validateResourceAccess: validateWorkspaceInviteBeforeFinalizationFactory({
|
|
getWorkspace: getWorkspaceFactory({ db })
|
|
})
|
|
})
|
|
|
|
await cancelInvite({
|
|
resourceId: args.workspaceId,
|
|
inviteId: args.inviteId,
|
|
cancelerId: ctx.userId!,
|
|
resourceType: WorkspaceInviteResourceType,
|
|
cancelerResourceAccessLimits: ctx.resourceAccessRules
|
|
})
|
|
return ctx.loaders.workspaces!.getWorkspace.load(args.workspaceId)
|
|
}
|
|
},
|
|
Workspace: {
|
|
role: async (parent, _args, ctx) => {
|
|
const workspace = await ctx.loaders.workspaces!.getWorkspace.load(parent.id)
|
|
return workspace?.role || null
|
|
},
|
|
team: async (parent, args) => {
|
|
const getTeam = getWorkspaceCollaboratorsFactory({ db })
|
|
const collaborators = await getTeam({
|
|
workspaceId: parent.id,
|
|
filter: removeNullOrUndefinedKeys(args?.filter ?? {})
|
|
})
|
|
|
|
return collaborators
|
|
},
|
|
invitedTeam: async (parent) => {
|
|
const getPendingTeam = getPendingWorkspaceCollaboratorsFactory({
|
|
queryAllResourceInvites: queryAllResourceInvitesFactory({
|
|
db,
|
|
filterQuery: workspaceInviteValidityFilter
|
|
}),
|
|
getInvitationTargetUsers: getInvitationTargetUsersFactory({ getUsers })
|
|
})
|
|
|
|
return await getPendingTeam({ workspaceId: parent.id })
|
|
},
|
|
projects: async (parent, args) => {
|
|
const getWorkspaceProjects = getWorkspaceProjectsFactory({ getStreams })
|
|
return await getWorkspaceProjects(
|
|
{
|
|
workspaceId: parent.id
|
|
},
|
|
{
|
|
limit: args.limit || 25,
|
|
cursor: args.cursor || null,
|
|
filter: { ...(args.filter || {}) }
|
|
}
|
|
)
|
|
}
|
|
},
|
|
WorkspaceCollaborator: {
|
|
user: async (parent) => {
|
|
return parent
|
|
},
|
|
role: async (parent) => {
|
|
return parent.workspaceRole
|
|
}
|
|
},
|
|
PendingWorkspaceCollaborator: {
|
|
workspaceName: async (parent, _args, ctx) => {
|
|
const workspace = await ctx.loaders.workspaces!.getWorkspace.load(
|
|
parent.workspaceId
|
|
)
|
|
return workspace!.name
|
|
},
|
|
invitedBy: async (parent, _args, ctx) => {
|
|
const { invitedById } = parent
|
|
if (!invitedById) return null
|
|
|
|
const user = await ctx.loaders.users.getUser.load(invitedById)
|
|
return user ? removePrivateFields(user) : null
|
|
},
|
|
token: async (parent, _args, ctx) => {
|
|
const authedUserId = ctx.userId
|
|
const targetUserId = parent.user?.id
|
|
const inviteId = parent.inviteId
|
|
|
|
// Only returning it for the user that is the pending stream collaborator
|
|
if (!authedUserId || !targetUserId || authedUserId !== targetUserId) {
|
|
return null
|
|
}
|
|
|
|
const invite = await ctx.loaders.invites.getInvite.load(inviteId)
|
|
return invite?.token || null
|
|
}
|
|
},
|
|
User: {
|
|
workspaces: async (_parent, _args, context) => {
|
|
if (!context.userId) {
|
|
throw new WorkspacesNotAuthorizedError()
|
|
}
|
|
|
|
const getWorkspace = getWorkspaceFactory({ db })
|
|
const getWorkspaceRolesForUser = getWorkspaceRolesForUserFactory({ db })
|
|
|
|
const getWorkspacesForUser = getWorkspacesForUserFactory({
|
|
getWorkspace,
|
|
getWorkspaceRolesForUser
|
|
})
|
|
|
|
const workspaces = await getWorkspacesForUser({ userId: context.userId })
|
|
|
|
// TODO: Pagination
|
|
return {
|
|
items: workspaces,
|
|
totalCount: workspaces.length
|
|
}
|
|
},
|
|
workspaceInvites: async (parent) => {
|
|
const getInvites = getUserPendingWorkspaceInvitesFactory({
|
|
getUser,
|
|
getUserResourceInvites: queryAllUserResourceInvitesFactory({
|
|
db,
|
|
filterQuery: workspaceInviteValidityFilter
|
|
})
|
|
})
|
|
|
|
return await getInvites(parent.id)
|
|
}
|
|
},
|
|
Project: {
|
|
workspace: async () => {
|
|
// Get workspaceId from project, get and return workspace data
|
|
throw new WorkspacesNotYetImplementedError()
|
|
}
|
|
},
|
|
AdminQueries: {
|
|
workspaceList: async () => {
|
|
throw new WorkspacesNotYetImplementedError()
|
|
}
|
|
},
|
|
ActiveUserMutations: {
|
|
workspaceMutations: () => ({})
|
|
},
|
|
UserWorkspaceMutations: {
|
|
leave: async (parent, args, ctx) => {
|
|
const userId = ctx.userId!
|
|
|
|
const getWorkspaceRoles = getWorkspaceRolesFactory({ db })
|
|
const emitWorkspaceEvent = getEventBus().emit
|
|
|
|
const deleteWorkspaceRole = deleteWorkspaceRoleFactory({
|
|
deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db }),
|
|
getWorkspaceRoles,
|
|
emitWorkspaceEvent,
|
|
getStreams,
|
|
revokeStreamPermissions
|
|
})
|
|
|
|
await deleteWorkspaceRole({ workspaceId: args.id, userId })
|
|
|
|
return true
|
|
}
|
|
}
|
|
} as Resolvers)
|
|
: {}
|