From 27fdb00d07cfb547350f8931d8ccad7453538493 Mon Sep 17 00:00:00 2001 From: martmull Date: Thu, 28 Mar 2024 17:59:48 +0100 Subject: [PATCH] 4586 fix workspace member feature (#4680) * Fix import * Handle delete workspace member consequences * Add a patch to request deleted workspace member's userId * Remove useless relations * Handle delete workspace + refactor * Add missing migration * Fix test * Code review returns * Add missing operation in migration file * Fix code review return update * Fix workspaceMember<>ConnectedAccount relation --- .../1711557405330-addMissingMigration.ts | 102 +++++++++++++ .../1711624086253-updateRefreshTokenTable.ts | 12 ++ .../workspace-query-runner.service.ts | 43 +++++- .../user/services/user.service.ts | 42 ++++++ .../handle-workspace-member-deleted.job.ts | 22 +++ .../services/workspace.service.spec.ts | 5 + .../workspace/services/workspace.service.ts | 139 +++--------------- .../workspace-workspace-member.listener.ts | 35 +++++ .../workspace/workspace.module.ts | 12 +- .../integrations/message-queue/jobs.module.ts | 5 + .../message-queue/message-queue.constants.ts | 1 + .../delete-incomplete-workspaces.command.ts | 5 +- .../workspace-member.object-metadata.ts | 1 + packages/twenty-server/tsconfig.json | 1 + 14 files changed, 298 insertions(+), 127 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/1711557405330-addMissingMigration.ts create mode 100644 packages/twenty-server/src/engine/core-modules/workspace/handle-workspace-member-deleted.job.ts create mode 100644 packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1711557405330-addMissingMigration.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1711557405330-addMissingMigration.ts new file mode 100644 index 0000000000..4cbc77a4d9 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1711557405330-addMissingMigration.ts @@ -0,0 +1,102 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddMissingMigration1711557405330 implements MigrationInterface { + name = 'AddMissingMigration1711557405330'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_cb488f32c6a0827b938edadf221"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE USING "createdAt"::TIMESTAMP WITH TIME ZONE`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ALTER COLUMN "createdAt" SET DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ALTER COLUMN "createdAt" SET NOT NULL;`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE USING "updatedAt"::TIMESTAMP WITH TIME ZONE`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ALTER COLUMN "updatedAt" SET DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ALTER COLUMN "updatedAt" SET NOT NULL;`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ALTER COLUMN "deletedAt" TYPE TIMESTAMP WITH TIME ZONE USING "deletedAt"::TIMESTAMP WITH TIME ZONE`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."user" DROP CONSTRAINT "FK_2ec910029395fa7655621c88908"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."user" ALTER COLUMN "defaultWorkspaceId" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "IndexOnUserIdAndWorkspaceIdUnique" UNIQUE ("userId", "workspaceId")`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "FK_a2da2ea7d6cd1e5a4c5cb1791f8" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "FK_22f5e76f493c3fb20237cfc48b0" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."user" ADD CONSTRAINT "FK_2ec910029395fa7655621c88908" FOREIGN KEY ("defaultWorkspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."user" DROP CONSTRAINT "FK_2ec910029395fa7655621c88908"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_22f5e76f493c3fb20237cfc48b0"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_a2da2ea7d6cd1e5a4c5cb1791f8"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "IndexOnUserIdAndWorkspaceIdUnique"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."user" ALTER COLUMN "defaultWorkspaceId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."user" ADD CONSTRAINT "FK_2ec910029395fa7655621c88908" FOREIGN KEY ("defaultWorkspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP COLUMN "deletedAt"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ADD "deletedAt" TIMESTAMP`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP COLUMN "updatedAt"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ADD "updatedAt" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP COLUMN "createdAt"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "FK_cb488f32c6a0827b938edadf221" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1711624086253-updateRefreshTokenTable.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1711624086253-updateRefreshTokenTable.ts index efb9a3dd34..5ea12b91fa 100644 --- a/packages/twenty-server/src/database/typeorm/core/migrations/1711624086253-updateRefreshTokenTable.ts +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1711624086253-updateRefreshTokenTable.ts @@ -19,9 +19,21 @@ export class UpdateRefreshTokenTable1711624086253 await queryRunner.query( `ALTER TABLE "core"."appToken" ADD CONSTRAINT "FK_d6ae19a7aa2bbd4919053257772" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); + await queryRunner.query( + `ALTER TABLE "core"."appToken" DROP CONSTRAINT "FK_7008a2b0fb083127f60b5f4448e"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."appToken" ADD CONSTRAINT "FK_8cd4819144baf069777b5729136" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); } public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."appToken" ADD CONSTRAINT "FK_7008a2b0fb083127f60b5f4448e" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."appToken" DROP CONSTRAINT "FK_8cd4819144baf069777b5729136"`, + ); await queryRunner.query( `ALTER TABLE "core"."appToken" DROP CONSTRAINT "FK_d6ae19a7aa2bbd4919053257772"`, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts index c3b13e3935..2c0dc67938 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts @@ -407,6 +407,15 @@ export class WorkspaceQueryRunnerService { args, options, ); + + // TODO START: remove this awful patch and use our upcoming custom ORM is developed + const deletedWorkspaceMember = await this.handleDeleteWorkspaceMember( + args.id, + workspaceId, + objectMetadataItem, + ); + // TODO END + const result = await this.execute(query, workspaceId); const parsedResults = ( @@ -429,7 +438,10 @@ export class WorkspaceQueryRunnerService { recordId: args.id, objectMetadata: objectMetadataItem, details: { - before: this.removeNestedProperties(parsedResults?.[0]), + before: { + ...(deletedWorkspaceMember ?? {}), + ...this.removeNestedProperties(parsedResults?.[0]), + }, }, } satisfies ObjectRecordDeleteEvent); @@ -555,4 +567,33 @@ export class WorkspaceQueryRunnerService { ); }); } + + async handleDeleteWorkspaceMember( + id: string, + workspaceId: string, + objectMetadataItem: ObjectMetadataInterface, + ) { + if (objectMetadataItem.nameSingular !== 'workspaceMember') { + return; + } + + const workspaceMemberResult = await this.executeAndParse( + ` + query { + workspaceMemberCollection(filter: {id: {eq: "${id}"}}) { + edges { + node { + userId: userId + } + } + } + } + `, + objectMetadataItem, + '', + workspaceId, + ); + + return workspaceMemberResult.edges?.[0]?.node; + } } diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index 505cb32c93..998066e0b6 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -113,4 +113,46 @@ export class UserService extends TypeOrmQueryService { return user; } + + async handleRemoveWorkspaceMember(workspaceId: string, userId: string) { + await this.userWorkspaceRepository.delete({ + userId, + workspaceId, + }); + await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId); + } + + private async reassignOrRemoveUserDefaultWorkspace( + workspaceId: string, + userId: string, + ) { + const userWorkspaces = await this.userWorkspaceRepository.find({ + where: { userId: userId }, + }); + + if (userWorkspaces.length === 0) { + await this.userRepository.delete({ id: userId }); + + return; + } + + const user = await this.userRepository.findOne({ + where: { + id: userId, + }, + }); + + if (!user) { + throw new Error(`User ${userId} not found in workspace ${workspaceId}`); + } + + if (user.defaultWorkspaceId === workspaceId) { + await this.userRepository.update( + { id: userId }, + { + defaultWorkspaceId: userWorkspaces[0].workspaceId, + }, + ); + } + } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/handle-workspace-member-deleted.job.ts b/packages/twenty-server/src/engine/core-modules/workspace/handle-workspace-member-deleted.job.ts new file mode 100644 index 0000000000..7f36247c1e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workspace/handle-workspace-member-deleted.job.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; + +import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface'; + +import { UserService } from 'src/engine/core-modules/user/services/user.service'; + +export type HandleWorkspaceMemberDeletedJobData = { + workspaceId: string; + userId: string; +}; +@Injectable() +export class HandleWorkspaceMemberDeletedJob + implements MessageQueueJob +{ + constructor(private readonly userService: UserService) {} + + async handle(data: HandleWorkspaceMemberDeletedJobData): Promise { + const { workspaceId, userId } = data; + + await this.userService.handleRemoveWorkspaceMember(workspaceId, userId); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts index cce4de1f42..8b6dbbfd1d 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts @@ -7,6 +7,7 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works import { User } from 'src/engine/core-modules/user/user.entity'; import { BillingService } from 'src/engine/core-modules/billing/billing.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { WorkspaceService } from './workspace.service'; @@ -37,6 +38,10 @@ describe('WorkspaceService', () => { provide: UserWorkspaceService, useValue: {}, }, + { + provide: UserService, + useValue: {}, + }, { provide: BillingService, useValue: {}, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index 7ff8309d23..532ae2fa6e 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -13,6 +13,7 @@ import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/a import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { BillingService } from 'src/engine/core-modules/billing/billing.service'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; export class WorkspaceService extends TypeOrmQueryService { constructor( @@ -20,11 +21,10 @@ export class WorkspaceService extends TypeOrmQueryService { private readonly workspaceRepository: Repository, @InjectRepository(UserWorkspace, 'core') private readonly userWorkspaceRepository: Repository, - @InjectRepository(User, 'core') - private readonly userRepository: Repository, private readonly workspaceManagerService: WorkspaceManagerService, private readonly userWorkspaceService: UserWorkspaceService, private readonly billingService: BillingService, + private readonly userService: UserService, ) { super(workspaceRepository); } @@ -49,7 +49,7 @@ export class WorkspaceService extends TypeOrmQueryService { return await this.workspaceManagerService.doesDataSourceExist(id); } - async deleteWorkspace(id: string, shouldDeleteCoreWorkspace = true) { + async solfDeleteWorkspace(id: string) { const workspace = await this.workspaceRepository.findOneBy({ id }); assert(workspace, 'Workspace not found'); @@ -58,9 +58,24 @@ export class WorkspaceService extends TypeOrmQueryService { await this.billingService.deleteSubscription(workspace.id); await this.workspaceManagerService.delete(id); - if (shouldDeleteCoreWorkspace) { - await this.workspaceRepository.delete(id); + + return workspace; + } + + async deleteWorkspace(id: string) { + const userWorkspaces = await this.userWorkspaceRepository.findBy({ + workspaceId: id, + }); + + const workspace = await this.solfDeleteWorkspace(id); + + for (const userWorkspace of userWorkspaces) { + await this.userService.handleRemoveWorkspaceMember( + id, + userWorkspace.userId, + ); } + await this.workspaceRepository.delete(id); return workspace; } @@ -70,118 +85,4 @@ export class WorkspaceService extends TypeOrmQueryService { .find() .then((workspaces) => workspaces.map((workspace) => workspace.id)); } - - private async reassignDefaultWorkspace( - currentWorkspaceId: string, - user: User, - worskpaces: UserWorkspace[], - ) { - // We'll filter all user workspaces without the one which its getting removed from - const filteredUserWorkspaces = worskpaces.filter( - (workspace) => workspace.workspaceId !== currentWorkspaceId, - ); - - // Loop over each workspace in the filteredUserWorkspaces array and check if it currently exists in - // the database - for (let index = 0; index < filteredUserWorkspaces.length; index++) { - const userWorkspace = filteredUserWorkspaces[index]; - - const nextWorkspace = await this.workspaceRepository.findOneBy({ - id: userWorkspace.workspaceId, - }); - - if (nextWorkspace) { - await this.userRepository.save({ - id: user.id, - defaultWorkspace: nextWorkspace, - updatedAt: new Date().toISOString(), - }); - break; - } - - // if no workspaces are valid then we delete the user - if (index === filteredUserWorkspaces.length - 1) { - await this.userRepository.delete({ id: user.id }); - } - } - } - - /* - async removeWorkspaceMember(workspaceId: string, memberId: string) { - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - workspaceId, - ); - - const workspaceDataSource = - await this.typeORMService.connectToDataSource(dataSourceMetadata); - - // using "SELECT *" here because we will need the corresponding members userId later - const [workspaceMember] = await workspaceDataSource?.query( - `SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "id" = '${memberId}'`, - ); - - if (!workspaceMember) { - throw new NotFoundException('Member not found.'); - } - - await workspaceDataSource?.query( - `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "id" = '${memberId}'`, - ); - - const workspaceMemberUser = await this.userRepository.findOne({ - where: { - id: workspaceMember.userId, - }, - relations: ['defaultWorkspace'], - }); - - if (!workspaceMemberUser) { - throw new NotFoundException('User not found'); - } - - const userWorkspaces = await this.userWorkspaceRepository.find({ - where: { userId: workspaceMemberUser.id }, - relations: ['workspace'], - }); - - // We want to check if we the user has signed up to more than one workspace - if (userWorkspaces.length > 1) { - // We neeed to check if the workspace that its getting removed from is its default workspace, if it is then - // change the default workspace to point to the next workspace available. - if (workspaceMemberUser.defaultWorkspace.id === workspaceId) { - await this.reassignDefaultWorkspace( - workspaceId, - workspaceMemberUser, - userWorkspaces, - ); - } - // if its not the default workspace then simply delete the user-workspace mapping - await this.userWorkspaceRepository.delete({ - userId: workspaceMemberUser.id, - workspaceId, - }); - } else { - await this.userWorkspaceRepository.delete({ - userId: workspaceMemberUser.id, - }); - - // After deleting the user-workspace mapping, we have a condition where we have the users default workspace points to a - // workspace which it doesnt have access to. So we delete the user. - await this.userRepository.delete({ id: workspaceMemberUser.id }); - } - - const payload = - new ObjectRecordDeleteEvent(); - - payload.workspaceId = workspaceId; - payload.details = { - before: workspaceMember, - }; - - this.eventEmitter.emit('workspaceMember.deleted', payload); - - return memberId; - } - */ } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts new file mode 100644 index 0000000000..3b6982cfe0 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts @@ -0,0 +1,35 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata'; +import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event'; +import { + HandleWorkspaceMemberDeletedJob, + HandleWorkspaceMemberDeletedJobData, +} from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job'; + +@Injectable() +export class WorkspaceWorkspaceMemberListener { + constructor( + @Inject(MessageQueue.workspaceQueue) + private readonly messageQueueService: MessageQueueService, + ) {} + + @OnEvent('workspaceMember.deleted') + async handleDeleteEvent( + payload: ObjectRecordDeleteEvent, + ) { + const userId = payload.details.before.userId; + + if (!userId) { + return; + } + + await this.messageQueueService.add( + HandleWorkspaceMemberDeletedJob.name, + { workspaceId: payload.workspaceId, userId }, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 60497404a6..ea8050b348 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -8,11 +8,12 @@ import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.r import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { User } from 'src/engine/core-modules/user/user.entity'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { UserModule } from 'src/engine/core-modules/user/user.module'; +import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; import { Workspace } from './workspace.entity'; @@ -27,19 +28,24 @@ import { WorkspaceService } from './services/workspace.service'; BillingModule, FileUploadModule, NestjsQueryTypeOrmModule.forFeature( - [User, Workspace, UserWorkspace, FeatureFlagEntity], + [Workspace, UserWorkspace, FeatureFlagEntity], 'core', ), UserWorkspaceModule, WorkspaceManagerModule, DataSourceModule, TypeORMModule, + UserModule, ], services: [WorkspaceService], resolvers: workspaceAutoResolverOpts, }), ], exports: [WorkspaceService], - providers: [WorkspaceResolver, WorkspaceService], + providers: [ + WorkspaceResolver, + WorkspaceService, + WorkspaceWorkspaceMemberListener, + ], }) export class WorkspaceModule {} diff --git a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts index 2257cf4adf..821cc082f0 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts @@ -47,6 +47,7 @@ import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-obj import { SaveEventToDbJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job'; import { CreateCompanyAndContactJob } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job'; import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.object-metadata'; +import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job'; import { GmailFullSynV2Module } from 'src/modules/messaging/services/gmail-full-sync-v2/gmail-full-sync.v2.module'; import { GmailFetchMessageContentFromCacheModule } from 'src/modules/messaging/services/gmail-fetch-message-content-from-cache/gmail-fetch-message-content-from-cache.module'; import { FetchAllMessagesFromCacheCronJob } from 'src/modules/messaging/commands/crons/fetch-all-messages-from-cache.cron-job'; @@ -139,6 +140,10 @@ import { GmailPartialSyncV2Module } from 'src/modules/messaging/services/gmail-p useClass: DeleteConnectedAccountAssociatedCalendarDataJob, }, { provide: UpdateSubscriptionJob.name, useClass: UpdateSubscriptionJob }, + { + provide: HandleWorkspaceMemberDeletedJob.name, + useClass: HandleWorkspaceMemberDeletedJob, + }, { provide: RecordPositionBackfillJob.name, useClass: RecordPositionBackfillJob, diff --git a/packages/twenty-server/src/engine/integrations/message-queue/message-queue.constants.ts b/packages/twenty-server/src/engine/integrations/message-queue/message-queue.constants.ts index 64d576839c..7576b8693a 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/message-queue.constants.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/message-queue.constants.ts @@ -9,6 +9,7 @@ export enum MessageQueue { calendarQueue = 'calendar-queue', contactCreationQueue = 'contact-creation-queue', billingQueue = 'billing-queue', + workspaceQueue = 'workspace-queue', recordPositionBackfillQueue = 'record-position-backfill-queue', entityEventsToDbQueue = 'entity-events-to-db-queue', } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-incomplete-workspaces.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-incomplete-workspaces.command.ts index 27f02bcc48..372968b1c8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-incomplete-workspaces.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-incomplete-workspaces.command.ts @@ -83,10 +83,7 @@ export class DeleteIncompleteWorkspacesCommand extends CommandRunner { } name: '${incompleteWorkspace.displayName}'`, ); if (!options.dryRun) { - await this.workspaceService.deleteWorkspace( - incompleteWorkspace.id, - false, - ); + await this.workspaceService.solfDeleteWorkspace(incompleteWorkspace.id); } } } diff --git a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts index 6a961658fd..86cd02db13 100644 --- a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts +++ b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.object-metadata.ts @@ -186,6 +186,7 @@ export class WorkspaceMemberObjectMetadata extends BaseObjectMetadata { type: RelationMetadataType.ONE_TO_MANY, inverseSideTarget: () => ConnectedAccountObjectMetadata, inverseSideFieldKey: 'accountOwner', + onDelete: RelationOnDeleteAction.CASCADE, }) connectedAccounts: ConnectedAccountObjectMetadata[]; diff --git a/packages/twenty-server/tsconfig.json b/packages/twenty-server/tsconfig.json index bd6cd65c0a..c0fc8b9bb4 100644 --- a/packages/twenty-server/tsconfig.json +++ b/packages/twenty-server/tsconfig.json @@ -25,6 +25,7 @@ "types": ["jest", "node"], "paths": { "src/*": ["packages/twenty-server/src/*"], + "test/*": ["packages/twenty-server/test/*"], "twenty-emails": ["packages/twenty-emails/src/index.ts"] } },