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 e84b9402c8..3d9b8a91d0 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 @@ -9,7 +9,6 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { Repository } from 'typeorm'; -import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service'; @@ -51,7 +50,6 @@ import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version @Injectable() // eslint-disable-next-line @nx/workspace-inject-workspace-repository export class WorkspaceService extends TypeOrmQueryService { - private readonly featureLookUpKey = BillingEntitlementKey.CUSTOM_DOMAIN; protected readonly logger = new Logger(WorkspaceService.name); private readonly WORKSPACE_FIELD_PERMISSIONS: Record< diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts index b774a47b2f..4a801c2fe0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts @@ -1,11 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { isDefined } from 'twenty-shared/utils'; +import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util'; import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util'; @@ -165,50 +166,127 @@ export class ObjectMetadataServiceV2 { workspaceId: string; isSystemBuild?: boolean; }): Promise { + const deletedObjectMetadataDtos = await this.deleteManyObjectMetadatas({ + deleteObjectInputs: [deleteObjectInput], + workspaceId, + isSystemBuild, + }); + + if (deletedObjectMetadataDtos.length !== 1) { + throw new ObjectMetadataException( + 'Could not retrieve deleted object metadata dto', + ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + + const [deletedObjectMetadataDto] = deletedObjectMetadataDtos; + + return deletedObjectMetadataDto; + } + + private async deleteManyObjectMetadatas({ + workspaceId, + deleteObjectInputs, + isSystemBuild = false, + }: { + deleteObjectInputs: DeleteOneObjectInput[]; + workspaceId: string; + isSystemBuild?: boolean; + }): Promise { + if (deleteObjectInputs.length === 0) { + return []; + } + + const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatObjectMetadataMaps', + 'flatIndexMaps', + 'flatFieldMetadataMaps', + ], + }, + ); + + const initialAccumulator: { + flatFieldMetadatasToDeleteById: Record; + flatObjectMetadatasToDeleteById: Record; + flatIndexToDeleteById: Record; + } = { + flatFieldMetadatasToDeleteById: {}, + flatIndexToDeleteById: {}, + flatObjectMetadatasToDeleteById: {}, + }; + const { - flatObjectMetadataMaps: existingFlatObjectMetadataMaps, - flatIndexMaps: existingFlatIndexMaps, - flatFieldMetadataMaps: existingFlatFieldMetadataMaps, - } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: [ - 'flatObjectMetadataMaps', - 'flatIndexMaps', - 'flatFieldMetadataMaps', - ], - }, - ); + flatFieldMetadatasToDeleteById, + flatIndexToDeleteById, + flatObjectMetadatasToDeleteById, + } = deleteObjectInputs.reduce((accumulator, deleteObjectInput) => { + const { + flatFieldMetadatasToDelete, + flatObjectMetadataToDelete, + flatIndexToDelete, + } = fromDeleteObjectInputToFlatFieldMetadatasToDelete({ + flatObjectMetadataMaps, + flatIndexMaps, + flatFieldMetadataMaps, + deleteObjectInput, + }); + + return { + flatFieldMetadatasToDeleteById: { + ...accumulator.flatFieldMetadatasToDeleteById, + ...fromArrayToUniqueKeyRecord({ + array: flatFieldMetadatasToDelete, + uniqueKey: 'id', + }), + }, + flatIndexToDeleteById: { + ...accumulator.flatIndexToDeleteById, + ...fromArrayToUniqueKeyRecord({ + array: flatIndexToDelete, + uniqueKey: 'id', + }), + }, + flatObjectMetadatasToDeleteById: { + ...accumulator.flatObjectMetadatasToDeleteById, + [flatObjectMetadataToDelete.id]: flatObjectMetadataToDelete, + }, + }; + }, initialAccumulator); const { flatFieldMetadatasToDelete, - flatObjectMetadataToDelete, + flatObjectMetadatasToDelete, flatIndexToDelete, - } = fromDeleteObjectInputToFlatFieldMetadatasToDelete({ - flatObjectMetadataMaps: existingFlatObjectMetadataMaps, - flatIndexMaps: existingFlatIndexMaps, - flatFieldMetadataMaps: existingFlatFieldMetadataMaps, - deleteObjectInput, - }); + } = { + flatFieldMetadatasToDelete: Object.values(flatFieldMetadatasToDeleteById), + flatObjectMetadatasToDelete: Object.values( + flatObjectMetadatasToDeleteById, + ), + flatIndexToDelete: Object.values(flatIndexToDeleteById), + }; const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { flatObjectMetadataMaps: computeFlatEntityMapsFromTo({ - flatEntityMaps: existingFlatObjectMetadataMaps, + flatEntityMaps: flatObjectMetadataMaps, flatEntityToCreate: [], - flatEntityToDelete: [flatObjectMetadataToDelete], + flatEntityToDelete: flatObjectMetadatasToDelete, flatEntityToUpdate: [], }), flatIndexMaps: computeFlatEntityMapsFromTo({ - flatEntityMaps: existingFlatIndexMaps, + flatEntityMaps: flatIndexMaps, flatEntityToCreate: [], flatEntityToDelete: flatIndexToDelete, flatEntityToUpdate: [], }), flatFieldMetadataMaps: computeFlatEntityMapsFromTo({ - flatEntityMaps: existingFlatFieldMetadataMaps, + flatEntityMaps: flatFieldMetadataMaps, flatEntityToCreate: [], flatEntityToDelete: flatFieldMetadatasToDelete, flatEntityToUpdate: [], @@ -229,12 +307,12 @@ export class ObjectMetadataServiceV2 { if (isDefined(validateAndBuildResult)) { throw new WorkspaceMigrationBuilderExceptionV2( validateAndBuildResult, - 'Multiple validation errors occurred while deleting object', + `Multiple validation errors occurred while deleting object${deleteObjectInputs.length > 1 ? 's' : ''}`, ); } - return fromFlatObjectMetadataToObjectMetadataDto( - flatObjectMetadataToDelete, + return flatObjectMetadatasToDelete.map( + fromFlatObjectMetadataToObjectMetadataDto, ); } @@ -445,4 +523,30 @@ export class ObjectMetadataServiceV2 { position: favoriteCount, }); } + + public async deleteWorkspaceAllObjectMetadata({ + workspaceId, + }: { + workspaceId: string; + }) { + const { flatObjectMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps'], + }, + ); + + const deleteObjectInputs = Object.values(flatObjectMetadataMaps.byId) + .filter(isDefined) + .map((flatObjectMetadata) => ({ + id: flatObjectMetadata.id, + })); + + await this.deleteManyObjectMetadatas({ + deleteObjectInputs, + workspaceId, + isSystemBuild: true, + }); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts index fcb7b918a8..297f11879e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts @@ -107,6 +107,7 @@ export class ObjectMetadataEntity @Column({ default: false }) isLabelSyncedWithName: boolean; + // TODO create a relation to workspace with cascade delete will also create foreignKey @Column({ nullable: false, type: 'uuid' }) workspaceId: string; diff --git a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-column-manager.service.ts b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-column-manager.service.ts index 831cb92667..fcc55a1198 100644 --- a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-column-manager.service.ts +++ b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-column-manager.service.ts @@ -33,20 +33,23 @@ export class WorkspaceSchemaColumnManagerService { schemaName, tableName, columnNames, + cascade = false, }: { queryRunner: QueryRunner; schemaName: string; tableName: string; columnNames: string[]; + cascade?: boolean; }): Promise { if (columnNames.length === 0) return; const safeSchemaName = removeSqlDDLInjection(schemaName); const safeTableName = removeSqlDDLInjection(tableName); + const cascadeClause = cascade ? ' CASCADE' : ''; const dropClauses = columnNames.map((name) => { const safeName = removeSqlDDLInjection(name); - return `DROP COLUMN IF EXISTS "${safeName}"`; + return `DROP COLUMN IF EXISTS "${safeName}"${cascadeClause}`; }); const sql = `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ${dropClauses.join(', ')}`; diff --git a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-table-manager.service.ts b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-table-manager.service.ts index cc4e8d84fe..679ad2c3cc 100644 --- a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-table-manager.service.ts +++ b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-table-manager.service.ts @@ -39,14 +39,17 @@ export class WorkspaceSchemaTableManagerService { queryRunner, schemaName, tableName, + cascade = false, }: { queryRunner: QueryRunner; schemaName: string; tableName: string; + cascade?: boolean; }): Promise { const safeSchemaName = removeSqlDDLInjection(schemaName); const safeTableName = removeSqlDDLInjection(tableName); - const sql = `DROP TABLE IF EXISTS "${safeSchemaName}"."${safeTableName}"`; + const cascadeClause = cascade ? ' CASCADE' : ''; + const sql = `DROP TABLE IF EXISTS "${safeSchemaName}"."${safeTableName}"${cascadeClause}`; await queryRunner.query(sql); } diff --git a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts index 7e2b7644f9..240efddac8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts @@ -11,6 +11,7 @@ import { AgentService } from 'src/engine/metadata-modules/agent/agent.service'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; @@ -27,7 +28,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works describe('WorkspaceManagerService', () => { let service: WorkspaceManagerService; - let objectMetadataService: ObjectMetadataService; + let objectMetadataServiceV2: ObjectMetadataServiceV2; let workspaceMigrationRepository: Repository; let dataSourceRepository: Repository; let workspaceDataSourceService: WorkspaceDataSourceService; @@ -101,6 +102,10 @@ describe('WorkspaceManagerService', () => { provide: FeatureFlagService, useValue: {}, }, + { + provide: ObjectMetadataService, + useValue: {}, + }, { provide: RoleService, useValue: {}, @@ -123,11 +128,10 @@ describe('WorkspaceManagerService', () => { provide: WorkspaceSyncMetadataService, useValue: {}, }, - { - provide: ObjectMetadataService, + provide: ObjectMetadataServiceV2, useValue: { - deleteObjectsMetadata: jest.fn(), + deleteWorkspaceAllObjectMetadata: jest.fn(), }, }, { @@ -150,8 +154,8 @@ describe('WorkspaceManagerService', () => { }).compile(); service = module.get(WorkspaceManagerService); - objectMetadataService = module.get( - ObjectMetadataService, + objectMetadataServiceV2 = module.get( + ObjectMetadataServiceV2, ); workspaceMigrationRepository = module.get< Repository @@ -177,7 +181,9 @@ describe('WorkspaceManagerService', () => { describe('delete', () => { it('should delete all the workspace metadata tables and workspace schema', async () => { await service.delete('workspace-id'); - expect(objectMetadataService.deleteObjectsMetadata).toHaveBeenCalled(); + expect( + objectMetadataServiceV2.deleteWorkspaceAllObjectMetadata, + ).toHaveBeenCalled(); expect(workspaceMigrationRepository.delete).toHaveBeenCalledWith({ workspaceId: 'workspace-id', }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts index c92fbfb0b2..50ff0eafe0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -18,7 +19,6 @@ import { DevSeederModule } from 'src/engine/workspace-manager/dev-seeder/dev-see import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; -import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { WorkspaceManagerService } from './workspace-manager.service'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 69660fbe01..f0ce3bc0e4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -9,6 +9,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @@ -32,6 +33,7 @@ export class WorkspaceManagerService { private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly objectMetadataService: ObjectMetadataService, + private readonly objectMetadataServiceV2: ObjectMetadataServiceV2, private readonly dataSourceService: DataSourceService, private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService, @InjectRepository(UserWorkspaceEntity) @@ -136,32 +138,23 @@ export class WorkspaceManagerService { }); } + // TODO investigate why some entities are not on cascade delete + // Are foreign keys correctly applied ? public async delete(workspaceId: string): Promise { - //TODO: delete all logs when #611 closed - this.logger.log(`Deleting workspace ${workspaceId} ...`); - await this.roleTargetsRepository.delete({ workspaceId, }); - this.logger.log(`workspace ${workspaceId} role targets deleted`); - await this.roleRepository.delete({ workspaceId, }); - this.logger.log(`workspace ${workspaceId} role deleted`); - await this.objectMetadataService.deleteObjectsMetadata(workspaceId); - this.logger.log(`workspace ${workspaceId} object metadata deleted`); + await this.objectMetadataServiceV2.deleteWorkspaceAllObjectMetadata({ + workspaceId, + }); await this.workspaceMigrationService.deleteAllWithinWorkspace(workspaceId); - this.logger.log(`workspace ${workspaceId} migration deleted`); - await this.dataSourceService.delete(workspaceId); - this.logger.log(`workspace ${workspaceId} data source deleted`); - // Delete schema await this.workspaceDataSourceService.deleteWorkspaceDBSchema(workspaceId); - - this.logger.log(`workspace ${workspaceId} schema deleted`); } private async setupDefaultRoles( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/delete-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/delete-field-action-handler.service.ts index f6eacbff0f..95f9e60872 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/delete-field-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/delete-field-action-handler.service.ts @@ -103,6 +103,7 @@ export class DeleteFieldActionHandlerService extends WorkspaceMigrationRunnerAct schemaName, tableName, columnNames: columnNamesToDrop, + cascade: true, }); const enumOperations = collectEnumOperationsForField({ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service.ts index 08f7420528..fe5cc61d01 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service.ts @@ -89,6 +89,7 @@ export class DeleteObjectActionHandlerService extends WorkspaceMigrationRunnerAc queryRunner, schemaName, tableName, + cascade: true, }); const objectFlatFieldMetadatas = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface.ts index d1fed02cf0..e95752c954 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface.ts @@ -73,21 +73,26 @@ export abstract class BaseWorkspaceMigrationRunnerActionHandlerService< | MetadataToFlatEntityMapsKey > > { - await Promise.all([ - this.asyncMethodPerformanceMetricWrapper({ - label: 'executeForMetadata', - method: async () => this.executeForMetadata(context), - }), - this.asyncMethodPerformanceMetricWrapper({ - label: 'executeForWorkspaceSchema', - method: async () => this.executeForWorkspaceSchema(context), - }), - ]); + try { + await Promise.all([ + this.asyncMethodPerformanceMetricWrapper({ + label: 'executeForMetadata', + method: async () => this.executeForMetadata(context), + }), + this.asyncMethodPerformanceMetricWrapper({ + label: 'executeForWorkspaceSchema', + method: async () => this.executeForWorkspaceSchema(context), + }), + ]); - return this.optimisticallyApplyActionOnAllFlatEntityMaps({ - action: context.action, - allFlatEntityMaps: context.allFlatEntityMaps, - }); + return this.optimisticallyApplyActionOnAllFlatEntityMaps({ + action: context.action, + allFlatEntityMaps: context.allFlatEntityMaps, + }); + } catch (error) { + this.logger.error(`${this.actionType} execution failed`, error); + throw error; + } } async rollback( diff --git a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts index 7c6a388331..8f5531f6a2 100644 --- a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts @@ -5,6 +5,7 @@ import { getAuthTokensFromLoginToken } from 'test/integration/graphql/utils/get- import { getCurrentUser } from 'test/integration/graphql/utils/get-current-user.util'; import { signUpInNewWorkspace } from 'test/integration/graphql/utils/sign-up-in-new-workspace.util'; import { signUp } from 'test/integration/graphql/utils/sign-up.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; @@ -118,4 +119,128 @@ describe('Successful user and workspace creation', () => { workpsaceCustomApplication.id, ); }); + + it('should delete workspace and related metadata entities when last user is deleted', async () => { + const { data } = await signUp({ + input: { + email: `test-delete-5678@example.com`, + password: 'Test123!@#', + }, + expectToFail: false, + }); + + createdUserAccessToken = + data.signUp.tokens.accessOrWorkspaceAgnosticToken.token; + + const { + data: { signUpInNewWorkspace: signUpInNewWorkspaceData }, + } = await signUpInNewWorkspace({ + accessToken: createdUserAccessToken, + expectToFail: false, + }); + + const workspaceId = signUpInNewWorkspaceData.workspace.id; + + const { + data: { getAuthTokensFromLoginToken: authTokensData }, + } = await getAuthTokensFromLoginToken({ + origin: signUpInNewWorkspaceData.workspace.workspaceUrls.subdomainUrl, + loginToken: signUpInNewWorkspaceData.loginToken.token, + expectToFail: false, + }); + + const newWorkspaceAccessToken = + authTokensData.tokens.accessOrWorkspaceAgnosticToken.token; + + await activateWorkspace({ + accessToken: newWorkspaceAccessToken, + displayName: 'Test Workspace for Deletion', + expectToFail: false, + }); + + await createOneObjectMetadata({ + input: { + nameSingular: 'workspaceEviction', + namePlural: 'workspaceEvictions', + labelPlural: 'whatevers', + labelSingular: 'whatever', + isLabelSyncedWithName: false, + }, + token: newWorkspaceAccessToken, + expectToFail: false, + }); + + const workspaceBeforeDeletion = await testDataSource.query( + 'SELECT * FROM core.workspace WHERE id = $1', + [workspaceId], + ); + + expect(workspaceBeforeDeletion).toHaveLength(1); + + const tablesToVerify = [ + 'dataSource', + 'objectMetadata', + 'fieldMetadata', + 'indexMetadata', + 'searchFieldMetadata', + 'workspaceMigration', + 'role', + 'roleTargets', + 'objectPermission', + 'fieldPermission', + 'permissionFlag', + 'serverlessFunction', + 'serverlessFunctionLayer', + 'agent', + 'agentHandoff', + 'remoteServer', + 'remoteTable', + 'databaseEventTrigger', + 'view', + 'viewField', + 'viewFilter', + 'viewFilterGroup', + 'viewGroup', + 'viewSort', + 'cronTrigger', + 'routeTrigger', + ]; + + let totalRecordsBefore = 0; + + for (const table of tablesToVerify) { + const result = await testDataSource.query( + `SELECT COUNT(*) as count FROM core."${table}" WHERE "workspaceId" = $1`, + [workspaceId], + ); + + totalRecordsBefore += parseInt(result[0].count); + } + + expect(totalRecordsBefore).toBeGreaterThan(0); + + await deleteUser({ + accessToken: createdUserAccessToken, + expectToFail: false, + }); + + createdUserAccessToken = undefined; + + const workspaceAfterDeletion = await testDataSource.query( + 'SELECT * FROM core.workspace WHERE id = $1', + [workspaceId], + ); + + expect(workspaceAfterDeletion).toHaveLength(0); + + for (const table of tablesToVerify) { + const result = await testDataSource.query( + `SELECT COUNT(*) as count FROM core."${table}" WHERE "workspaceId" = $1`, + [workspaceId], + ); + const count = parseInt(result[0].count); + + expect({ count, table }).toEqual({ count: 0, table }); + } + }); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util.ts index e13f915fed..aa827c42e7 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util.ts @@ -14,6 +14,7 @@ export const createOneObjectMetadata = async ({ input, gqlFields, expectToFail = false, + token, }: PerformMetadataQueryParams): CommonResponseBody<{ createOneObject: ObjectMetadataDTO; }> => { @@ -22,7 +23,7 @@ export const createOneObjectMetadata = async ({ gqlFields, }); - const response = await makeMetadataAPIRequest(graphqlOperation); + const response = await makeMetadataAPIRequest(graphqlOperation, token); if (expectToFail === true) { warnIfNoErrorButExpectedToFail({ diff --git a/packages/twenty-server/test/integration/metadata/suites/utils/make-metadata-api-request.util.ts b/packages/twenty-server/test/integration/metadata/suites/utils/make-metadata-api-request.util.ts index 0380f55987..b60d8d5ba7 100644 --- a/packages/twenty-server/test/integration/metadata/suites/utils/make-metadata-api-request.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/utils/make-metadata-api-request.util.ts @@ -6,12 +6,15 @@ type GraphqlOperation = { variables?: Record; }; -export const makeMetadataAPIRequest = (graphqlOperation: GraphqlOperation) => { +export const makeMetadataAPIRequest = ( + graphqlOperation: GraphqlOperation, + token: string = APPLE_JANE_ADMIN_ACCESS_TOKEN, +) => { const client = request(`http://localhost:${APP_PORT}`); return client .post('/metadata') - .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .set('Authorization', `Bearer ${token}`) .send({ query: print(graphqlOperation.query), variables: graphqlOperation.variables || {}, diff --git a/packages/twenty-server/test/integration/metadata/types/perform-metadata-query.type.ts b/packages/twenty-server/test/integration/metadata/types/perform-metadata-query.type.ts index db0ba48267..4a1ad49c96 100644 --- a/packages/twenty-server/test/integration/metadata/types/perform-metadata-query.type.ts +++ b/packages/twenty-server/test/integration/metadata/types/perform-metadata-query.type.ts @@ -2,4 +2,5 @@ export type PerformMetadataQueryParams = { input: T; gqlFields?: string; expectToFail?: boolean | null; + token?: string; };