import { db } from '@/db/knex' import { Resolvers } from '@/modules/core/graph/generated/graphql' import { removePrivateFields } from '@/modules/core/helpers/userHelper' import { getProjectCollaboratorsFactory, getProjectFactory, updateProjectFactory, upsertProjectRoleFactory, getRolesByUserIdFactory, getStreamFactory, deleteStreamFactory, revokeStreamPermissionsFactory, grantStreamPermissionsFactory, legacyGetStreamsFactory, getUserStreamsPageFactory, getUserStreamsCountFactory } from '@/modules/core/repositories/streams' import { InviteCreateValidationError } from '@/modules/serverinvites/errors' import { deleteAllResourceInvitesFactory, deleteInviteFactory, deleteInvitesByTargetFactory, deleteServerOnlyInvitesFactory, findInviteFactory, findUserByTargetFactory, insertInviteAndDeleteOldFactory, markInviteUpdatedfactory, queryAllResourceInvitesFactory, queryAllUserResourceInvitesFactory, updateAllInviteTargetsFactory } from '@/modules/serverinvites/repositories/serverInvites' import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents' import { createAndSendInviteFactory, resendInviteEmailFactory } from '@/modules/serverinvites/services/creation' import { cancelResourceInviteFactory, finalizeInvitedServerRegistrationFactory, 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 { withTransaction } from '@/modules/shared/helpers/dbHelper' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { getEventBus } from '@/modules/shared/services/eventBus' import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants' import { WorkspaceInvalidRoleError, WorkspaceJoinNotAllowedError, 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, getWorkspaceCollaboratorsTotalCountFactory, deleteWorkspaceDomainFactory as repoDeleteWorkspaceDomainFactory, storeWorkspaceDomainFactory, getWorkspaceDomainsFactory, getUserDiscoverableWorkspacesFactory, getWorkspaceWithDomainsFactory, countProjectsVersionsByWorkspaceIdFactory, countWorkspaceRoleWithOptionalProjectRoleFactory, getUserIdsWithRoleInWorkspaceFactory, getWorkspaceRoleForUserFactory, getWorkspaceBySlugFactory } from '@/modules/workspaces/repositories/workspaces' import { buildWorkspaceInviteEmailContentsFactory, collectAndValidateWorkspaceTargetsFactory, createWorkspaceInviteFactory, getPendingWorkspaceCollaboratorsFactory, getUserPendingWorkspaceInviteFactory, getUserPendingWorkspaceInvitesFactory, processFinalizedWorkspaceInviteFactory, validateWorkspaceInviteBeforeFinalizationFactory } from '@/modules/workspaces/services/invites' import { addDomainToWorkspaceFactory, createWorkspaceFactory, deleteWorkspaceFactory, deleteWorkspaceRoleFactory, generateValidSlugFactory, updateWorkspaceFactory, updateWorkspaceRoleFactory, validateSlugFactory } from '@/modules/workspaces/services/management' import { getWorkspaceProjectsFactory, getWorkspaceRoleToDefaultProjectRoleMappingFactory, moveProjectToWorkspaceFactory, queryAllWorkspaceProjectsFactory, updateWorkspaceProjectRoleFactory } from '@/modules/workspaces/services/projects' import { getDiscoverableWorkspacesForUserFactory, getPaginatedWorkspaceTeamFactory, getWorkspacesForUserFactory } from '@/modules/workspaces/services/retrieval' import { Roles, WorkspaceRoles, removeNullOrUndefinedKeys } from '@speckle/shared' import { chunk } from 'lodash' import { findEmailsByUserIdFactory, findVerifiedEmailsByUserIdFactory, createUserEmailFactory, ensureNoPrimaryEmailForUserFactory, findEmailFactory } from '@/modules/core/repositories/userEmails' import { joinWorkspaceFactory } from '@/modules/workspaces/services/join' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' import { WORKSPACE_MAX_PROJECTS_VERSIONS } from '@/modules/gatekeeper/domain/constants' import { getWorkspaceCostFactory, getWorkspaceCostItemsFactory } from '@/modules/workspaces/services/cost' import { deleteWorkspaceDomainFactory, isUserWorkspaceDomainPolicyCompliantFactory } from '@/modules/workspaces/services/domains' import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { parseDefaultProjectRole } from '@/modules/workspaces/domain/logic' import { saveActivityFactory } from '@/modules/activitystream/repositories' import { addOrUpdateStreamCollaboratorFactory, isStreamCollaboratorFactory, removeStreamCollaboratorFactory, validateStreamAccessFactory } from '@/modules/core/services/streams/access' import { addStreamInviteAcceptedActivityFactory, addStreamPermissionsAddedActivityFactory, addStreamPermissionsRevokedActivityFactory } from '@/modules/activitystream/services/streamActivity' import { publish } from '@/modules/shared/utils/subscriptions' import { updateStreamRoleAndNotifyFactory } from '@/modules/core/services/streams/management' import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users' import { getServerInfoFactory } from '@/modules/core/repositories/server' const getServerInfo = getServerInfoFactory({ db }) const getUser = getUserFactory({ db }) const getUsers = getUsersFactory({ db }) const getStream = getStreamFactory({ db }) const requestNewEmailVerification = requestNewEmailVerificationFactory({ findEmail: findEmailFactory({ db }), getUser, getServerInfo, deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }), renderEmail, sendEmail }) const buildCollectAndValidateResourceTargets = () => collectAndValidateWorkspaceTargetsFactory({ getStream, getWorkspace: getWorkspaceFactory({ db }), getWorkspaceDomains: getWorkspaceDomainsFactory({ db }), findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }) }) const buildCreateAndSendServerOrProjectInvite = () => createAndSendInviteFactory({ findUserByTarget: findUserByTargetFactory({ db }), insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), buildInviteEmailContents: buildCoreInviteEmailContentsFactory({ getStream }), emitEvent: ({ eventName, payload }) => getEventBus().emit({ eventName, payload }), getUser, getServerInfo }) const buildCreateAndSendWorkspaceInvite = () => createAndSendInviteFactory({ findUserByTarget: findUserByTargetFactory({ db }), insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }), collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), buildInviteEmailContents: buildWorkspaceInviteEmailContentsFactory({ getStream, getWorkspace: getWorkspaceFactory({ db }) }), emitEvent: ({ eventName, payload }) => getEventBus().emit({ eventName, payload }), getUser, getServerInfo }) const deleteStream = deleteStreamFactory({ db }) const saveActivity = saveActivityFactory({ db }) const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver }) const isStreamCollaborator = isStreamCollaboratorFactory({ getStream }) const removeStreamCollaborator = removeStreamCollaboratorFactory({ validateStreamAccess, isStreamCollaborator, revokeStreamPermissions: revokeStreamPermissionsFactory({ db }), addStreamPermissionsRevokedActivity: addStreamPermissionsRevokedActivityFactory({ saveActivity, publish }) }) const updateStreamRoleAndNotify = updateStreamRoleAndNotifyFactory({ isStreamCollaborator, addOrUpdateStreamCollaborator: addOrUpdateStreamCollaboratorFactory({ validateStreamAccess, getUser, grantStreamPermissions: grantStreamPermissionsFactory({ db }), addStreamInviteAcceptedActivity: addStreamInviteAcceptedActivityFactory({ saveActivity, publish }), addStreamPermissionsAddedActivity: addStreamPermissionsAddedActivityFactory({ saveActivity, publish }) }), removeStreamCollaborator }) const getUserStreams = getUserStreamsPageFactory({ db }) const getUserStreamsCount = getUserStreamsCountFactory({ db }) 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 }, workspaceBySlug: async (_parent, args, ctx) => { const workspace = await getWorkspaceBySlugFactory({ db })({ workspaceSlug: args.slug }) if (!workspace) { throw new WorkspaceNotFoundError() } await authorizeResolver( ctx.userId, workspace.id, Roles.Workspace.Guest, ctx.resourceAccessRules ) return workspace }, workspaceInvite: async (_parent, args, ctx) => { const getPendingInvite = getUserPendingWorkspaceInviteFactory({ findInvite: findInviteFactory({ db, filterQuery: workspaceInviteValidityFilter }), getUser, getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) }) const useSlug = !!args.options?.useSlug return await getPendingInvite({ userId: ctx.userId!, token: args.token, ...(useSlug ? { workspaceSlug: args.workspaceId } : { workspaceId: args.workspaceId }) }) }, validateWorkspaceSlug: async (_parent, args) => { const validateSlug = validateSlugFactory({ getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) }) await validateSlug({ slug: args.slug }) return true } }, 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(), getStream }) 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, allowWorkspacedProjects: true }) }) ) } return ctx.loaders.streams.getStream.load(args.projectId) } }, WorkspaceMutations: { create: async (_parent, args, context) => { const { name, description, defaultLogoIndex, logo, slug } = args.input const createWorkspace = createWorkspaceFactory({ validateSlug: validateSlugFactory({ getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) }), generateValidSlug: generateValidSlugFactory({ getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) }), upsertWorkspace: upsertWorkspaceFactory({ db }), upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), emitWorkspaceEvent: getEventBus().emit }) const workspace = await createWorkspace({ userId: context.userId!, workspaceInput: { name, slug, description: description ?? null, logo: logo ?? null, defaultLogoIndex: defaultLogoIndex ?? 0 }, 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 getStreams = legacyGetStreamsFactory({ db }) 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({ validateSlug: validateSlugFactory({ getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) }), getWorkspace: getWorkspaceWithDomainsFactory({ db }), upsertWorkspace: upsertWorkspaceFactory({ db }), emitWorkspaceEvent: getEventBus().emit }) const workspace = await updateWorkspace({ workspaceId, workspaceInput: { ...workspaceInput, defaultProjectRole: parseDefaultProjectRole(args.input.defaultProjectRole) } }) return workspace }, updateRole: async (_parent, args, context) => { const { userId, workspaceId, role } = args.input await authorizeResolver( context.userId, workspaceId, Roles.Workspace.Admin, context.resourceAccessRules ) if (!role) { const trx = await db.transaction() const deleteWorkspaceRole = deleteWorkspaceRoleFactory({ deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db: trx }), getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }), emitWorkspaceEvent: getEventBus().emit }) await withTransaction(deleteWorkspaceRole(args.input), trx) } else { if (!isWorkspaceRole(role)) { throw new WorkspaceInvalidRoleError() } const trx = await db.transaction() const updateWorkspaceRole = updateWorkspaceRoleFactory({ upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db: trx }), getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db: trx }), findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db: trx }), getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }), emitWorkspaceEvent: getEventBus().emit }) await withTransaction( updateWorkspaceRole({ userId, workspaceId, role }), trx ) } return await getWorkspaceFactory({ db })({ workspaceId }) }, addDomain: async (_parent, args, context) => { await authorizeResolver( context.userId!, args.input.workspaceId, Roles.Workspace.Admin, context.resourceAccessRules ) await addDomainToWorkspaceFactory({ getWorkspace: getWorkspaceFactory({ db }), findEmailsByUserId: findEmailsByUserIdFactory({ db }), storeWorkspaceDomain: storeWorkspaceDomainFactory({ db }), upsertWorkspace: upsertWorkspaceFactory({ db }), getDomains: getWorkspaceDomainsFactory({ db }), emitWorkspaceEvent: getEventBus().emit })({ workspaceId: args.input.workspaceId, userId: context.userId!, domain: args.input.domain }) return await getWorkspaceFactory({ db })({ workspaceId: args.input.workspaceId, userId: context.userId }) }, async deleteDomain(_parent, args, context) { await authorizeResolver( context.userId!, args.input.workspaceId, Roles.Workspace.Admin, context.resourceAccessRules ) await deleteWorkspaceDomainFactory({ deleteWorkspaceDomain: repoDeleteWorkspaceDomainFactory({ db }), countDomainsByWorkspaceId: countProjectsVersionsByWorkspaceIdFactory({ db }), updateWorkspace: updateWorkspaceFactory({ validateSlug: validateSlugFactory({ getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) }), getWorkspace: getWorkspaceWithDomainsFactory({ db }), upsertWorkspace: upsertWorkspaceFactory({ db }), emitWorkspaceEvent: getEventBus().emit }) })({ workspaceId: args.input.workspaceId, domainId: args.input.id }) return await getWorkspaceFactory({ db })({ workspaceId: args.input.workspaceId, userId: context.userId }) }, async join(_parent, args, context) { if (!context.userId) throw new WorkspaceJoinNotAllowedError() await joinWorkspaceFactory({ getUserEmails: findEmailsByUserIdFactory({ db }), getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), emitWorkspaceEvent: getEventBus().emit })({ userId: context.userId, workspaceId: args.input.workspaceId }) return await getWorkspaceFactory({ db })({ workspaceId: args.input.workspaceId, userId: context.userId }) }, leave: async (_parent, args, context) => { const trx = await db.transaction() const deleteWorkspaceRole = deleteWorkspaceRoleFactory({ deleteWorkspaceRole: repoDeleteWorkspaceRoleFactory({ db: trx }), getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }), emitWorkspaceEvent: getEventBus().emit }) await withTransaction( deleteWorkspaceRole({ workspaceId: args.id, userId: context.userId! }), trx ) return true }, invites: () => ({}), projects: () => ({}) }, WorkspaceInviteMutations: { resend: async (_parent, args, ctx) => { const { input: { inviteId, workspaceId } } = args await authorizeResolver( ctx.userId!, workspaceId, Roles.Workspace.Admin, ctx.resourceAccessRules ) const resendInviteEmail = resendInviteEmailFactory({ buildInviteEmailContents: buildWorkspaceInviteEmailContentsFactory({ getStream, getWorkspace: getWorkspaceFactory({ db }) }), findUserByTarget: findUserByTargetFactory({ db }), findInvite: findInviteFactory({ db, filterQuery: workspaceInviteValidityFilter }), markInviteUpdated: markInviteUpdatedfactory({ db }), getUser, getServerInfo }) await resendInviteEmail({ inviteId, resourceFilter: { resourceType: WorkspaceInviteResourceType, resourceId: workspaceId } }) return true }, 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, filterQuery: workspaceInviteValidityFilter }), 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({ getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }), getWorkspaceRoles: getWorkspaceRolesFactory({ db }), upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), emitWorkspaceEvent: getEventBus().emit }) }), findEmail: findEmailFactory({ db }), validateAndCreateUserEmail: validateAndCreateUserEmailFactory({ createUserEmail: createUserEmailFactory({ db }), ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }), findEmail: findEmailFactory({ db }), updateEmailInvites: finalizeInvitedServerRegistrationFactory({ deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }), updateAllInviteTargets: updateAllInviteTargetsFactory({ db }) }), requestNewEmailVerification }), collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(), getUser, getServerInfo }) await finalizeInvite({ finalizerUserId: ctx.userId!, finalizerResourceAccessLimits: ctx.resourceAccessRules, token: args.input.token, accept: args.input.accept, resourceType: WorkspaceInviteResourceType, allowAttachingNewEmail: args.input.addNewEmail ?? undefined }) return true }, cancel: async (_parent, args, ctx) => { await authorizeResolver( ctx.userId, args.workspaceId, Roles.Workspace.Admin, ctx.resourceAccessRules ) const cancelInvite = cancelResourceInviteFactory({ findInvite: findInviteFactory({ db, filterQuery: workspaceInviteValidityFilter }), 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) } }, WorkspaceProjectMutations: { updateRole: async (_parent, args, context) => { const updateWorkspaceProjectRole = updateWorkspaceProjectRoleFactory({ getStream, updateStreamRoleAndNotify, getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db }) }) return await updateWorkspaceProjectRole({ role: args.input, updater: { userId: context.userId!, resourceAccessRules: 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({ db }), updateProject: updateProjectFactory({ db: trx }), upsertProjectRole: upsertProjectRoleFactory({ db: trx }), getProjectCollaborators: getProjectCollaboratorsFactory({ db }), 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: { role: async (parent, _args, ctx) => { const workspace = await ctx.loaders.workspaces!.getWorkspace.load(parent.id) return workspace?.role || null }, team: async (parent, args) => { const team = await getPaginatedWorkspaceTeamFactory({ getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ db }), getWorkspaceCollaboratorsTotalCount: getWorkspaceCollaboratorsTotalCountFactory({ db }) })({ workspaceId: parent.id, filter: removeNullOrUndefinedKeys(args?.filter || {}), limit: args.limit, cursor: args.cursor ?? undefined }) return team }, invitedTeam: async (parent, args) => { const getPendingTeam = getPendingWorkspaceCollaboratorsFactory({ queryAllResourceInvites: queryAllResourceInvitesFactory({ db, filterQuery: workspaceInviteValidityFilter }), getInvitationTargetUsers: getInvitationTargetUsersFactory({ getUsers }) }) return await getPendingTeam({ workspaceId: parent.id, filter: args.filter }) }, projects: async (parent, args, ctx) => { if (!ctx.userId) return [] const getWorkspaceProjects = getWorkspaceProjectsFactory({ getStreams: getUserStreams }) const filter = { ...(args.filter || {}), userId: ctx.userId, workspaceId: parent.id } const { items, cursor } = await getWorkspaceProjects( { workspaceId: parent.id }, { limit: args.limit || 25, cursor: args.cursor || null, filter } ) return { items, cursor, totalCount: await getUserStreamsCount(filter) } }, domains: async (parent) => { return await getWorkspaceDomainsFactory({ db })({ workspaceIds: [parent.id] }) }, billing: (parent) => ({ parent }) }, WorkspaceBilling: { versionsCount: async ({ parent }) => { const workspaceId = parent.id return { current: await countProjectsVersionsByWorkspaceIdFactory({ db })({ workspaceId }), max: WORKSPACE_MAX_PROJECTS_VERSIONS } }, cost: async ({ parent }) => { const workspaceId = parent.id return getWorkspaceCostFactory({ getWorkspaceCostItems: getWorkspaceCostItemsFactory({ countRole: countWorkspaceRoleWithOptionalProjectRoleFactory({ db }), getUserIdsWithRoleInWorkspace: getUserIdsWithRoleInWorkspaceFactory({ db }) }) })({ workspaceId }) } }, WorkspaceCollaborator: { user: async (parent) => { return parent }, role: async (parent) => { return parent.workspaceRole }, projectRoles: async (parent) => { const projectRoles = await getRolesByUserIdFactory({ db })({ userId: parent.id, workspaceId: parent.workspaceId }) return projectRoles.map(({ role, resourceId }) => ({ projectId: resourceId, role })) } }, ProjectRole: { project: async (parent, _args, ctx) => { return await ctx.loaders.streams.getStream.load(parent.projectId) } }, PendingWorkspaceCollaborator: { workspaceName: async (parent, _args, ctx) => { const workspace = await ctx.loaders.workspaces!.getWorkspace.load( parent.workspaceId ) return workspace!.name }, workspaceSlug: async (parent, _args, ctx) => { const workspace = await ctx.loaders.workspaces!.getWorkspace.load( parent.workspaceId ) return workspace!.slug }, 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) => { // If it was specified with the request, just return it if (parent.token?.length) return parent.token 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 }, email: async (parent, _args, ctx) => { if (!parent.user) return parent.email // TODO: Tests to check token & email access? const token = parent.token const authedUserId = ctx.userId const targetUserId = parent.user?.id // Only returning it for the user that is the pending stream collaborator // OR if the token was specified if ( (!authedUserId || !targetUserId || authedUserId !== targetUserId) && !token ) { return null } return parent.email } }, User: { discoverableWorkspaces: async (_parent, _args, context) => { if (!context.userId) { throw new WorkspacesNotAuthorizedError() } const getDiscoverableWorkspacesForUser = getDiscoverableWorkspacesForUserFactory({ findEmailsByUserId: findEmailsByUserIdFactory({ db }), getDiscoverableWorkspaces: getUserDiscoverableWorkspacesFactory({ db }) }) return await getDiscoverableWorkspacesForUser({ userId: context.userId }) }, 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 (parent, _args, context) => { if (!parent.workspaceId) { return null } const workspace = await context.loaders.workspaces!.getWorkspace.load( parent.workspaceId ) if (!workspace) { throw new WorkspaceNotFoundError() } await authorizeResolver( context.userId, parent.workspaceId, Roles.Workspace.Guest, context.resourceAccessRules ) return workspace } }, AdminQueries: { workspaceList: async () => { throw new WorkspacesNotYetImplementedError() } }, LimitedUser: { workspaceDomainPolicyCompliant: async (parent, args) => { const workspaceId = args.workspaceId if (!workspaceId) return null const userId = parent.id return await isUserWorkspaceDomainPolicyCompliantFactory({ findEmailsByUserId: findEmailsByUserIdFactory({ db }), getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }) })({ workspaceId, userId }) } }, ServerInfo: { workspaces: () => ({}) }, ServerWorkspacesInfo: { workspacesEnabled: () => true } } as Resolvers) : {}