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) : {}