diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-add-workspace-foreign-keys-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-add-workspace-foreign-keys-migration.command.ts new file mode 100644 index 0000000000..16d6e4109e --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-add-workspace-foreign-keys-migration.command.ts @@ -0,0 +1,68 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { DataSource, Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { addWorkspaceForeignKeysQueries } from 'src/database/typeorm/core/migrations/utils/1767002571103-addWorkspaceForeignKeys.util'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; + +@Command({ + name: 'upgrade:1-16:add-workspace-foreign-keys-migration', + description: 'Add foreign key constraints for workspaceId on core tables', +}) +export class AddWorkspaceForeignKeysMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + private hasRunOnce = false; + + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + @InjectDataSource() + private readonly coreDataSource: DataSource, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + options, + }: RunOnWorkspaceArgs): Promise { + if (this.hasRunOnce) { + this.logger.log( + 'Skipping has already been run once AddWorkspaceForeignKeysMigrationCommand', + ); + + return; + } + + if (options.dryRun) { + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + await addWorkspaceForeignKeysQueries(queryRunner); + + await queryRunner.commitTransaction(); + this.logger.log( + 'Successfully run AddWorkspaceForeignKeysMigrationCommand', + ); + this.hasRunOnce = true; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.log( + `Rollbacking AddWorkspaceForeignKeysMigrationCommand: ${error.message}`, + ); + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts index 0d2ea350ae..1e726e9a29 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { AddWorkspaceForeignKeysMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-add-workspace-foreign-keys-migration.command'; import { BackfillUpdatedByFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-updated-by-field.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -26,7 +27,13 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache WorkspaceCacheModule, ApplicationModule, ], - providers: [BackfillUpdatedByFieldCommand], - exports: [BackfillUpdatedByFieldCommand], + providers: [ + BackfillUpdatedByFieldCommand, + AddWorkspaceForeignKeysMigrationCommand, + ], + exports: [ + BackfillUpdatedByFieldCommand, + AddWorkspaceForeignKeysMigrationCommand, + ], }) export class V1_16_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index a5eb65b348..f86fa11791 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -20,6 +20,7 @@ import { DeleteRemovedAgentsCommand } from 'src/database/commands/upgrade-versio import { UpdateCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/1-14/1-14-update-created-by-enum.command'; import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-fix-nan-position-values-in-notes.command'; import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command'; +import { AddWorkspaceForeignKeysMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-add-workspace-foreign-keys-migration.command'; import { BackfillUpdatedByFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-updated-by-field.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -59,6 +60,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { // 1.16 Commands protected readonly backfillUpdatedByFieldCommand: BackfillUpdatedByFieldCommand, + protected readonly addWorkspaceForeignKeysMigrationCommand: AddWorkspaceForeignKeysMigrationCommand, ) { super( workspaceRepository, @@ -90,7 +92,10 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.fixNanPositionValuesInNotesCommand, ]; - const commands_1160: VersionCommands = [this.backfillUpdatedByFieldCommand]; + const commands_1160: VersionCommands = [ + this.backfillUpdatedByFieldCommand, + this.addWorkspaceForeignKeysMigrationCommand, + ]; this.allCommands = { '1.12.0': commands_1120, diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1767002571103-addWorkspaceForeignKeys.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1767002571103-addWorkspaceForeignKeys.ts index c5a61d8737..d6fbb3aaa3 100644 --- a/packages/twenty-server/src/database/typeorm/core/migrations/common/1767002571103-addWorkspaceForeignKeys.ts +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1767002571103-addWorkspaceForeignKeys.ts @@ -1,59 +1,37 @@ import { type MigrationInterface, type QueryRunner } from 'typeorm'; +import { addWorkspaceForeignKeysQueries } from 'src/database/typeorm/core/migrations/utils/1767002571103-addWorkspaceForeignKeys.util'; + export class AddWorkspaceForeignKeys1767002571103 implements MigrationInterface { name = 'AddWorkspaceForeignKeys1767002571103'; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "core"."indexMetadata" ADD CONSTRAINT "FK_5c988136a6d6f25a100c1064789" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."roleTarget" ADD CONSTRAINT "FK_a86894bed7b7e1cc8b3f1d6186f" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."role" ADD CONSTRAINT "FK_d2532f520d84f8c22ee45681c5a" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."fieldMetadata" ADD CONSTRAINT "FK_9ce5ba7878f498bcf79e447a9a6" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."objectMetadata" ADD CONSTRAINT "FK_d82a05a204136c01388ea80bc7a" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."cronTrigger" ADD CONSTRAINT "FK_058c319eeb9799a4637908ce362" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."databaseEventTrigger" ADD CONSTRAINT "FK_cf158c3199dcf2d52b0da05c33b" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."routeTrigger" ADD CONSTRAINT "FK_5e004929fcf5e67398544b43885" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."serverlessFunction" ADD CONSTRAINT "FK_ef5dde6a681970b9c1e10563498" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."dataSource" ADD CONSTRAINT "FK_e1914827ee8b22fba4254578311" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."objectPermission" ADD CONSTRAINT "FK_edcd87df18d3284141757bf6e16" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."permissionFlag" ADD CONSTRAINT "FK_835bc9f7ef959debfc5cd268049" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."serverlessFunctionLayer" ADD CONSTRAINT "FK_ca0699c3c906e903d7381c6a771" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."workspaceMigration" ADD CONSTRAINT "FK_bebfcf8dcb299fd39ad04ed4c7f" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."remoteTable" ADD CONSTRAINT "FK_30b429b14ddc6aab7495fa884f3" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "core"."remoteServer" ADD CONSTRAINT "FK_d1293835c5a0bb89fc0ed45713f" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); + const savepointName = 'sp_add_workspace_foreign_keys'; + + try { + await queryRunner.query(`SAVEPOINT ${savepointName}`); + + await addWorkspaceForeignKeysQueries(queryRunner); + + await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`); + } catch (e) { + try { + await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`); + await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`); + } catch (rollbackError) { + // eslint-disable-next-line no-console + console.error( + 'Failed to rollback to savepoint in AddWorkspaceForeignKeys1767002571103', + rollbackError, + ); + throw rollbackError; + } + + // eslint-disable-next-line no-console + console.error('Swallowing AddWorkspaceForeignKeys1767002571103 error', e); + } } public async down(queryRunner: QueryRunner): Promise { diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/utils/1767002571103-addWorkspaceForeignKeys.util.ts b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1767002571103-addWorkspaceForeignKeys.util.ts new file mode 100644 index 0000000000..ed6f8d0c16 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1767002571103-addWorkspaceForeignKeys.util.ts @@ -0,0 +1,54 @@ +import { type QueryRunner } from 'typeorm'; + +export const addWorkspaceForeignKeysQueries = async ( + queryRunner: QueryRunner, +): Promise => { + await queryRunner.query( + `ALTER TABLE "core"."indexMetadata" ADD CONSTRAINT "FK_5c988136a6d6f25a100c1064789" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."roleTarget" ADD CONSTRAINT "FK_a86894bed7b7e1cc8b3f1d6186f" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."role" ADD CONSTRAINT "FK_d2532f520d84f8c22ee45681c5a" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" ADD CONSTRAINT "FK_9ce5ba7878f498bcf79e447a9a6" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."objectMetadata" ADD CONSTRAINT "FK_d82a05a204136c01388ea80bc7a" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."cronTrigger" ADD CONSTRAINT "FK_058c319eeb9799a4637908ce362" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."databaseEventTrigger" ADD CONSTRAINT "FK_cf158c3199dcf2d52b0da05c33b" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."routeTrigger" ADD CONSTRAINT "FK_5e004929fcf5e67398544b43885" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunction" ADD CONSTRAINT "FK_ef5dde6a681970b9c1e10563498" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."dataSource" ADD CONSTRAINT "FK_e1914827ee8b22fba4254578311" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."objectPermission" ADD CONSTRAINT "FK_edcd87df18d3284141757bf6e16" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ADD CONSTRAINT "FK_835bc9f7ef959debfc5cd268049" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunctionLayer" ADD CONSTRAINT "FK_ca0699c3c906e903d7381c6a771" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspaceMigration" ADD CONSTRAINT "FK_bebfcf8dcb299fd39ad04ed4c7f" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."remoteTable" ADD CONSTRAINT "FK_30b429b14ddc6aab7495fa884f3" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."remoteServer" ADD CONSTRAINT "FK_d1293835c5a0bb89fc0ed45713f" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); +}; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts index e89f1b6c9d..1eb09c40ed 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts @@ -28,6 +28,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; describe('WorkspaceService', () => { @@ -116,6 +117,12 @@ describe('WorkspaceService', () => { flush: jest.fn(), }, }, + { + provide: WorkspaceDataSourceService, + useValue: { + deleteWorkspaceDBSchema: jest.fn(), + }, + }, { provide: WorkspaceManyOrAllFlatEntityMapsCacheService, 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 70c67e4755..011ce90a24 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 @@ -45,6 +45,7 @@ import { } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags'; import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch'; @@ -95,6 +96,7 @@ export class WorkspaceService extends TypeOrmQueryService { private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly subdomainManagerService: SubdomainManagerService, + private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly customDomainManagerService: CustomDomainManagerService, @InjectMessageQueue(MessageQueue.deleteCascadeQueue) private readonly messageQueueService: MessageQueueService, @@ -276,15 +278,15 @@ export class WorkspaceService extends TypeOrmQueryService { }); } + /** + * @deprecated Should be removed once AddWorkspaceForeignKeysMigrationCommand has been run successfully in production + * As we will be able to rely on foreignKey delete cascading + */ async deleteMetadataSchemaCacheAndUserWorkspace(workspace: WorkspaceEntity) { await this.userWorkspaceService.deleteUserWorkspace({ userWorkspaceId: workspace.id, }); - if (this.billingService.isBillingEnabled()) { - await this.billingSubscriptionService.deleteSubscriptions(workspace.id); - } - await this.workspaceManagerService.delete(workspace.id); return workspace; @@ -323,11 +325,11 @@ export class WorkspaceService extends TypeOrmQueryService { }); this.logger.log(`workspace ${id} cache flushed`); - if (softDelete) { - if (this.billingService.isBillingEnabled()) { - await this.billingSubscriptionService.deleteSubscriptions(workspace.id); - } + if (this.billingService.isBillingEnabled()) { + await this.billingSubscriptionService.deleteSubscriptions(workspace.id); + } + if (softDelete) { await this.workspaceRepository.softDelete({ id }); this.logger.log(`workspace ${id} soft deleted`); @@ -337,6 +339,8 @@ export class WorkspaceService extends TypeOrmQueryService { await this.deleteMetadataSchemaCacheAndUserWorkspace(workspace); + await this.workspaceDataSourceService.deleteWorkspaceDBSchema(workspace.id); + await this.messageQueueService.add( FileWorkspaceFolderDeletionJob.name, { workspaceId: id }, 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 72da403d1c..c37e813420 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 @@ -35,6 +35,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; import { ViewModule } from 'src/engine/metadata-modules/view/view.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; @Module({ @@ -59,6 +60,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m FeatureFlagModule, DataSourceModule, OnboardingModule, + WorkspaceDataSourceModule, TypeORMModule, PermissionsModule, WorkspaceCacheStorageModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity.ts index 471d5dd430..b57ae409a6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity.ts @@ -10,8 +10,8 @@ import { UpdateDateColumn, } from 'typeorm'; -import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; export type CronTriggerSettings = { pattern: string; @@ -40,9 +40,6 @@ export class CronTriggerEntity @Column({ nullable: false, type: 'uuid' }) serverlessFunctionId: string; - @Column({ nullable: false, type: 'uuid' }) - workspaceId: string; - @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity.ts index 79ea16f4c0..3a3d6ad80f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity.ts @@ -10,8 +10,8 @@ import { UpdateDateColumn, } from 'typeorm'; -import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; export type DatabaseEventTriggerSettings = { eventName: string; @@ -40,9 +40,6 @@ export class DatabaseEventTriggerEntity @Column({ nullable: false, type: 'uuid' }) serverlessFunctionId: string; - @Column({ nullable: false, type: 'uuid' }) - workspaceId: string; - @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts index 8dd14a0670..49d2813aaa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts @@ -12,10 +12,10 @@ import { UpdateDateColumn, } from 'typeorm'; -import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; @Unique('IDX_INDEX_METADATA_NAME_WORKSPACE_ID_OBJECT_METADATA_ID_UNIQUE', [ 'name', @@ -43,9 +43,6 @@ export class IndexMetadataEntity @Column({ nullable: false }) name: string; - @Column({ nullable: false, type: 'uuid' }) - workspaceId: string; - @Column({ nullable: false, type: 'uuid' }) objectMetadataId: string; 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 4130900961..fd392a46f1 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 @@ -10,7 +10,6 @@ import { UpdateDateColumn, } from 'typeorm'; -import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -19,6 +18,7 @@ import { type ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/obj import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; @Entity('objectMetadata') @Unique('IDX_OBJECT_METADATA_NAME_SINGULAR_WORKSPACE_ID_UNIQUE', [ @@ -106,10 +106,6 @@ 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; - @OneToMany(() => FieldMetadataEntity, (field) => field.object, { cascade: true, }) diff --git a/packages/twenty-server/src/engine/metadata-modules/role-target/role-target.entity.ts b/packages/twenty-server/src/engine/metadata-modules/role-target/role-target.entity.ts index 15d78846e0..ca883ea35f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role-target/role-target.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role-target/role-target.entity.ts @@ -12,9 +12,9 @@ import { UpdateDateColumn, } from 'typeorm'; -import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; @Entity('roleTarget') @Unique('IDX_ROLE_TARGET_UNIQUE_USER_WORKSPACE', [ @@ -34,9 +34,6 @@ export class RoleTargetEntity extends SyncableEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ nullable: false, type: 'uuid' }) - workspaceId: string; - @Column({ nullable: false, type: 'uuid' }) roleId: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts index cb6e9b5999..4c23775ee2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts @@ -9,11 +9,11 @@ import { UpdateDateColumn, } from 'typeorm'; -import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; @Entity('role') @Unique('IDX_ROLE_LABEL_WORKSPACE_ID_UNIQUE', ['label', 'workspaceId']) @@ -51,9 +51,6 @@ export class RoleEntity extends SyncableEntity implements Required { @Column({ nullable: true, type: 'varchar' }) icon: string | null; - @Column({ nullable: false, type: 'uuid' }) - workspaceId: string; - @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts index 95143acaf9..b818a61e7f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts @@ -13,11 +13,11 @@ import { UpdateDateColumn, } from 'typeorm'; -import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity'; import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity'; import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface'; const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes @@ -63,9 +63,6 @@ export class ServerlessFunctionEntity @Check(`"timeoutSeconds" >= 1 AND "timeoutSeconds" <= 900`) timeoutSeconds: number; - @Column({ nullable: false, type: 'uuid' }) - workspaceId: string; - @Column({ nullable: true, type: 'text' }) checksum: string | null; 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 5aacc97b43..c80d0ce4bd 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 @@ -18,6 +18,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; @@ -31,9 +32,9 @@ describe('WorkspaceManagerService', () => { let service: WorkspaceManagerService; let workspaceMigrationRepository: Repository; let dataSourceRepository: Repository; - let workspaceDataSourceService: WorkspaceDataSourceService; let roleTargetRepository: Repository; let roleRepository: Repository; + let serverlessFunctionRepository: Repository; let mockDataSource: jest.Mocked; let objectMetadataService: ObjectMetadataService; @@ -95,6 +96,12 @@ describe('WorkspaceManagerService', () => { delete: jest.fn(), }, }, + { + provide: getRepositoryToken(ServerlessFunctionEntity), + useValue: { + delete: jest.fn(), + }, + }, { provide: PermissionsService, useValue: {}, @@ -169,15 +176,15 @@ describe('WorkspaceManagerService', () => { dataSourceRepository = module.get>( getRepositoryToken(DataSourceEntity), ); - workspaceDataSourceService = module.get( - WorkspaceDataSourceService, - ); roleTargetRepository = module.get>( getRepositoryToken(RoleTargetEntity), ); roleRepository = module.get>( getRepositoryToken(RoleEntity), ); + serverlessFunctionRepository = module.get< + Repository + >(getRepositoryToken(ServerlessFunctionEntity)); objectMetadataService = module.get( ObjectMetadataService, ); @@ -205,9 +212,9 @@ describe('WorkspaceManagerService', () => { expect(roleRepository.delete).toHaveBeenCalledWith({ workspaceId: 'workspace-id', }); - expect( - workspaceDataSourceService.deleteWorkspaceDBSchema, - ).toHaveBeenCalled(); + expect(serverlessFunctionRepository.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 3ba364f9ce..9d44cbefcf 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 @@ -10,6 +10,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @@ -46,6 +47,7 @@ import { WorkspaceManagerService } from './workspace-manager.service'; FieldMetadataEntity, RoleTargetEntity, RoleEntity, + ServerlessFunctionEntity, ]), ], exports: [WorkspaceManagerService], 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 dcb659b603..6afa4dfec6 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 @@ -17,6 +17,7 @@ import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metada import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @@ -49,6 +50,8 @@ export class WorkspaceManagerService { private readonly roleRepository: Repository, @InjectRepository(RoleTargetEntity) private readonly roleTargetRepository: Repository, + @InjectRepository(ServerlessFunctionEntity) + private readonly serverlessFunctionRepository: Repository, protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly applicationService: ApplicationService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, @@ -182,8 +185,10 @@ export class WorkspaceManagerService { }); } - // TODO investigate why some entities are not on cascade delete - // Are foreign keys correctly applied ? + /** + * @deprecated Should be removed once AddWorkspaceForeignKeysMigrationCommand has been run successfully in production + * As we will be able to rely on foreignKey delete cascading + */ public async delete(workspaceId: string): Promise { await this.roleTargetRepository.delete({ workspaceId, @@ -192,13 +197,16 @@ export class WorkspaceManagerService { workspaceId, }); + await this.serverlessFunctionRepository.delete({ + workspaceId, + }); + await this.objectMetadataService.deleteWorkspaceAllObjectMetadata({ workspaceId, }); await this.workspaceMigrationService.deleteAllWithinWorkspace(workspaceId); await this.dataSourceService.delete(workspaceId); - await this.workspaceDataSourceService.deleteWorkspaceDBSchema(workspaceId); } private async setupDefaultRoles({ diff --git a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation-v1.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation-v1.integration-spec.ts index 40e25a2d37..1cabb2214c 100644 --- a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation-v1.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation-v1.integration-spec.ts @@ -8,10 +8,15 @@ import { getCurrentUser } from 'test/integration/graphql/utils/get-current-user. 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 { createOneCronTrigger } from 'test/integration/metadata/suites/serverless-function/utils/create-one-cron-trigger.util'; +import { createOneDatabaseEventTrigger } from 'test/integration/metadata/suites/serverless-function/utils/create-one-database-event-trigger.util'; +import { createOneRouteTrigger } from 'test/integration/metadata/suites/serverless-function/utils/create-one-route-trigger.util'; +import { createOneServerlessFunction } from 'test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function.util'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; import { isDefined } from 'twenty-shared/utils'; +import { HTTPMethod } from 'twenty-shared/types'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; @@ -206,6 +211,54 @@ describe('Successful user and workspace creation', () => { expectToFail: false, }); + // Create a serverless function with route, cron, and database event triggers + const { + data: { createOneServerlessFunction: serverlessFunction }, + } = await createOneServerlessFunction({ + input: { + name: 'test-function-for-deletion', + description: 'A test serverless function for workspace deletion test', + }, + token: newWorkspaceAccessToken, + expectToFail: false, + }); + + // Create a route trigger for the serverless function + await createOneRouteTrigger({ + input: { + path: '/test-route-for-deletion', + isAuthRequired: true, + httpMethod: HTTPMethod.GET, + serverlessFunctionId: serverlessFunction.id, + }, + token: newWorkspaceAccessToken, + expectToFail: false, + }); + + // Create a cron trigger for the serverless function + await createOneCronTrigger({ + input: { + settings: { + pattern: '0 0 * * *', + }, + serverlessFunctionId: serverlessFunction.id, + }, + token: newWorkspaceAccessToken, + expectToFail: false, + }); + + // Create a database event trigger for the serverless function + await createOneDatabaseEventTrigger({ + input: { + settings: { + eventName: 'company.created', + }, + serverlessFunctionId: serverlessFunction.id, + }, + token: newWorkspaceAccessToken, + expectToFail: false, + }); + const workspaceBeforeDeletion = await testDataSource.query( 'SELECT * FROM core.workspace WHERE id = $1', [workspaceId], 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 2fc70af8fb..e21052e5e5 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 @@ -8,7 +8,12 @@ import { getCurrentUser } from 'test/integration/graphql/utils/get-current-user. 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 { createOneCronTrigger } from 'test/integration/metadata/suites/serverless-function/utils/create-one-cron-trigger.util'; +import { createOneDatabaseEventTrigger } from 'test/integration/metadata/suites/serverless-function/utils/create-one-database-event-trigger.util'; +import { createOneRouteTrigger } from 'test/integration/metadata/suites/serverless-function/utils/create-one-route-trigger.util'; +import { createOneServerlessFunction } from 'test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function.util'; import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { HTTPMethod } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; @@ -188,6 +193,54 @@ describe('Successful user and workspace creation', () => { expectToFail: false, }); + // Create a serverless function with route, cron, and database event triggers + const { + data: { createOneServerlessFunction: serverlessFunction }, + } = await createOneServerlessFunction({ + input: { + name: 'test-function-for-deletion', + description: 'A test serverless function for workspace deletion test', + }, + token: newWorkspaceAccessToken, + expectToFail: false, + }); + + // Create a route trigger for the serverless function + await createOneRouteTrigger({ + input: { + path: '/test-route-for-deletion', + isAuthRequired: true, + httpMethod: HTTPMethod.GET, + serverlessFunctionId: serverlessFunction.id, + }, + token: newWorkspaceAccessToken, + expectToFail: false, + }); + + // Create a cron trigger for the serverless function + await createOneCronTrigger({ + input: { + settings: { + pattern: '0 0 * * *', + }, + serverlessFunctionId: serverlessFunction.id, + }, + token: newWorkspaceAccessToken, + expectToFail: false, + }); + + // Create a database event trigger for the serverless function + await createOneDatabaseEventTrigger({ + input: { + settings: { + eventName: 'company.created', + }, + serverlessFunctionId: serverlessFunction.id, + }, + token: newWorkspaceAccessToken, + expectToFail: false, + }); + const workspaceBeforeDeletion = await testDataSource.query( 'SELECT * FROM core.workspace WHERE id = $1', [workspaceId], diff --git a/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-cron-trigger-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-cron-trigger-query-factory.util.ts new file mode 100644 index 0000000000..07de1b997b --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-cron-trigger-query-factory.util.ts @@ -0,0 +1,27 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/create-cron-trigger.input'; + +export type CreateOneCronTriggerFactoryInput = CreateCronTriggerInput; + +const DEFAULT_CRON_TRIGGER_GQL_FIELDS = ` + id + settings +`; + +export const createOneCronTriggerQueryFactory = ({ + input, + gqlFields = DEFAULT_CRON_TRIGGER_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreateOneCronTrigger($input: CreateCronTriggerInput!) { + createOneCronTrigger(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-cron-trigger.util.ts b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-cron-trigger.util.ts new file mode 100644 index 0000000000..84349c9d79 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-cron-trigger.util.ts @@ -0,0 +1,43 @@ +import { + type CreateOneCronTriggerFactoryInput, + createOneCronTriggerQueryFactory, +} from 'test/integration/metadata/suites/serverless-function/utils/create-one-cron-trigger-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto'; + +export const createOneCronTrigger = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createOneCronTrigger: CronTriggerDTO; +}> => { + const graphqlOperation = createOneCronTriggerQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Cron Trigger creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Cron Trigger creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-database-event-trigger-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-database-event-trigger-query-factory.util.ts new file mode 100644 index 0000000000..6cfadb3b0e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-database-event-trigger-query-factory.util.ts @@ -0,0 +1,28 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/create-database-event-trigger.input'; + +export type CreateOneDatabaseEventTriggerFactoryInput = + CreateDatabaseEventTriggerInput; + +const DEFAULT_DATABASE_EVENT_TRIGGER_GQL_FIELDS = ` + id + settings +`; + +export const createOneDatabaseEventTriggerQueryFactory = ({ + input, + gqlFields = DEFAULT_DATABASE_EVENT_TRIGGER_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreateOneDatabaseEventTrigger($input: CreateDatabaseEventTriggerInput!) { + createOneDatabaseEventTrigger(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-database-event-trigger.util.ts b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-database-event-trigger.util.ts new file mode 100644 index 0000000000..b3c6aae4ef --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-database-event-trigger.util.ts @@ -0,0 +1,44 @@ +import { + type CreateOneDatabaseEventTriggerFactoryInput, + createOneDatabaseEventTriggerQueryFactory, +} from 'test/integration/metadata/suites/serverless-function/utils/create-one-database-event-trigger-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto'; + +export const createOneDatabaseEventTrigger = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createOneDatabaseEventTrigger: DatabaseEventTriggerDTO; +}> => { + const graphqlOperation = createOneDatabaseEventTriggerQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'Database Event Trigger creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Database Event Trigger creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-route-trigger-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-route-trigger-query-factory.util.ts new file mode 100644 index 0000000000..84f6b8c1d6 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-route-trigger-query-factory.util.ts @@ -0,0 +1,29 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/create-route-trigger.input'; + +export type CreateOneRouteTriggerFactoryInput = CreateRouteTriggerInput; + +const DEFAULT_ROUTE_TRIGGER_GQL_FIELDS = ` + id + path + isAuthRequired + httpMethod +`; + +export const createOneRouteTriggerQueryFactory = ({ + input, + gqlFields = DEFAULT_ROUTE_TRIGGER_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreateOneRouteTrigger($input: CreateRouteTriggerInput!) { + createOneRouteTrigger(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-route-trigger.util.ts b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-route-trigger.util.ts new file mode 100644 index 0000000000..f70d796ff8 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-route-trigger.util.ts @@ -0,0 +1,43 @@ +import { + type CreateOneRouteTriggerFactoryInput, + createOneRouteTriggerQueryFactory, +} from 'test/integration/metadata/suites/serverless-function/utils/create-one-route-trigger-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto'; + +export const createOneRouteTrigger = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createOneRouteTrigger: RouteTriggerDTO; +}> => { + const graphqlOperation = createOneRouteTriggerQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Route Trigger creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Route Trigger creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function-query-factory.util.ts new file mode 100644 index 0000000000..dd7cd93cf0 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function-query-factory.util.ts @@ -0,0 +1,34 @@ +import gql from 'graphql-tag'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; + +export type CreateOneServerlessFunctionFactoryInput = + CreateServerlessFunctionInput; + +const DEFAULT_SERVERLESS_FUNCTION_GQL_FIELDS = ` + id + name + description + runtime + latestVersion + publishedVersions + createdAt + updatedAt +`; + +export const createOneServerlessFunctionQueryFactory = ({ + input, + gqlFields = DEFAULT_SERVERLESS_FUNCTION_GQL_FIELDS, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreateOneServerlessFunction($input: CreateServerlessFunctionInput!) { + createOneServerlessFunction(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function.util.ts b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function.util.ts new file mode 100644 index 0000000000..121d5d4218 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function.util.ts @@ -0,0 +1,44 @@ +import { + type CreateOneServerlessFunctionFactoryInput, + createOneServerlessFunctionQueryFactory, +} from 'test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; + +export const createOneServerlessFunction = async ({ + input, + gqlFields, + expectToFail = false, + token, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createOneServerlessFunction: ServerlessFunctionDTO; +}> => { + const graphqlOperation = createOneServerlessFunctionQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'Serverless Function creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Serverless Function creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +};