[DO_NOT_RELEASE_MAIN_UNTIL_MERGED] Prevent migration failure due to workspace orphan metadata rows (#16863)
# Introduction The `AddWorkspaceForeignKeys1767002571103` migration would fail when released in production right now, as `foreignKey` be applicable as there's a lof of orphan entries in database As a workaround in order not to block any patch release we're fallbacking the migration using save point and an upgrade command that will attempt to apply the `foreignKey` on every workspace upgrade until it succeed We should keep in mind that any new fresh self installation will have the foreignKey double checked that it would not implies regression on workspace deletion using the integration tests ## Cleaning upgrade command We won't implement the cleaning command in this PR yet either will I as discussed with @Weiko someone else might be taking the subject starting next week <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Strengthens workspace data integrity and makes the FK migration resilient. > > - Adds `upgrade:1-16:add-workspace-foreign-keys-migration` command to apply `workspaceId` FKs once per run; wires into `V1_16_UpgradeVersionCommandModule` and 1.16 upgrade sequence > - Refactors migration `1767002571103` to use `addWorkspaceForeignKeysQueries` util and wrap in a savepoint, swallowing errors to avoid blocking releases > - Extracts FK DDL into `utils/1767002571103-addWorkspaceForeignKeys.util` for reuse by command and migration > - Removes duplicate `workspaceId` columns from entities (e.g., `cronTrigger`, `databaseEventTrigger`, `indexMetadata`, `objectMetadata`, `roleTarget`, `role`, `serverlessFunction`) relying on `SyncableEntity`; keeps indexes/relations > - Marks legacy delete paths as deprecated; temporarily extends `WorkspaceManagerService.delete` to also delete `serverlessFunction` by `workspaceId` > - Updates wiring to inject `ServerlessFunctionEntity` repository in `workspace-manager` module/service and corresponding unit test > - Extends integration tests and adds GraphQL helpers to create serverless functions and triggers; verifies cascade deletion of related metadata on workspace removal > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6805bf5d1b32828b4bb1e9f130bfe6e478f66aee. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This commit is contained in:
+14
-7
@@ -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<WorkspaceMigrationEntity>;
|
||||
let dataSourceRepository: Repository<DataSourceEntity>;
|
||||
let workspaceDataSourceService: WorkspaceDataSourceService;
|
||||
let roleTargetRepository: Repository<RoleTargetEntity>;
|
||||
let roleRepository: Repository<RoleEntity>;
|
||||
let serverlessFunctionRepository: Repository<ServerlessFunctionEntity>;
|
||||
let mockDataSource: jest.Mocked<DataSource>;
|
||||
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<Repository<DataSourceEntity>>(
|
||||
getRepositoryToken(DataSourceEntity),
|
||||
);
|
||||
workspaceDataSourceService = module.get<WorkspaceDataSourceService>(
|
||||
WorkspaceDataSourceService,
|
||||
);
|
||||
roleTargetRepository = module.get<Repository<RoleTargetEntity>>(
|
||||
getRepositoryToken(RoleTargetEntity),
|
||||
);
|
||||
roleRepository = module.get<Repository<RoleEntity>>(
|
||||
getRepositoryToken(RoleEntity),
|
||||
);
|
||||
serverlessFunctionRepository = module.get<
|
||||
Repository<ServerlessFunctionEntity>
|
||||
>(getRepositoryToken(ServerlessFunctionEntity));
|
||||
objectMetadataService = module.get<ObjectMetadataService>(
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<RoleEntity>,
|
||||
@InjectRepository(RoleTargetEntity)
|
||||
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
|
||||
@InjectRepository(ServerlessFunctionEntity)
|
||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||
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<void> {
|
||||
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({
|
||||
|
||||
Reference in New Issue
Block a user