diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 670d2c1d843f..67ec645e5b9a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -18,6 +18,7 @@ import { WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnDrop, WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; @@ -152,7 +153,7 @@ export class FieldMetadataService extends TypeOrmQueryService [ { name: computeObjectTargetTable(createdObjectMetadata), - action: 'create', + action: WorkspaceMigrationTableActionType.CREATE, } satisfies WorkspaceMigrationTableAction, // Add activity target relation { name: computeObjectTargetTable(activityTargetObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -36,7 +37,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(activityTargetObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, @@ -52,7 +53,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( // Add attachment relation { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -66,7 +67,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, @@ -82,7 +83,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( // Add event relation { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -96,7 +97,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, @@ -112,7 +113,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( // Add favorite relation { name: computeObjectTargetTable(favoriteObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -126,7 +127,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(favoriteObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, @@ -141,7 +142,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(createdObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -154,7 +155,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( // This is temporary until we implement mainIdentifier { name: computeObjectTargetTable(createdObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.util.ts index 091b71fc8c31..51bbdb8e39ea 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-workspace-migrations-for-remote-object.util.ts @@ -6,6 +6,7 @@ import { WorkspaceMigrationTableAction, WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnCreate, + WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; @@ -61,7 +62,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( return [ { name: computeObjectTargetTable(activityTargetObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -75,7 +76,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(activityTargetObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -88,7 +89,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(activityTargetObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, @@ -104,7 +105,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( // Add attachment relation { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -118,7 +119,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -131,7 +132,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, @@ -147,7 +148,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( // Add event relation { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -161,7 +162,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -174,7 +175,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, @@ -190,7 +191,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( // Add favorite relation { name: computeObjectTargetTable(favoriteObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -204,7 +205,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(favoriteObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -217,7 +218,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(favoriteObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts index 26287772bc9e..a5a755ad0316 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts @@ -17,7 +17,10 @@ import { CreateRelationInput } from 'src/engine/metadata-modules/relation-metada import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { WorkspaceMigrationColumnActionType } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationTableActionType, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; @@ -171,7 +174,7 @@ export class RelationMetadataService extends TypeOrmQueryService @@ -252,7 +253,7 @@ export class RemoteTableService { [ { name: remoteTableName, - action: 'drop_foreign_table', + action: WorkspaceMigrationTableActionType.DROP_FOREIGN_TABLE, }, ], ); diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts index 667d0c697bf5..6ed2f58babaa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts @@ -80,14 +80,21 @@ export type WorkspaceMigrationColumnAction = { | WorkspaceMigrationCreateComment ); +/** + * Enum values are lowercase to avoid issues with already existing enum values + */ +export enum WorkspaceMigrationTableActionType { + CREATE = 'create', + ALTER = 'alter', + DROP = 'drop', + CREATE_FOREIGN_TABLE = 'create_foreign_table', + DROP_FOREIGN_TABLE = 'drop_foreign_table' +} + export type WorkspaceMigrationTableAction = { name: string; - action: - | 'create' - | 'alter' - | 'drop' - | 'create_foreign_table' - | 'drop_foreign_table'; + newName?: string; + action: WorkspaceMigrationTableActionType; columns?: WorkspaceMigrationColumnAction[]; foreignTable?: WorkspaceMigrationForeignTable; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts index ceecfef00690..d764bbc8d20b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts @@ -11,6 +11,7 @@ import { WorkspaceMigrationColumnActionType, WorkspaceMigrationEntity, WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; @@ -94,7 +95,7 @@ export class WorkspaceMigrationFieldFactory { name: computeObjectTargetTable( originalObjectMetadataMap[fieldMetadata.objectMetadataId], ), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: this.workspaceMigrationFactory.createColumnActions( WorkspaceMigrationColumnActionType.CREATE, fieldMetadata, @@ -132,7 +133,7 @@ export class WorkspaceMigrationFieldFactory { fieldMetadataUpdate.current.objectMetadataId ], ), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: this.workspaceMigrationFactory.createColumnActions( WorkspaceMigrationColumnActionType.ALTER, fieldMetadataUpdate.current, @@ -171,7 +172,7 @@ export class WorkspaceMigrationFieldFactory { name: computeObjectTargetTable( originalObjectMetadataMap[fieldMetadata.objectMetadataId], ), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.DROP, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts index e5839b232e76..3102956c1bff 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts @@ -8,11 +8,17 @@ import { WorkspaceMigrationColumnActionType, WorkspaceMigrationEntity, WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; +export interface ObjectMetadataUpdate { + current: ObjectMetadataEntity; + altered: ObjectMetadataEntity; +} + @Injectable() export class WorkspaceMigrationObjectFactory { constructor( @@ -21,13 +27,35 @@ export class WorkspaceMigrationObjectFactory { async create( objectMetadataCollection: ObjectMetadataEntity[], + action: + | WorkspaceMigrationBuilderAction.CREATE + | WorkspaceMigrationBuilderAction.DELETE, + ): Promise[]>; + + async create( + objectMetadataUpdateCollection: ObjectMetadataUpdate[], + action: WorkspaceMigrationBuilderAction.UPDATE, + ): Promise[]>; + + async create( + objectMetadataCollectionOrObjectMetadataUpdateCollection: + | ObjectMetadataEntity[] + | ObjectMetadataUpdate[], action: WorkspaceMigrationBuilderAction, ): Promise[]> { switch (action) { case WorkspaceMigrationBuilderAction.CREATE: - return this.createObjectMigration(objectMetadataCollection); + return this.createObjectMigration( + objectMetadataCollectionOrObjectMetadataUpdateCollection as ObjectMetadataEntity[], + ); + case WorkspaceMigrationBuilderAction.UPDATE: + return this.updateObjectMigration( + objectMetadataCollectionOrObjectMetadataUpdateCollection as ObjectMetadataUpdate[], + ); case WorkspaceMigrationBuilderAction.DELETE: - return this.deleteObjectMigration(objectMetadataCollection); + return this.deleteObjectMigration( + objectMetadataCollectionOrObjectMetadataUpdateCollection as ObjectMetadataEntity[], + ); default: return []; } @@ -42,7 +70,7 @@ export class WorkspaceMigrationObjectFactory { const migrations: WorkspaceMigrationTableAction[] = [ { name: computeObjectTargetTable(objectMetadata), - action: 'create', + action: WorkspaceMigrationTableActionType.CREATE, }, ]; @@ -53,7 +81,7 @@ export class WorkspaceMigrationObjectFactory { migrations.push({ name: computeObjectTargetTable(objectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: this.workspaceMigrationFactory.createColumnActions( WorkspaceMigrationColumnActionType.CREATE, field, @@ -72,6 +100,40 @@ export class WorkspaceMigrationObjectFactory { return workspaceMigrations; } + private async updateObjectMigration( + objectMetadataUpdateCollection: ObjectMetadataUpdate[], + ): Promise[]> { + const workspaceMigrations: Partial[] = []; + + for (const objectMetadataUpdate of objectMetadataUpdateCollection) { + const oldTableName = computeObjectTargetTable( + objectMetadataUpdate.current, + ); + const newTableName = computeObjectTargetTable( + objectMetadataUpdate.altered, + ); + + if (oldTableName !== newTableName) { + workspaceMigrations.push({ + workspaceId: objectMetadataUpdate.current.workspaceId, + name: generateMigrationName( + `rename-${objectMetadataUpdate.current.nameSingular}`, + ), + isCustom: false, + migrations: [ + { + name: oldTableName, + newName: newTableName, + action: WorkspaceMigrationTableActionType.ALTER, + }, + ], + }); + } + } + + return workspaceMigrations; + } + private async deleteObjectMigration( objectMetadataCollection: ObjectMetadataEntity[], ): Promise[]> { @@ -81,8 +143,7 @@ export class WorkspaceMigrationObjectFactory { const migrations: WorkspaceMigrationTableAction[] = [ { name: computeObjectTargetTable(objectMetadata), - action: 'drop', - columns: [], + action: WorkspaceMigrationTableActionType.DROP, }, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-relation.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-relation.factory.ts index b195479d8ab1..73e8c3ed99d5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-relation.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-relation.factory.ts @@ -7,6 +7,7 @@ import { WorkspaceMigrationColumnActionType, WorkspaceMigrationEntity, WorkspaceMigrationTableAction, + WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { @@ -90,7 +91,7 @@ export class WorkspaceMigrationRelationFactory { const migrations: WorkspaceMigrationTableAction[] = [ { name: computeObjectTargetTable(toObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.DROP_FOREIGN_KEY, @@ -100,7 +101,7 @@ export class WorkspaceMigrationRelationFactory { }, { name: computeObjectTargetTable(toObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, @@ -166,7 +167,7 @@ export class WorkspaceMigrationRelationFactory { const migrations: WorkspaceMigrationTableAction[] = [ { name: computeObjectTargetTable(toObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts index 86792704947d..96fbc52eb09e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts @@ -18,6 +18,7 @@ import { WorkspaceMigrationColumnCreateRelation, WorkspaceMigrationColumnAlter, WorkspaceMigrationColumnDropRelation, + WorkspaceMigrationTableActionType, WorkspaceMigrationForeignTable, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; @@ -118,18 +119,30 @@ export class WorkspaceMigrationRunnerService { tableMigration: WorkspaceMigrationTableAction, ) { switch (tableMigration.action) { - case 'create': + case WorkspaceMigrationTableActionType.CREATE: await this.createTable(queryRunner, schemaName, tableMigration.name); break; - case 'alter': - await this.handleColumnChanges( - queryRunner, - schemaName, - tableMigration.name, - tableMigration?.columns, - ); + case WorkspaceMigrationTableActionType.ALTER: { + if (tableMigration.newName) { + await this.renameTable( + queryRunner, + schemaName, + tableMigration.name, + tableMigration.newName, + ); + } + + if (tableMigration.columns && tableMigration.columns.length > 0) { + await this.handleColumnChanges( + queryRunner, + schemaName, + tableMigration.newName ?? tableMigration.name, + tableMigration.columns, + ); + } break; - case 'drop': + } + case WorkspaceMigrationTableActionType.DROP: await queryRunner.dropTable(`${schemaName}.${tableMigration.name}`); break; case 'create_foreign_table': @@ -179,6 +192,25 @@ export class WorkspaceMigrationRunnerService { `); } + /** + * Rename a table + * @param queryRunner QueryRunner + * @param schemaName string + * @param oldTableName string + * @param newTableName string + */ + private async renameTable( + queryRunner: QueryRunner, + schemaName: string, + oldTableName: string, + newTableName: string, + ) { + await queryRunner.renameTable( + `${schemaName}.${oldTableName}`, + newTableName, + ); + } + /** * Handles column changes for a given migration * diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts index c43d6f9a4bbc..b6d15d677bd5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts @@ -42,27 +42,39 @@ export class SyncWorkspaceMetadataCommand extends CommandRunner { : await this.workspaceService.getWorkspaceIds(); for (const workspaceId of workspaceIds) { - const issues = await this.workspaceHealthService.healthCheck(workspaceId); - - // Security: abort if there are issues. - if (issues.length > 0) { - if (!options.force) { - this.logger.error( - `Workspace contains ${issues.length} issues, aborting.`, - ); - - this.logger.log( - 'If you want to force the migration, use --force flag', + try { + const issues = + await this.workspaceHealthService.healthCheck(workspaceId); + + // Security: abort if there are issues. + if (issues.length > 0) { + if (!options.force) { + this.logger.error( + `Workspace contains ${issues.length} issues, aborting.`, + ); + + this.logger.log( + 'If you want to force the migration, use --force flag', + ); + this.logger.log( + 'Please use `workspace:health` command to check issues and fix them before running this command.', + ); + + return; + } + + this.logger.warn( + `Workspace contains ${issues.length} issues, sync has been forced.`, ); - this.logger.log( - 'Please use `workspace:health` command to check issues and fix them before running this command.', - ); - - return; + } + } catch (error) { + if (!options.force) { + throw error; } this.logger.warn( - `Workspace contains ${issues.length} issues, sync has been forced.`, + `Workspace health check failed with error, but sync has been forced.`, + error, ); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts index bb83fc881dec..c39a440a77c8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface.ts @@ -33,7 +33,9 @@ export interface ComparatorDeleteResult { export type ObjectComparatorResult = | ComparatorSkipResult | ComparatorCreateResult - | ComparatorUpdateResult>; + | ComparatorUpdateResult< + Partial & { id: string } + >; export type FieldComparatorResult = | ComparatorSkipResult diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts index 85b09148fd17..6eb43d92586a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts @@ -1,8 +1,13 @@ import { Injectable, Logger } from '@nestjs/common'; -import { EntityManager, In } from 'typeorm'; +import { + EntityManager, + EntityTarget, + FindOptionsWhere, + In, + ObjectLiteral, +} from 'typeorm'; import { v4 as uuidV4 } from 'uuid'; -import omit from 'lodash.omit'; import { PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; @@ -14,6 +19,8 @@ import { import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage'; +import { FieldMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; +import { ObjectMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory'; @Injectable() export class WorkspaceMetadataUpdaterService { @@ -24,7 +31,7 @@ export class WorkspaceMetadataUpdaterService { storage: WorkspaceSyncStorage, ): Promise<{ createdObjectMetadataCollection: ObjectMetadataEntity[]; - updatedObjectMetadataCollection: ObjectMetadataEntity[]; + updatedObjectMetadataCollection: ObjectMetadataUpdate[]; }> { const objectMetadataRepository = manager.getRepository(ObjectMetadataEntity); @@ -56,10 +63,17 @@ export class WorkspaceMetadataUpdaterService { /** * Update object metadata */ - const updatedObjectMetadataCollection = await objectMetadataRepository.save( - storage.objectMetadataUpdateCollection.map((objectMetadata) => - omit(objectMetadata, ['fields']), - ), + const updatedObjectMetadataCollection = await this.updateEntities( + manager, + ObjectMetadataEntity, + storage.objectMetadataUpdateCollection, + [ + 'fields', + 'dataSourceId', + 'workspaceId', + 'labelIdentifierFieldMetadataId', + 'imageIdentifierFieldMetadataId', + ], ); /** @@ -108,10 +122,7 @@ export class WorkspaceMetadataUpdaterService { storage: WorkspaceSyncStorage, ): Promise<{ createdFieldMetadataCollection: FieldMetadataEntity[]; - updatedFieldMetadataCollection: { - current: FieldMetadataEntity; - altered: FieldMetadataEntity; - }[]; + updatedFieldMetadataCollection: FieldMetadataUpdate[]; }> { const fieldMetadataRepository = manager.getRepository(FieldMetadataEntity); @@ -127,41 +138,12 @@ export class WorkspaceMetadataUpdaterService { /** * Update field metadata */ - const oldFieldMetadataCollection = await fieldMetadataRepository.findBy({ - id: In(storage.fieldMetadataUpdateCollection.map((field) => field.id)), - }); - // Pre-process old collection into a mapping for quick access - const oldFieldMetadataMap = new Map( - oldFieldMetadataCollection.map((field) => [field.id, field]), - ); - // Combine old and new field metadata to get whole updated entities - const fieldMetadataUpdateCollection = - storage.fieldMetadataUpdateCollection.map((updateFieldMetadata) => { - const oldFieldMetadata = oldFieldMetadataMap.get( - updateFieldMetadata.id, - ); - - if (!oldFieldMetadata) { - throw new Error(` - Field ${updateFieldMetadata.id} not found in oldFieldMetadataCollection`); - } - - // TypeORM 😢 - // If we didn't provide the old value, it will be set to null fields that are not in the updateFieldMetadata - // and override the old value with null in the DB. - // Also save method doesn't return the whole entity if you give a partial one. - // https://github.com/typeorm/typeorm/issues/3490 - // To avoid calling update in a for loop, we did this hack. - return { - ...omit(oldFieldMetadata, ['objectMetadataId', 'workspaceId']), - ...omit(updateFieldMetadata, ['objectMetadataId', 'workspaceId']), - options: updateFieldMetadata.options ?? oldFieldMetadata.options, - }; - }); - - const updatedFieldMetadataCollection = await fieldMetadataRepository.save( - fieldMetadataUpdateCollection, - ); + const updatedFieldMetadataCollection = await this.updateEntities< + FieldMetadataEntity<'default'> + >(manager, FieldMetadataEntity, storage.objectMetadataUpdateCollection, [ + 'objectMetadataId', + 'workspaceId', + ]); /** * Delete field metadata @@ -183,28 +165,7 @@ export class WorkspaceMetadataUpdaterService { return { createdFieldMetadataCollection: createdFieldMetadataCollection as FieldMetadataEntity[], - updatedFieldMetadataCollection: updatedFieldMetadataCollection.map( - (alteredFieldMetadata) => { - const oldFieldMetadata = oldFieldMetadataMap.get( - alteredFieldMetadata.id, - ); - - if (!oldFieldMetadata) { - throw new Error(` - Field ${alteredFieldMetadata.id} not found in oldFieldMetadataCollection - `); - } - - return { - current: oldFieldMetadata as FieldMetadataEntity, - altered: { - ...alteredFieldMetadata, - objectMetadataId: oldFieldMetadata.objectMetadataId, - workspaceId: oldFieldMetadata.workspaceId, - } as FieldMetadataEntity, - }; - }, - ), + updatedFieldMetadataCollection, }; } @@ -267,4 +228,83 @@ export class WorkspaceMetadataUpdaterService { updatedRelationMetadataCollection, }; } + + /** + * Update entities in the database + * @param manager EntityManager + * @param entityClass Entity class + * @param updateCollection Update collection + * @param keysToOmit keys to omit in the merge process + * @returns Promise<{ current: Entity; altered: Entity }[]> + */ + private async updateEntities( + manager: EntityManager, + entityClass: EntityTarget, + updateCollection: Array< + DeepPartial> & { id: string } + >, + keysToOmit: (keyof Entity)[] = [], + ): Promise<{ current: Entity; altered: Entity }[]> { + const repository = manager.getRepository(entityClass); + + const oldEntities = await repository.findBy({ + id: In(updateCollection.map((updateItem) => updateItem.id)), + } as FindOptionsWhere); + + // Pre-process old collection into a mapping for quick access + const oldEntitiesMap = new Map( + oldEntities.map((oldEntity) => [oldEntity.id, oldEntity]), + ); + + // Combine old and new field metadata to get whole updated entities + const entityUpdateCollection = updateCollection.map((updateItem) => { + const oldEntity = oldEntitiesMap.get(updateItem.id); + + if (!oldEntity) { + throw new Error(` + Entity ${updateItem.id} not found in oldEntities`); + } + + // TypeORM 😢 + // If we didn't provide the old value, it will be set to null objects that are not in the updateObjectMetadata + // and override the old value with null in the DB. + // Also save method doesn't return the whole entity if you give a partial one. + // https://github.com/typeorm/typeorm/issues/3490 + // To avoid calling update in a for loop, we did this hack. + const mergedUpdate = { + ...oldEntity, + ...updateItem, + }; + + // Omit keys that we don't want to override + keysToOmit.forEach((key) => { + delete mergedUpdate[key]; + }); + + return mergedUpdate; + }); + + const updatedEntities = await repository.save(entityUpdateCollection); + + return updatedEntities.map((updatedEntity) => { + const oldEntity = oldEntitiesMap.get(updatedEntity.id); + + if (!oldEntity) { + throw new Error(` + Entity ${updatedEntity.id} not found in oldEntitiesMap + `); + } + + return { + current: oldEntity, + altered: { + ...updatedEntity, + ...keysToOmit.reduce( + (acc, key) => ({ ...acc, [key]: oldEntity[key] }), + {}, + ), + }, + }; + }); + } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts index 4707c7c45e04..147389b18918 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts @@ -154,6 +154,12 @@ export class WorkspaceSyncObjectMetadataService { WorkspaceMigrationBuilderAction.CREATE, ); + const updateObjectWorkspaceMigrations = + await this.workspaceMigrationObjectFactory.create( + metadataObjectUpdaterResult.updatedObjectMetadataCollection, + WorkspaceMigrationBuilderAction.UPDATE, + ); + const deleteObjectWorkspaceMigrations = await this.workspaceMigrationObjectFactory.create( storage.objectMetadataDeleteCollection, @@ -164,6 +170,7 @@ export class WorkspaceSyncObjectMetadataService { return [ ...createObjectWorkspaceMigrations, + ...updateObjectWorkspaceMigrations, ...deleteObjectWorkspaceMigrations, ]; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts index 9c3904054643..4aa5c5a08694 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage.ts @@ -10,8 +10,9 @@ export class WorkspaceSyncStorage { // Object metadata private readonly _objectMetadataCreateCollection: ComputedPartialObjectMetadata[] = []; - private readonly _objectMetadataUpdateCollection: Partial[] = - []; + private readonly _objectMetadataUpdateCollection: (Partial & { + id: string; + })[] = []; private readonly _objectMetadataDeleteCollection: ObjectMetadataEntity[] = []; // Field metadata @@ -72,7 +73,9 @@ export class WorkspaceSyncStorage { this._objectMetadataCreateCollection.push(object); } - addUpdateObjectMetadata(object: Partial) { + addUpdateObjectMetadata( + object: Partial & { id: string }, + ) { this._objectMetadataUpdateCollection.push(object); }