fix(call-recording): rename all colliding custom objects, not just the first

This commit is contained in:
ehconitin
2026-06-04 15:10:37 +05:30
parent 426f87dfe6
commit 73e619174f
3 changed files with 115 additions and 55 deletions
@@ -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<FlatCommandMenuItem>;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
applicationId: string;
workspaceId: string;
now: string;
renamedCollisionObjectMetadata?: {
renamedCollisionObjectMetadatas: {
universalIdentifier: string;
nameSingular: string;
};
}[];
}): FlatEntityMaps<FlatCommandMenuItem> {
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<string>();
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 = [
@@ -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({
@@ -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>,
): 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<FlatObjectMetadata>,
additionalTakenNames: ReadonlySet<string> = 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}`;