diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/2-9-workspace-command-1799000040000-sync-call-recording-standard-objects.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/2-9-workspace-command-1799000040000-sync-call-recording-standard-objects.command.ts index 9438c2ec241..c06223ca5a9 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/2-9-workspace-command-1799000040000-sync-call-recording-standard-objects.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/2-9-workspace-command-1799000040000-sync-call-recording-standard-objects.command.ts @@ -10,7 +10,7 @@ import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/c import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; import { - findCollidingCustomCallRecordingObject, + findCollidingCustomCallRecordingObjects, resolveAvailableOldNames, } from 'src/database/commands/upgrade-version-command/2-9/utils/call-recording-name-collision.util'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; @@ -157,17 +157,17 @@ export class SyncCallRecordingStandardObjectsCommand extends ActiveOrSuspendedWo applicationId, workspaceId, now, - renamedCollisionObjectMetadata, + renamedCollisionObjectMetadatas, }: { fromMaps: FlatEntityMaps; flatObjectMetadataMaps: FlatEntityMaps; applicationId: string; workspaceId: string; now: string; - renamedCollisionObjectMetadata?: { + renamedCollisionObjectMetadatas: { universalIdentifier: string; nameSingular: string; - }; + }[]; }): FlatEntityMaps { let toMaps = fromMaps; let nextPosition = @@ -218,7 +218,7 @@ export class SyncCallRecordingStandardObjectsCommand extends ActiveOrSuspendedWo }); } - if (isDefined(renamedCollisionObjectMetadata)) { + for (const renamedCollisionObjectMetadata of renamedCollisionObjectMetadatas) { const renamedNavigationCommandMenuItemUniversalIdentifier = v5( renamedCollisionObjectMetadata.universalIdentifier, NAVIGATION_COMMAND_UUID_NAMESPACE, @@ -294,51 +294,62 @@ export class SyncCallRecordingStandardObjectsCommand extends ActiveOrSuspendedWo 'featureFlagsMap', ]); - let renamedCollisionObjectMetadata: - | { universalIdentifier: string; nameSingular: string } - | undefined; + const renamedCollisionObjectMetadatas: { + universalIdentifier: string; + nameSingular: string; + }[] = []; - const collidingCustomObject = findCollidingCustomCallRecordingObject( + const collidingCustomObjects = findCollidingCustomCallRecordingObjects( flatObjectMetadataMaps, ); - if (isDefined(collidingCustomObject)) { + // Each rename consumes a callRecordingOld name; reserve assigned names so the + // next colliding object falls back to callRecordingOld2, callRecordingOld3, etc. + const reservedOldNames = new Set(); + + for (const collidingCustomObject of collidingCustomObjects) { const { nameSingular, namePlural, labelSingular, labelPlural } = - resolveAvailableOldNames(flatObjectMetadataMaps); + resolveAvailableOldNames(flatObjectMetadataMaps, reservedOldNames); + + reservedOldNames.add(nameSingular); + reservedOldNames.add(namePlural); if (isDryRun) { this.logger.log( `[DRY RUN] Would rename colliding custom object (${collidingCustomObject.nameSingular}) to '${nameSingular}' for workspace ${workspaceId}`, ); - } else { - await this.objectMetadataService.updateOneObject({ - workspaceId, - updateObjectInput: { - id: collidingCustomObject.id, - update: { - nameSingular, - namePlural, - labelSingular, - labelPlural, - isLabelSyncedWithName: false, - }, - }, - }); - - this.logger.log( - `Renamed colliding custom object to '${nameSingular}' for workspace ${workspaceId}`, - ); - - renamedCollisionObjectMetadata = { - universalIdentifier: collidingCustomObject.universalIdentifier, - nameSingular, - }; - - ({ flatObjectMetadataMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatObjectMetadataMaps', - ])); + continue; } + + await this.objectMetadataService.updateOneObject({ + workspaceId, + updateObjectInput: { + id: collidingCustomObject.id, + update: { + nameSingular, + namePlural, + labelSingular, + labelPlural, + isLabelSyncedWithName: false, + }, + }, + }); + + this.logger.log( + `Renamed colliding custom object to '${nameSingular}' for workspace ${workspaceId}`, + ); + + renamedCollisionObjectMetadatas.push({ + universalIdentifier: collidingCustomObject.universalIdentifier, + nameSingular, + }); + } + + if (renamedCollisionObjectMetadatas.length > 0) { + ({ flatObjectMetadataMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatObjectMetadataMaps', + ])); } const { twentyStandardFlatApplication } = @@ -491,7 +502,7 @@ export class SyncCallRecordingStandardObjectsCommand extends ActiveOrSuspendedWo applicationId: twentyStandardFlatApplication.id, workspaceId, now, - renamedCollisionObjectMetadata, + renamedCollisionObjectMetadatas, }); const newEntityCount = [ diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/utils/__tests__/call-recording-name-collision.util.spec.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/utils/__tests__/call-recording-name-collision.util.spec.ts index 3c3bfa5aeb7..51ed42298aa 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/utils/__tests__/call-recording-name-collision.util.spec.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/utils/__tests__/call-recording-name-collision.util.spec.ts @@ -1,7 +1,7 @@ import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; import { - findCollidingCustomCallRecordingObject, + findCollidingCustomCallRecordingObjects, resolveAvailableOldNames, } from 'src/database/commands/upgrade-version-command/2-9/utils/call-recording-name-collision.util'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; @@ -26,8 +26,8 @@ const buildFlatObjectMetadataMaps = ( universalIdentifiersByApplicationId: {}, }); -describe('findCollidingCustomCallRecordingObject', () => { - it('returns undefined when no object uses the callRecording name', () => { +describe('findCollidingCustomCallRecordingObjects', () => { + it('returns an empty array when no object uses the callRecording name', () => { const maps = buildFlatObjectMetadataMaps([ getFlatObjectMetadataMock({ universalIdentifier: 'unrelated-object', @@ -36,7 +36,7 @@ describe('findCollidingCustomCallRecordingObject', () => { }), ]); - expect(findCollidingCustomCallRecordingObject(maps)).toBeUndefined(); + expect(findCollidingCustomCallRecordingObjects(maps)).toEqual([]); }); it('returns the custom object whose singular name collides', () => { @@ -49,8 +49,10 @@ describe('findCollidingCustomCallRecordingObject', () => { ]); expect( - findCollidingCustomCallRecordingObject(maps)?.universalIdentifier, - ).toBe('colliding-singular'); + findCollidingCustomCallRecordingObjects(maps).map( + (object) => object.universalIdentifier, + ), + ).toEqual(['colliding-singular']); }); it('returns the custom object whose plural name collides', () => { @@ -63,8 +65,31 @@ describe('findCollidingCustomCallRecordingObject', () => { ]); expect( - findCollidingCustomCallRecordingObject(maps)?.universalIdentifier, - ).toBe('colliding-plural'); + findCollidingCustomCallRecordingObjects(maps).map( + (object) => object.universalIdentifier, + ), + ).toEqual(['colliding-plural']); + }); + + it('returns every colliding object when singular and plural collide on different objects', () => { + const maps = buildFlatObjectMetadataMaps([ + getFlatObjectMetadataMock({ + universalIdentifier: 'colliding-singular', + nameSingular: 'callRecording', + namePlural: 'myRecordings', + }), + getFlatObjectMetadataMock({ + universalIdentifier: 'colliding-plural', + nameSingular: 'myRecording', + namePlural: 'callRecordings', + }), + ]); + + expect( + findCollidingCustomCallRecordingObjects(maps) + .map((object) => object.universalIdentifier) + .sort(), + ).toEqual(['colliding-plural', 'colliding-singular']); }); it('excludes the standard callRecording object itself', () => { @@ -78,7 +103,7 @@ describe('findCollidingCustomCallRecordingObject', () => { }), ]); - expect(findCollidingCustomCallRecordingObject(maps)).toBeUndefined(); + expect(findCollidingCustomCallRecordingObjects(maps)).toEqual([]); }); }); @@ -122,6 +147,28 @@ describe('resolveAvailableOldNames', () => { }); }); + it('skips Old names provided as already reserved', () => { + const maps = buildFlatObjectMetadataMaps([ + getFlatObjectMetadataMock({ + universalIdentifier: 'colliding', + nameSingular: 'callRecording', + namePlural: 'callRecordings', + }), + ]); + + expect( + resolveAvailableOldNames( + maps, + new Set(['callRecordingOld', 'callRecordingsOld']), + ), + ).toEqual({ + nameSingular: 'callRecordingOld2', + namePlural: 'callRecordingsOld2', + labelSingular: 'Call Recording (Old) 2', + labelPlural: 'Call Recordings (Old) 2', + }); + }); + it('skips a discriminator when only the plural Old name is taken', () => { const maps = buildFlatObjectMetadataMaps([ getFlatObjectMetadataMock({ diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/utils/call-recording-name-collision.util.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/utils/call-recording-name-collision.util.ts index 0b5b6d152f6..b012215fc42 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/utils/call-recording-name-collision.util.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-9/utils/call-recording-name-collision.util.ts @@ -10,10 +10,10 @@ const CALL_RECORDING_OLD_NAME_SINGULAR = 'callRecordingOld'; const CALL_RECORDING_OLD_NAME_PLURAL = 'callRecordingsOld'; const MAX_OLD_NAME_ATTEMPTS = 100; -export const findCollidingCustomCallRecordingObject = ( +export const findCollidingCustomCallRecordingObjects = ( flatObjectMetadataMaps: FlatEntityMaps, -): FlatObjectMetadata | undefined => - Object.values(flatObjectMetadataMaps.byUniversalIdentifier).find( +): FlatObjectMetadata[] => + Object.values(flatObjectMetadataMaps.byUniversalIdentifier).filter( (flatObjectMetadata): flatObjectMetadata is FlatObjectMetadata => isDefined(flatObjectMetadata) && flatObjectMetadata.universalIdentifier !== @@ -27,20 +27,22 @@ export const findCollidingCustomCallRecordingObject = ( export const resolveAvailableOldNames = ( flatObjectMetadataMaps: FlatEntityMaps, + additionalTakenNames: ReadonlySet = new Set(), ): { nameSingular: string; namePlural: string; labelSingular: string; labelPlural: string; } => { - const takenNames = new Set( - Object.values(flatObjectMetadataMaps.byUniversalIdentifier) + const takenNames = new Set([ + ...Object.values(flatObjectMetadataMaps.byUniversalIdentifier) .filter(isDefined) .flatMap((flatObjectMetadata) => [ flatObjectMetadata.nameSingular, flatObjectMetadata.namePlural, ]), - ); + ...additionalTakenNames, + ]); for (let attempt = 0; attempt < MAX_OLD_NAME_ATTEMPTS; attempt++) { const discriminator = attempt === 0 ? '' : `${attempt + 1}`;