From a8ae414bdeb693aa3db9793c5e3b491edc1ad246 Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Thu, 15 May 2025 13:22:42 +0100 Subject: [PATCH] chore(workspaces): drop legacy `join()` (#4752) * fix(workspaces): drop legacy discoverable workspace join * fix(workspaces): one more reference --- .../lib/common/generated/gql/graphql.ts | 7 - .../typedefs/workspaces.graphql | 1 - .../modules/core/graph/generated/graphql.ts | 7 - .../graph/generated/graphql.ts | 6 - .../modules/workspaces/errors/workspace.ts | 6 - .../workspaces/events/eventListener.ts | 2 - .../workspaces/graph/resolvers/workspaces.ts | 44 ------ .../modules/workspaces/services/join.ts | 58 -------- .../tests/unit/services/join.spec.ts | 135 ------------------ .../modules/workspacesCore/domain/events.ts | 8 -- .../graph/resolvers/workspacesCore.ts | 3 - .../server/test/graphql/generated/graphql.ts | 6 - 12 files changed, 283 deletions(-) delete mode 100644 packages/server/modules/workspaces/services/join.ts delete mode 100644 packages/server/modules/workspaces/tests/unit/services/join.spec.ts diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index 0a6e14ff6..c057bea2a 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -4749,7 +4749,6 @@ export type WorkspaceMutations = { /** Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed" */ dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; - join: Workspace; leave: Scalars['Boolean']['output']; projects: WorkspaceProjectMutations; requestToJoin: Scalars['Boolean']['output']; @@ -4792,11 +4791,6 @@ export type WorkspaceMutationsDismissArgs = { }; -export type WorkspaceMutationsJoinArgs = { - input: JoinWorkspaceInput; -}; - - export type WorkspaceMutationsLeaveArgs = { id: Scalars['ID']['input']; }; @@ -9008,7 +9002,6 @@ export type WorkspaceMutationsFieldArgs = { deleteSsoProvider: WorkspaceMutationsDeleteSsoProviderArgs, dismiss: WorkspaceMutationsDismissArgs, invites: {}, - join: WorkspaceMutationsJoinArgs, leave: WorkspaceMutationsLeaveArgs, projects: {}, requestToJoin: WorkspaceMutationsRequestToJoinArgs, diff --git a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql index b7895469e..7032e154a 100644 --- a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql +++ b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql @@ -141,7 +141,6 @@ type WorkspaceMutations { @hasScope(scope: "workspace:update") @hasServerRole(role: SERVER_USER) leave(id: ID!): Boolean! @hasServerRole(role: SERVER_GUEST) - join(input: JoinWorkspaceInput!): Workspace! @hasScope(scope: "workspace:update") addDomain(input: AddDomainToWorkspaceInput!): Workspace! @hasScope(scope: "workspace:update") deleteDomain(input: WorkspaceDomainDeleteInput!): Workspace! diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index ed3708335..d98a0018f 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -4772,7 +4772,6 @@ export type WorkspaceMutations = { /** Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed" */ dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; - join: Workspace; leave: Scalars['Boolean']['output']; projects: WorkspaceProjectMutations; requestToJoin: Scalars['Boolean']['output']; @@ -4815,11 +4814,6 @@ export type WorkspaceMutationsDismissArgs = { }; -export type WorkspaceMutationsJoinArgs = { - input: JoinWorkspaceInput; -}; - - export type WorkspaceMutationsLeaveArgs = { id: Scalars['ID']['input']; }; @@ -7529,7 +7523,6 @@ export type WorkspaceMutationsResolvers>; dismiss?: Resolver>; invites?: Resolver; - join?: Resolver>; leave?: Resolver>; projects?: Resolver; requestToJoin?: Resolver>; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 240f9ce66..cef428105 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -4752,7 +4752,6 @@ export type WorkspaceMutations = { /** Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed" */ dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; - join: Workspace; leave: Scalars['Boolean']['output']; projects: WorkspaceProjectMutations; requestToJoin: Scalars['Boolean']['output']; @@ -4795,11 +4794,6 @@ export type WorkspaceMutationsDismissArgs = { }; -export type WorkspaceMutationsJoinArgs = { - input: JoinWorkspaceInput; -}; - - export type WorkspaceMutationsLeaveArgs = { id: Scalars['ID']['input']; }; diff --git a/packages/server/modules/workspaces/errors/workspace.ts b/packages/server/modules/workspaces/errors/workspace.ts index 5a81b76b1..90439873d 100644 --- a/packages/server/modules/workspaces/errors/workspace.ts +++ b/packages/server/modules/workspaces/errors/workspace.ts @@ -84,12 +84,6 @@ export class WorkspaceNotJoinableError extends BaseError { static statusCode = 400 } -export class WorkspaceJoinNotAllowedError extends BaseError { - static defaultMessage = 'You do not have permissions to join this workspace' - static code = 'WORKSPACE_JOIN_NOT_ALLOWED' - static statusCode = 403 -} - export class WorkspaceUnverifiedDomainError extends BaseError { static defaultMessage = 'Cannot add unverified domain to workspace' static code = 'WORKSPACE_UNVERIFIED_DOMAIN_ERROR' diff --git a/packages/server/modules/workspaces/events/eventListener.ts b/packages/server/modules/workspaces/events/eventListener.ts index c111f4d53..d0b82ed83 100644 --- a/packages/server/modules/workspaces/events/eventListener.ts +++ b/packages/server/modules/workspaces/events/eventListener.ts @@ -650,8 +650,6 @@ export const workspaceTrackingFactory = ...(speckleMembers.hasSpeckleMembers ? speckleMembers : {}) }) break - case 'workspace.joined-from-discovery': - break default: throwUncoveredError(eventName) } diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index 2bffdde7d..424ffb86f 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -46,7 +46,6 @@ import { getEventBus } from '@/modules/shared/services/eventBus' import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/constants' import { WorkspaceInvalidRoleError, - WorkspaceJoinNotAllowedError, WorkspaceNotFoundError, WorkspacePaidPlanActiveError, WorkspacesNotAuthorizedError @@ -122,7 +121,6 @@ import { 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 { @@ -987,48 +985,6 @@ export = FF_WORKSPACES_MODULE_ENABLED return true }, - async join(_parent, args, context) { - if (!context.userId) throw new WorkspaceJoinNotAllowedError() - const workspaceId = args.input.workspaceId - - const logger = context.log.child({ - workspaceId - }) - - const joinWorkspace = joinWorkspaceFactory({ - getUserEmails: findEmailsByUserIdFactory({ db }), - getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), - emitWorkspaceEvent: getEventBus().emit, - addOrUpdateWorkspaceRole: addOrUpdateWorkspaceRoleFactory({ - getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), - findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ - db - }), - getWorkspaceRoles: getWorkspaceRolesFactory({ db }), - upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), - emitWorkspaceEvent: getEventBus().emit, - ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({ - createWorkspaceSeat: createWorkspaceSeatFactory({ db }), - getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }), - eventEmit: getEventBus().emit - }) - }) - }) - - await withOperationLogging( - async () => await joinWorkspace({ userId: context.userId!, workspaceId }), - { - logger, - operationName: 'joinWorkspace', - operationDescription: 'Join workspace' - } - ) - - return await getWorkspaceFactory({ db })({ - workspaceId, - userId: context.userId - }) - }, leave: async (_parent, args, context) => { const workspaceId = args.id diff --git a/packages/server/modules/workspaces/services/join.ts b/packages/server/modules/workspaces/services/join.ts deleted file mode 100644 index 46c3cfeb0..000000000 --- a/packages/server/modules/workspaces/services/join.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations' -import { EventBus } from '@/modules/shared/services/eventBus' -import { - AddOrUpdateWorkspaceRole, - GetWorkspaceWithDomains -} from '@/modules/workspaces/domain/operations' -import { - WorkspaceJoinNotAllowedError, - WorkspaceNotDiscoverableError, - WorkspaceNotJoinableError -} from '@/modules/workspaces/errors/workspace' -import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events' -import { Roles } from '@speckle/shared' - -export const joinWorkspaceFactory = - ({ - getUserEmails, - getWorkspaceWithDomains, - emitWorkspaceEvent, - addOrUpdateWorkspaceRole - }: { - getUserEmails: FindEmailsByUserId - getWorkspaceWithDomains: GetWorkspaceWithDomains - addOrUpdateWorkspaceRole: AddOrUpdateWorkspaceRole - emitWorkspaceEvent: EventBus['emit'] - }) => - async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => { - const userEmails = await getUserEmails({ userId }) - const workspace = await getWorkspaceWithDomains({ id: workspaceId }) - if (!workspace?.discoverabilityEnabled) throw new WorkspaceNotDiscoverableError() - - const workspaceDomains = workspace.domains.filter((domain) => domain.verified) - - if (!workspaceDomains.length) throw new WorkspaceNotJoinableError() - - const matchingEmail = userEmails.find((userEmail) => { - if (!userEmail.verified) return false - return workspaceDomains - .map((domain) => domain.domain) - .includes(userEmail.email.split('@')[1]) - }) - - if (!matchingEmail) throw new WorkspaceJoinNotAllowedError() - - const role = Roles.Workspace.Member - - await addOrUpdateWorkspaceRole({ - userId, - workspaceId, - role, - updatedByUserId: userId - }) - - await emitWorkspaceEvent({ - eventName: WorkspaceEvents.JoinedFromDiscovery, - payload: { userId, workspaceId, role } - }) - } diff --git a/packages/server/modules/workspaces/tests/unit/services/join.spec.ts b/packages/server/modules/workspaces/tests/unit/services/join.spec.ts deleted file mode 100644 index 38a239fed..000000000 --- a/packages/server/modules/workspaces/tests/unit/services/join.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { UserEmail } from '@/modules/core/domain/userEmails/types' -import { createRandomPassword } from '@/modules/core/helpers/testHelpers' -import { - WorkspaceJoinNotAllowedError, - WorkspaceNotDiscoverableError, - WorkspaceNotJoinableError -} from '@/modules/workspaces/errors/workspace' -import { joinWorkspaceFactory } from '@/modules/workspaces/services/join' -import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events' -import { - WorkspaceAcl, - WorkspaceDomain, - WorkspaceWithDomains -} from '@/modules/workspacesCore/domain/types' -import { expectToThrow } from '@/test/assertionHelper' -import { Roles } from '@speckle/shared' -import { expect } from 'chai' -import { assign } from 'lodash' - -const createTestWorkspaceWithDomains = ( - arg?: Partial -): WorkspaceWithDomains => { - const workspace: WorkspaceWithDomains = { - createdAt: new Date(), - updatedAt: new Date(), - name: createRandomPassword(), - slug: createRandomPassword(), - description: createRandomPassword(), - id: createRandomPassword(), - logo: null, - domains: [], - discoverabilityEnabled: false, - domainBasedMembershipProtectionEnabled: false - } - if (arg) assign(workspace, arg) - return workspace -} - -describe('Workspace join services', () => { - describe('joinWorkspaceFactory returns a function, that', () => { - it('throws an error if the workspace is not discoverable', async () => { - const userId = createRandomPassword() - const workspaceId = createRandomPassword() - const error = await expectToThrow(async () => { - await joinWorkspaceFactory({ - getUserEmails: async () => [], - getWorkspaceWithDomains: async () => { - return createTestWorkspaceWithDomains() - }, - emitWorkspaceEvent: async () => { - expect.fail() - }, - addOrUpdateWorkspaceRole: async () => { - throw new Error('Should not be called') - } - })({ userId, workspaceId }) - }) - expect(error.message).to.be.equal(new WorkspaceNotDiscoverableError().message) - }) - it('throws an error if the workspace has no verified domains', async () => { - const userId = createRandomPassword() - const workspaceId = createRandomPassword() - const error = await expectToThrow(async () => { - await joinWorkspaceFactory({ - getUserEmails: async () => [], - getWorkspaceWithDomains: async () => { - return createTestWorkspaceWithDomains({ - discoverabilityEnabled: true, - domains: [{ domain: 'example.com', verified: false }] as WorkspaceDomain[] - }) - }, - addOrUpdateWorkspaceRole: async () => { - expect.fail() - }, - emitWorkspaceEvent: async () => { - expect.fail() - } - })({ userId, workspaceId }) - }) - expect(error.message).to.be.equal(new WorkspaceNotJoinableError().message) - }) - it('throws an error if the user has no verified email matching the domains', async () => { - const userId = createRandomPassword() - const workspaceId = createRandomPassword() - const error = await expectToThrow(async () => { - await joinWorkspaceFactory({ - getUserEmails: async () => - [{ email: 'test@example.com', verified: false }] as UserEmail[], - getWorkspaceWithDomains: async () => { - return createTestWorkspaceWithDomains({ - discoverabilityEnabled: true, - domains: [{ domain: 'example.com', verified: true }] as WorkspaceDomain[] - }) - }, - emitWorkspaceEvent: async () => { - expect.fail() - }, - addOrUpdateWorkspaceRole: async () => { - throw new Error('Should not be called') - } - })({ userId, workspaceId }) - }) - expect(error.message).to.be.equal(new WorkspaceJoinNotAllowedError().message) - }) - it('creates a workspace member role and emits workspace events', async () => { - const userId = createRandomPassword() - const workspaceId = createRandomPassword() - let storedWorkspaceRole: - | Pick - | undefined = undefined - const firedEvents: string[] = [] - await joinWorkspaceFactory({ - getUserEmails: async () => - [{ email: 'test@example.com', verified: true }] as UserEmail[], - getWorkspaceWithDomains: async () => { - return createTestWorkspaceWithDomains({ - discoverabilityEnabled: true, - domains: [{ domain: 'example.com', verified: true }] as WorkspaceDomain[] - }) - }, - addOrUpdateWorkspaceRole: async (acl) => { - storedWorkspaceRole = acl - }, - emitWorkspaceEvent: async ({ eventName }) => { - firedEvents.push(eventName) - } - })({ userId, workspaceId }) - - expect(storedWorkspaceRole!.userId).to.equal(userId) - expect(storedWorkspaceRole!.workspaceId).to.equal(workspaceId) - expect(storedWorkspaceRole!.role).to.equal(Roles.Workspace.Member) - expect(firedEvents).deep.equal([WorkspaceEvents.JoinedFromDiscovery]) - }) - }) -}) diff --git a/packages/server/modules/workspacesCore/domain/events.ts b/packages/server/modules/workspacesCore/domain/events.ts index 18cbadf49..56394ced9 100644 --- a/packages/server/modules/workspacesCore/domain/events.ts +++ b/packages/server/modules/workspacesCore/domain/events.ts @@ -1,6 +1,5 @@ import { WorkspaceSeat } from '@/modules/gatekeeper/domain/billing' import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types' -import { WorkspaceRoles } from '@speckle/shared' export const workspaceEventNamespace = 'workspace' as const @@ -13,7 +12,6 @@ export const WorkspaceEvents = { Deleted: `${eventPrefix}deleted`, RoleDeleted: `${eventPrefix}role-deleted`, RoleUpdated: `${eventPrefix}role-updated`, - JoinedFromDiscovery: `${eventPrefix}joined-from-discovery`, SeatUpdated: `${eventPrefix}seat-updated` } as const @@ -41,11 +39,6 @@ type WorkspaceSeatUpdatedPayload = { seat: WorkspaceSeat updatedByUserId: string } -type WorkspaceJoinedFromDiscoveryPayload = { - userId: string - workspaceId: string - role: WorkspaceRoles -} export type WorkspaceEventsPayloads = { [WorkspaceEvents.Authorizing]: WorkspaceAuthorizedPayload @@ -55,5 +48,4 @@ export type WorkspaceEventsPayloads = { [WorkspaceEvents.RoleDeleted]: WorkspaceRoleDeletedPayload [WorkspaceEvents.RoleUpdated]: WorkspaceRoleUpdatedPayload [WorkspaceEvents.SeatUpdated]: WorkspaceSeatUpdatedPayload - [WorkspaceEvents.JoinedFromDiscovery]: WorkspaceJoinedFromDiscoveryPayload } diff --git a/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts b/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts index 8d3898831..899dd8942 100644 --- a/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts +++ b/packages/server/modules/workspacesCore/graph/resolvers/workspacesCore.ts @@ -44,9 +44,6 @@ export = !FF_WORKSPACES_MODULE_ENABLED deleteDomain: async () => { throw new WorkspacesModuleDisabledError() }, - join: async () => { - throw new WorkspacesModuleDisabledError() - }, leave: async () => { throw new WorkspacesModuleDisabledError() }, diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index ffbbc689e..9f7463623 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -4753,7 +4753,6 @@ export type WorkspaceMutations = { /** Dismiss a workspace from the discoverable list, behind the scene a join request is created with the status "dismissed" */ dismiss: Scalars['Boolean']['output']; invites: WorkspaceInviteMutations; - join: Workspace; leave: Scalars['Boolean']['output']; projects: WorkspaceProjectMutations; requestToJoin: Scalars['Boolean']['output']; @@ -4796,11 +4795,6 @@ export type WorkspaceMutationsDismissArgs = { }; -export type WorkspaceMutationsJoinArgs = { - input: JoinWorkspaceInput; -}; - - export type WorkspaceMutationsLeaveArgs = { id: Scalars['ID']['input']; };