[BREAKING CHANGE] remove call recording feature flag and backfill upgrade command for existing command menu items navigation command (#22176)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22176?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { SyncCallRecordingNavigationCommandMenuItemAvailabilityExpressionCommand } from 'src/database/commands/upgrade-version-command/2-17/2-17-workspace-command-1801000000000-sync-call-recording-navigation-command-menu-item-availability-expression.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ApplicationModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceIteratorModule,
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
providers: [
|
||||
SyncCallRecordingNavigationCommandMenuItemAvailabilityExpressionCommand,
|
||||
],
|
||||
})
|
||||
export class V2_17_UpgradeVersionCommandModule {}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { Command } from 'nest-commander';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations } from 'src/database/commands/upgrade-version-command/2-17/utils/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@RegisteredWorkspaceCommand('2.17.0', 1801000000000)
|
||||
@Command({
|
||||
name: 'upgrade:2-17:sync-call-recording-navigation-command-menu-item-availability-expression',
|
||||
description:
|
||||
'Remove the retired call recording feature-flag gate from existing CallRecording navigation command menu items',
|
||||
})
|
||||
export class SyncCallRecordingNavigationCommandMenuItemAvailabilityExpressionCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { flatCommandMenuItemMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatCommandMenuItemMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
const commandMenuItemOperations =
|
||||
buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations(
|
||||
{
|
||||
existingFlatCommandMenuItemMaps: flatCommandMenuItemMaps,
|
||||
existingFlatObjectMetadataMaps: flatObjectMetadataMaps,
|
||||
now: new Date().toISOString(),
|
||||
},
|
||||
);
|
||||
|
||||
const commandMenuItemsToUpdate =
|
||||
commandMenuItemOperations.flatEntityToUpdate;
|
||||
|
||||
if (commandMenuItemsToUpdate.length === 0) {
|
||||
this.logger.log(
|
||||
`CallRecording navigation command menu item availability expression already synced for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Syncing ${commandMenuItemsToUpdate.length} CallRecording navigation command menu item availability expression(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
isSystemBuild: true,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
commandMenuItem: commandMenuItemOperations,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to sync CallRecording navigation command menu item availability expression:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to sync CallRecording navigation command menu item availability expression for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully synced ${commandMenuItemsToUpdate.length} CallRecording navigation command menu item availability expression(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
import {
|
||||
buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations,
|
||||
CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION,
|
||||
LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION,
|
||||
} from 'src/database/commands/upgrade-version-command/2-17/utils/build-call-recording-navigation-command-menu-item-availability-expression-sync-operations.util';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { buildNavigationFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
const APPLICATION_ID = 'application-id';
|
||||
const WORKSPACE_ID = 'workspace-id';
|
||||
const CREATED_AT = '2026-06-01T00:00:00.000Z';
|
||||
const NOW = '2026-06-25T00:00:00.000Z';
|
||||
|
||||
const buildFlatCommandMenuItemMaps = (
|
||||
flatCommandMenuItems: FlatCommandMenuItem[],
|
||||
): FlatEntityMaps<FlatCommandMenuItem> => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
flatCommandMenuItems.map((flatCommandMenuItem) => [
|
||||
flatCommandMenuItem.universalIdentifier,
|
||||
flatCommandMenuItem,
|
||||
]),
|
||||
),
|
||||
universalIdentifierById: Object.fromEntries(
|
||||
flatCommandMenuItems.map((flatCommandMenuItem) => [
|
||||
flatCommandMenuItem.id,
|
||||
flatCommandMenuItem.universalIdentifier,
|
||||
]),
|
||||
),
|
||||
universalIdentifiersByApplicationId: {
|
||||
[APPLICATION_ID]: flatCommandMenuItems.map(
|
||||
(flatCommandMenuItem) => flatCommandMenuItem.universalIdentifier,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const buildFlatObjectMetadataMaps = (
|
||||
flatObjectMetadatas: FlatObjectMetadata[],
|
||||
): FlatEntityMaps<FlatObjectMetadata> => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
flatObjectMetadatas.map((flatObjectMetadata) => [
|
||||
flatObjectMetadata.universalIdentifier,
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
),
|
||||
universalIdentifierById: Object.fromEntries(
|
||||
flatObjectMetadatas.map((flatObjectMetadata) => [
|
||||
flatObjectMetadata.id,
|
||||
flatObjectMetadata.universalIdentifier,
|
||||
]),
|
||||
),
|
||||
universalIdentifiersByApplicationId: {
|
||||
[APPLICATION_ID]: flatObjectMetadatas.map(
|
||||
(flatObjectMetadata) => flatObjectMetadata.universalIdentifier,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const buildCallRecordingObjectMetadataMaps = () =>
|
||||
buildFlatObjectMetadataMaps([
|
||||
getFlatObjectMetadataMock({
|
||||
id: 'call-recording-object-metadata-id',
|
||||
universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
|
||||
nameSingular: 'callRecording',
|
||||
namePlural: 'callRecordings',
|
||||
applicationId: APPLICATION_ID,
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
}),
|
||||
]);
|
||||
|
||||
const buildCallRecordingNavigationCommandMenuItem = ({
|
||||
conditionalAvailabilityExpression,
|
||||
}: {
|
||||
conditionalAvailabilityExpression: string | null;
|
||||
}): FlatCommandMenuItem => ({
|
||||
...buildNavigationFlatCommandMenuItem({
|
||||
objectMetadata: {
|
||||
id: 'call-recording-object-metadata-id',
|
||||
universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
|
||||
nameSingular: 'callRecording',
|
||||
shortcut: null,
|
||||
},
|
||||
commandMenuItemId: 'call-recording-navigation-command-menu-item-id',
|
||||
applicationId: APPLICATION_ID,
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
position: 0,
|
||||
now: CREATED_AT,
|
||||
}),
|
||||
conditionalAvailabilityExpression,
|
||||
});
|
||||
|
||||
describe('buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations', () => {
|
||||
it('rewrites the legacy call recording feature-flag gate to the read-permission gate', () => {
|
||||
const legacyCallRecordingNavigationCommandMenuItem =
|
||||
buildCallRecordingNavigationCommandMenuItem({
|
||||
conditionalAvailabilityExpression:
|
||||
LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION,
|
||||
});
|
||||
|
||||
const result =
|
||||
buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations(
|
||||
{
|
||||
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([
|
||||
legacyCallRecordingNavigationCommandMenuItem,
|
||||
]),
|
||||
existingFlatObjectMetadataMaps: buildCallRecordingObjectMetadataMaps(),
|
||||
now: NOW,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.flatEntityToCreate).toHaveLength(0);
|
||||
expect(result.flatEntityToDelete).toHaveLength(0);
|
||||
expect(result.flatEntityToUpdate).toHaveLength(1);
|
||||
expect(result.flatEntityToUpdate[0]).toMatchObject({
|
||||
id: legacyCallRecordingNavigationCommandMenuItem.id,
|
||||
conditionalAvailabilityExpression:
|
||||
CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION,
|
||||
updatedAt: NOW,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update the command menu item when the expression is already synced', () => {
|
||||
const syncedCallRecordingNavigationCommandMenuItem =
|
||||
buildCallRecordingNavigationCommandMenuItem({
|
||||
conditionalAvailabilityExpression:
|
||||
CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION,
|
||||
});
|
||||
|
||||
const result =
|
||||
buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations(
|
||||
{
|
||||
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([
|
||||
syncedCallRecordingNavigationCommandMenuItem,
|
||||
]),
|
||||
existingFlatObjectMetadataMaps: buildCallRecordingObjectMetadataMaps(),
|
||||
now: NOW,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.flatEntityToUpdate).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not update unrelated command menu item availability expressions', () => {
|
||||
const customCallRecordingNavigationCommandMenuItem =
|
||||
buildCallRecordingNavigationCommandMenuItem({
|
||||
conditionalAvailabilityExpression:
|
||||
'targetObjectReadPermissions.callRecording and permissionFlags.DATA_MODEL',
|
||||
});
|
||||
|
||||
const result =
|
||||
buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations(
|
||||
{
|
||||
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([
|
||||
customCallRecordingNavigationCommandMenuItem,
|
||||
]),
|
||||
existingFlatObjectMetadataMaps: buildCallRecordingObjectMetadataMaps(),
|
||||
now: NOW,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.flatEntityToUpdate).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not update when the call recording object metadata is missing', () => {
|
||||
const legacyCallRecordingNavigationCommandMenuItem =
|
||||
buildCallRecordingNavigationCommandMenuItem({
|
||||
conditionalAvailabilityExpression:
|
||||
LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION,
|
||||
});
|
||||
|
||||
const result =
|
||||
buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations(
|
||||
{
|
||||
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([
|
||||
legacyCallRecordingNavigationCommandMenuItem,
|
||||
]),
|
||||
existingFlatObjectMetadataMaps: buildFlatObjectMetadataMaps([]),
|
||||
now: NOW,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.flatEntityToUpdate).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not update when the call recording navigation command menu item is missing', () => {
|
||||
const result =
|
||||
buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations(
|
||||
{
|
||||
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([]),
|
||||
existingFlatObjectMetadataMaps: buildCallRecordingObjectMetadataMaps(),
|
||||
now: NOW,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v5 } from 'uuid';
|
||||
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { NAVIGATION_COMMAND_UUID_NAMESPACE } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatEntityToCreateDeleteUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-to-create-delete-update.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
export const LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION =
|
||||
'featureFlags.IS_CALL_RECORDING_ENABLED and targetObjectReadPermissions.callRecording';
|
||||
|
||||
export const CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION =
|
||||
'targetObjectReadPermissions.callRecording';
|
||||
|
||||
const CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.callRecording.universalIdentifier;
|
||||
|
||||
const CALL_RECORDING_NAVIGATION_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER = v5(
|
||||
CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
NAVIGATION_COMMAND_UUID_NAMESPACE,
|
||||
);
|
||||
|
||||
export const buildCallRecordingNavigationCommandMenuItemAvailabilityExpressionSyncOperations =
|
||||
({
|
||||
existingFlatCommandMenuItemMaps,
|
||||
existingFlatObjectMetadataMaps,
|
||||
now,
|
||||
}: {
|
||||
existingFlatCommandMenuItemMaps: FlatEntityMaps<FlatCommandMenuItem>;
|
||||
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
now: string;
|
||||
}): FlatEntityToCreateDeleteUpdate<'commandMenuItem'> => {
|
||||
const existingCallRecordingObjectMetadata =
|
||||
existingFlatObjectMetadataMaps.byUniversalIdentifier[
|
||||
CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
const existingCallRecordingNavigationCommandMenuItem =
|
||||
existingFlatCommandMenuItemMaps.byUniversalIdentifier[
|
||||
CALL_RECORDING_NAVIGATION_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(existingCallRecordingObjectMetadata) ||
|
||||
!isDefined(existingCallRecordingNavigationCommandMenuItem) ||
|
||||
existingCallRecordingNavigationCommandMenuItem.conditionalAvailabilityExpression !==
|
||||
LEGACY_CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION
|
||||
) {
|
||||
return {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [
|
||||
{
|
||||
...existingCallRecordingNavigationCommandMenuItem,
|
||||
conditionalAvailabilityExpression:
|
||||
CALL_RECORDING_NAVIGATION_AVAILABILITY_EXPRESSION,
|
||||
updatedAt: now,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
+2
@@ -17,6 +17,7 @@ import { V2_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
|
||||
import { V2_14_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-14/2-14-upgrade-version-command.module';
|
||||
import { V2_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-15/2-15-upgrade-version-command.module';
|
||||
import { V2_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-16/2-16-upgrade-version-command.module';
|
||||
import { V2_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-17/2-17-upgrade-version-command.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -37,6 +38,7 @@ import { V2_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
|
||||
V2_14_UpgradeVersionCommandModule,
|
||||
V2_15_UpgradeVersionCommandModule,
|
||||
V2_16_UpgradeVersionCommandModule,
|
||||
V2_17_UpgradeVersionCommandModule,
|
||||
],
|
||||
})
|
||||
export class WorkspaceCommandProviderModule {}
|
||||
|
||||
Reference in New Issue
Block a user