fix(call-recording): rename all colliding custom objects, not just the first
This commit is contained in:
+51
-40
@@ -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 = [
|
||||
|
||||
+56
-9
@@ -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({
|
||||
|
||||
+8
-6
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user