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 f090e1d6460f2..4cdffdebe8b08 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 { generateTargetColumnMap } from 'src/engine/metadata-modules/field-metadata/utils/generate-target-column-map.util'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; @@ -153,7 +154,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, @@ -37,7 +38,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(activityTargetObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, @@ -54,7 +55,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( // Add attachment relation { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -69,7 +70,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, @@ -86,7 +87,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( // Add event relation { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -101,7 +102,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, @@ -118,7 +119,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( // Add favorite relation { name: computeObjectTargetTable(favoriteObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -133,7 +134,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(favoriteObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, @@ -149,7 +150,7 @@ export const buildWorkspaceMigrationsForCustomObject = ( }, { name: computeObjectTargetTable(createdObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -162,7 +163,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 320a65aee82ae..2cd1504f02473 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 @@ -5,6 +5,7 @@ import { WorkspaceMigrationTableAction, WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnCreate, + WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { computeCustomName } from 'src/engine/utils/compute-custom-name.util'; 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, @@ -76,7 +77,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(activityTargetObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -90,7 +91,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(activityTargetObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, @@ -106,7 +107,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( // Add attachment relation { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -121,7 +122,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -135,7 +136,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(attachmentObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, @@ -151,7 +152,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( // Add event relation { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -166,7 +167,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -180,7 +181,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(eventObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE_COMMENT, @@ -196,7 +197,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( // Add favorite relation { name: computeObjectTargetTable(favoriteObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -211,7 +212,7 @@ export const buildWorkspaceMigrationsForRemoteObject = async ( }, { name: computeObjectTargetTable(favoriteObjectMetadata), - action: 'alter', + action: WorkspaceMigrationTableActionType.ALTER, columns: [ { action: WorkspaceMigrationColumnActionType.CREATE, @@ -225,7 +226,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 b502772be1c7f..e6d49602ce0b0 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 { createCustomColumnName } from 'src/engine/utils/create-custom-column-name.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; @@ -181,7 +184,7 @@ export class RelationMetadataService extends TypeOrmQueryService[]>; + + 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 b195479d8ab16..73e8c3ed99d58 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 5c645a260e457..75afa11142844 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, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { WorkspaceMigrationEnumService } from 'src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service'; @@ -117,18 +118,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; default: @@ -165,6 +178,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 c43d6f9a4bbc6..b6d15d677bd57 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 bb83fc881dec3..c39a440a77c8f 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 85b09148fd170..6eb43d92586ac 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 4707c7c45e04b..147389b18918a 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/standard-objects/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts index f0eba612c0eae..0f4ad06a84017 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts @@ -1,6 +1,6 @@ import { ActivityTargetObjectMetadata } from 'src/modules/activity/standard-objects/activity-target.object-metadata'; import { ActivityObjectMetadata } from 'src/modules/activity/standard-objects/activity.object-metadata'; -import { ApiKeyObjectMetadata } from 'src/modules/api-key/standard-objects/api-key.object-metadata'; +import { ApiTokenObjectMetadata } from 'src/modules/api-key/standard-objects/api-key.object-metadata'; import { AttachmentObjectMetadata } from 'src/modules/attachment/standard-objects/attachment.object-metadata'; import { BlocklistObjectMetadata } from 'src/modules/connected-account/standard-objects/blocklist.object-metadata'; import { CalendarEventObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-event.object-metadata'; @@ -29,7 +29,7 @@ import { EventObjectMetadata } from 'src/modules/event/standard-objects/event.ob export const standardObjectMetadataDefinitions = [ ActivityTargetObjectMetadata, ActivityObjectMetadata, - ApiKeyObjectMetadata, + ApiTokenObjectMetadata, AttachmentObjectMetadata, BlocklistObjectMetadata, CommentObjectMetadata, 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 9c39040546430..4aa5c5a086949 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); } diff --git a/packages/twenty-server/src/modules/api-key/standard-objects/api-key.object-metadata.ts b/packages/twenty-server/src/modules/api-key/standard-objects/api-key.object-metadata.ts index cc62e90a6c7d3..acd18ecc0407a 100644 --- a/packages/twenty-server/src/modules/api-key/standard-objects/api-key.object-metadata.ts +++ b/packages/twenty-server/src/modules/api-key/standard-objects/api-key.object-metadata.ts @@ -9,14 +9,14 @@ import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync- @ObjectMetadata({ standardId: standardObjectIds.apiKey, - namePlural: 'apiKeys', - labelSingular: 'Api Key', - labelPlural: 'Api Keys', - description: 'An api key', + namePlural: 'apiTokens', + labelSingular: 'Api token', + labelPlural: 'Api tokens', + description: 'An api token', icon: 'IconRobot', }) @IsSystem() -export class ApiKeyObjectMetadata extends BaseObjectMetadata { +export class ApiTokenObjectMetadata extends BaseObjectMetadata { @FieldMetadata({ standardId: apiKeyStandardFieldIds.name, type: FieldMetadataType.TEXT,