Revert "feat(messaging): message campaign delivery stats + views" (#22452) (#22627)

Revert "feat(messaging): message campaign delivery stats + views
(#22452)"

This reverts commit 2e1117d442.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22627?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:
Paul Rastoin
2026-07-07 14:17:13 +02:00
committed by GitHub
parent 54aa52d11c
commit bd8bf89653
32 changed files with 721 additions and 2323 deletions
@@ -4,13 +4,16 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { BackfillWorkspaceCustomApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-workspace-command-1782853718000-backfill-workspace-custom-application-registration.command';
import { BackfillSystemUniqueIndexUniversalIdentifierCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-workspace-command-1783093620000-backfill-system-unique-index-universal-identifier.command';
import { BackfillDeterministicFieldUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-workspace-command-1783100000000-backfill-deterministic-field-universal-identifiers.command';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
@Module({
imports: [
@@ -18,16 +21,19 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
TypeOrmModule.forFeature([
WorkspaceEntity,
ApplicationEntity,
FieldMetadataEntity,
IndexMetadataEntity,
]),
WorkspaceIteratorModule,
WorkspaceCacheModule,
WorkspaceMetadataVersionModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
],
providers: [
BackfillWorkspaceCustomApplicationRegistrationCommand,
BackfillSystemUniqueIndexUniversalIdentifierCommand,
BackfillDeterministicFieldUniversalIdentifiersCommand,
],
})
export class V2_19_UpgradeVersionCommandModule {}
@@ -87,9 +87,9 @@ const isMorphOrRelationFieldMetadataType = (type: FieldMetadataType) =>
type === FieldMetadataType.RELATION ||
type === FieldMetadataType.MORPH_RELATION;
@RegisteredWorkspaceCommand('2.20.0', 1783100000000)
@RegisteredWorkspaceCommand('2.19.0', 1783100000000)
@Command({
name: 'upgrade:2-20:backfill-deterministic-field-universal-identifiers',
name: 'upgrade:2-19:backfill-deterministic-field-universal-identifiers',
description:
'Recompute the universal identifier of auto-provisioned field metadata (system fields and default relation fields) to the deterministic getFieldUniversalIdentifier derivation.',
})
@@ -8,7 +8,7 @@ import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/
// (the sequence only runs previous + current versions). down() restores it.
const TABLES = ['objectMetadata', 'fieldMetadata'] as const;
@RegisteredInstanceCommand('2.20.0', 1782991100000)
@RegisteredInstanceCommand('2.20.0', 1825000000000)
export class DropMetadataStandardOverridesColumnFastInstanceCommand
implements FastInstanceCommand
{
@@ -1,30 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { AddMessageCampaignStatFieldsCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783083423000-add-message-campaign-stat-fields.command';
import { CreateMessageListViewCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783083423001-create-message-list-view.command';
import { BackfillDeterministicFieldUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783100000000-backfill-deterministic-field-universal-identifiers.command';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
@Module({
imports: [
ApplicationModule,
TypeOrmModule.forFeature([ApplicationEntity, FieldMetadataEntity]),
WorkspaceIteratorModule,
WorkspaceCacheModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
],
providers: [
AddMessageCampaignStatFieldsCommand,
CreateMessageListViewCommand,
BackfillDeterministicFieldUniversalIdentifiersCommand,
],
})
export class V2_20_UpgradeVersionCommandModule {}
@@ -1,231 +0,0 @@
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
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 { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type TwentyStandardAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/types/twenty-standard-all-flat-entity-maps.type';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { type WorkspaceCacheDataMap } from 'src/engine/workspace-cache/types/workspace-cache-key.type';
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';
const CAMPAIGN = STANDARD_OBJECTS.messageCampaign;
const STAT_FIELD_UNIVERSAL_IDENTIFIERS = [
CAMPAIGN.fields.sentCount.universalIdentifier,
CAMPAIGN.fields.failedCount.universalIdentifier,
CAMPAIGN.fields.bouncedCount.universalIdentifier,
CAMPAIGN.fields.complainedCount.universalIdentifier,
];
// The allMessageCampaigns view is introduced by this feature, so existing
// workspaces have no messageCampaign view at all. Create the view and every one
// of its columns (not just the stat columns) so it materializes end to end.
const CAMPAIGN_VIEW_UNIVERSAL_IDENTIFIER =
CAMPAIGN.views.allMessageCampaigns.universalIdentifier;
const CAMPAIGN_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = Object.values(
CAMPAIGN.views.allMessageCampaigns.viewFields,
).map((viewField) => viewField.universalIdentifier);
@RegisteredWorkspaceCommand('2.20.0', 1783083423000)
@Command({
name: 'upgrade:2-20:add-message-campaign-stat-fields',
description:
'Add the MessageCampaign delivery-stat fields (sent/failed/bounced/complained) and their view columns on existing workspaces',
})
export class AddMessageCampaignStatFieldsCommand 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 {
flatFieldMetadataMaps,
flatObjectMetadataMaps,
flatViewMaps,
flatViewFieldMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatFieldMetadataMaps',
'flatObjectMetadataMaps',
'flatViewMaps',
'flatViewFieldMaps',
]);
const campaignObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: CAMPAIGN.universalIdentifier,
});
if (!isDefined(campaignObjectMetadata)) {
this.logger.log(
`messageCampaign object does not exist for workspace ${workspaceId}, skipping`,
);
return;
}
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const fieldsToCreate = STAT_FIELD_UNIVERSAL_IDENTIFIERS.filter(
(universalIdentifier) =>
!isDefined(
flatFieldMetadataMaps.byUniversalIdentifier[universalIdentifier],
),
).map((universalIdentifier) => {
const standardField =
findFlatEntityByUniversalIdentifier<FlatFieldMetadata>({
flatEntityMaps: standardAllFlatEntityMaps.flatFieldMetadataMaps,
universalIdentifier,
});
if (!isDefined(standardField)) {
throw new Error(
`Standard application is missing messageCampaign field ${universalIdentifier}`,
);
}
return standardField;
});
const viewsToCreate = this.resolveViewsToCreate({
flatViewMaps,
standardAllFlatEntityMaps,
});
const viewFieldsToCreate = CAMPAIGN_VIEW_FIELD_UNIVERSAL_IDENTIFIERS.filter(
(universalIdentifier) =>
!isDefined(flatViewFieldMaps.byUniversalIdentifier[universalIdentifier]),
).map((universalIdentifier) => {
const standardViewField = findFlatEntityByUniversalIdentifier<FlatViewField>(
{
flatEntityMaps: standardAllFlatEntityMaps.flatViewFieldMaps,
universalIdentifier,
},
);
if (!isDefined(standardViewField)) {
throw new Error(
`Standard application is missing messageCampaign view column ${universalIdentifier}`,
);
}
return standardViewField;
});
const hasMetadataChanges =
fieldsToCreate.length > 0 ||
viewsToCreate.length > 0 ||
viewFieldsToCreate.length > 0;
if (isDryRun) {
this.logger.log(
`[DRY RUN] Workspace ${workspaceId}: ${fieldsToCreate.length} field(s), ${viewsToCreate.length} view(s), ${viewFieldsToCreate.length} view column(s)`,
);
return;
}
if (hasMetadataChanges) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
isSystemBuild: true,
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
flatEntityToCreate: fieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: viewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: viewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to add messageCampaign stat fields:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to add messageCampaign stat fields for workspace ${workspaceId}`,
);
}
}
this.logger.log(
`Applied messageCampaign stat fields for workspace ${workspaceId}`,
);
}
private resolveViewsToCreate({
flatViewMaps,
standardAllFlatEntityMaps,
}: {
flatViewMaps: WorkspaceCacheDataMap['flatViewMaps'];
standardAllFlatEntityMaps: TwentyStandardAllFlatEntityMaps;
}): FlatView[] {
if (
isDefined(
flatViewMaps.byUniversalIdentifier[CAMPAIGN_VIEW_UNIVERSAL_IDENTIFIER],
)
) {
return [];
}
const standardView = findFlatEntityByUniversalIdentifier<FlatView>({
flatEntityMaps: standardAllFlatEntityMaps.flatViewMaps,
universalIdentifier: CAMPAIGN_VIEW_UNIVERSAL_IDENTIFIER,
});
if (!isDefined(standardView)) {
throw new Error(
`Standard application is missing messageCampaign view ${CAMPAIGN_VIEW_UNIVERSAL_IDENTIFIER}`,
);
}
return [standardView];
}
}
@@ -1,187 +0,0 @@
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
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 { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type TwentyStandardAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/types/twenty-standard-all-flat-entity-maps.type';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { type WorkspaceCacheDataMap } from 'src/engine/workspace-cache/types/workspace-cache-key.type';
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';
const LIST = STANDARD_OBJECTS.messageList;
const LIST_VIEW_UNIVERSAL_IDENTIFIER =
LIST.views.allMessageLists.universalIdentifier;
const LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = Object.values(
LIST.views.allMessageLists.viewFields,
).map((viewField) => viewField.universalIdentifier);
@RegisteredWorkspaceCommand('2.20.0', 1783083423001)
@Command({
name: 'upgrade:2-20:create-message-list-view',
description:
'Create the MessageList standard view and its columns on existing workspaces',
})
export class CreateMessageListViewCommand 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 { flatObjectMetadataMaps, flatViewMaps, flatViewFieldMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatViewMaps',
'flatViewFieldMaps',
]);
const listObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: LIST.universalIdentifier,
});
if (!isDefined(listObjectMetadata)) {
this.logger.log(
`messageList object does not exist for workspace ${workspaceId}, skipping`,
);
return;
}
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const viewsToCreate = this.resolveViewsToCreate({
flatViewMaps,
standardAllFlatEntityMaps,
});
const viewFieldsToCreate = LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS.filter(
(universalIdentifier) =>
!isDefined(flatViewFieldMaps.byUniversalIdentifier[universalIdentifier]),
).map((universalIdentifier) => {
const standardViewField =
findFlatEntityByUniversalIdentifier<FlatViewField>({
flatEntityMaps: standardAllFlatEntityMaps.flatViewFieldMaps,
universalIdentifier,
});
if (!isDefined(standardViewField)) {
throw new Error(
`Standard application is missing messageList view column ${universalIdentifier}`,
);
}
return standardViewField;
});
if (viewsToCreate.length === 0 && viewFieldsToCreate.length === 0) {
this.logger.log(
`messageList view already exists for workspace ${workspaceId}, skipping`,
);
return;
}
if (isDryRun) {
this.logger.log(
`[DRY RUN] Workspace ${workspaceId}: ${viewsToCreate.length} view(s), ${viewFieldsToCreate.length} view column(s)`,
);
return;
}
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
isSystemBuild: true,
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
allFlatEntityOperationByMetadataName: {
view: {
flatEntityToCreate: viewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: viewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create messageList view:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create messageList view for workspace ${workspaceId}`,
);
}
this.logger.log(`Created messageList view for workspace ${workspaceId}`);
}
private resolveViewsToCreate({
flatViewMaps,
standardAllFlatEntityMaps,
}: {
flatViewMaps: WorkspaceCacheDataMap['flatViewMaps'];
standardAllFlatEntityMaps: TwentyStandardAllFlatEntityMaps;
}): FlatView[] {
if (
isDefined(
flatViewMaps.byUniversalIdentifier[LIST_VIEW_UNIVERSAL_IDENTIFIER],
)
) {
return [];
}
const standardView = findFlatEntityByUniversalIdentifier<FlatView>({
flatEntityMaps: standardAllFlatEntityMaps.flatViewMaps,
universalIdentifier: LIST_VIEW_UNIVERSAL_IDENTIFIER,
});
if (!isDefined(standardView)) {
throw new Error(
`Standard application is missing messageList view ${LIST_VIEW_UNIVERSAL_IDENTIFIER}`,
);
}
return [standardView];
}
}
@@ -1,234 +0,0 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { type WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { AddMessageCampaignStatFieldsCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783083423000-add-message-campaign-stat-fields.command';
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { type WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
jest.mock(
'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant',
);
const computeTwentyStandardApplicationAllFlatEntityMapsMock =
computeTwentyStandardApplicationAllFlatEntityMaps as jest.Mock;
const WORKSPACE_ID = '20202020-0000-0000-0000-000000000001';
const STANDARD_APPLICATION = {
id: '20202020-0000-0000-0000-0000000000aa',
universalIdentifier: '20202020-0000-0000-0000-0000000000bb',
};
const CAMPAIGN = STANDARD_OBJECTS.messageCampaign;
const STAT_FIELD_UNIVERSAL_IDENTIFIERS = [
CAMPAIGN.fields.sentCount.universalIdentifier,
CAMPAIGN.fields.failedCount.universalIdentifier,
CAMPAIGN.fields.bouncedCount.universalIdentifier,
CAMPAIGN.fields.complainedCount.universalIdentifier,
];
const CAMPAIGN_VIEW_UNIVERSAL_IDENTIFIER =
CAMPAIGN.views.allMessageCampaigns.universalIdentifier;
const CAMPAIGN_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = Object.values(
CAMPAIGN.views.allMessageCampaigns.viewFields,
).map((viewField) => viewField.universalIdentifier);
const buildByUniversalIdentifierMap = (universalIdentifiers: string[]) => ({
byUniversalIdentifier: Object.fromEntries(
universalIdentifiers.map((universalIdentifier) => [
universalIdentifier,
{ universalIdentifier },
]),
),
});
describe('AddMessageCampaignStatFieldsCommand', () => {
let command: AddMessageCampaignStatFieldsCommand;
let findApplicationMock: jest.Mock;
let getOrRecomputeMock: jest.Mock;
let validateBuildAndRunWorkspaceMigrationMock: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
findApplicationMock = jest.fn().mockResolvedValue({
twentyStandardFlatApplication: STANDARD_APPLICATION,
});
getOrRecomputeMock = jest.fn();
validateBuildAndRunWorkspaceMigrationMock = jest
.fn()
.mockResolvedValue({ status: 'success' });
computeTwentyStandardApplicationAllFlatEntityMapsMock.mockReturnValue({
allFlatEntityMaps: {
flatFieldMetadataMaps: buildByUniversalIdentifierMap(
STAT_FIELD_UNIVERSAL_IDENTIFIERS,
),
flatViewMaps: buildByUniversalIdentifierMap([
CAMPAIGN_VIEW_UNIVERSAL_IDENTIFIER,
]),
flatViewFieldMaps: buildByUniversalIdentifierMap(
CAMPAIGN_VIEW_FIELD_UNIVERSAL_IDENTIFIERS,
),
},
});
command = new AddMessageCampaignStatFieldsCommand(
{} as WorkspaceIteratorService,
{
findWorkspaceTwentyStandardAndCustomApplicationOrThrow:
findApplicationMock,
} as unknown as ApplicationService,
{
getOrRecompute: getOrRecomputeMock,
} as unknown as WorkspaceCacheService,
{
validateBuildAndRunWorkspaceMigration:
validateBuildAndRunWorkspaceMigrationMock,
} as unknown as WorkspaceMigrationValidateBuildAndRunService,
);
});
const runOnWorkspace = (dryRun = false) =>
command.runOnWorkspace({
workspaceId: WORKSPACE_ID,
options: { dryRun },
index: 0,
total: 1,
});
const mockWorkspaceCache = ({
existingFields = [],
existingViews = [],
existingViewFields = [],
}: {
existingFields?: string[];
existingViews?: string[];
existingViewFields?: string[];
}) => {
getOrRecomputeMock.mockResolvedValue({
flatObjectMetadataMaps: buildByUniversalIdentifierMap([
CAMPAIGN.universalIdentifier,
]),
flatFieldMetadataMaps: buildByUniversalIdentifierMap(existingFields),
flatViewMaps: buildByUniversalIdentifierMap(existingViews),
flatViewFieldMaps: buildByUniversalIdentifierMap(existingViewFields),
});
};
it('creates missing stat fields, the campaign view, and all campaign view fields', async () => {
mockWorkspaceCache({});
await runOnWorkspace();
expect(validateBuildAndRunWorkspaceMigrationMock).toHaveBeenCalledWith({
isSystemBuild: true,
workspaceId: WORKSPACE_ID,
applicationUniversalIdentifier: STANDARD_APPLICATION.universalIdentifier,
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
flatEntityToCreate: expect.arrayContaining(
STAT_FIELD_UNIVERSAL_IDENTIFIERS.map((universalIdentifier) =>
expect.objectContaining({ universalIdentifier }),
),
),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: [
expect.objectContaining({
universalIdentifier: CAMPAIGN_VIEW_UNIVERSAL_IDENTIFIER,
}),
],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: expect.arrayContaining(
CAMPAIGN_VIEW_FIELD_UNIVERSAL_IDENTIFIERS.map(
(universalIdentifier) =>
expect.objectContaining({ universalIdentifier }),
),
),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
});
expect(
validateBuildAndRunWorkspaceMigrationMock.mock.calls[0][0]
.allFlatEntityOperationByMetadataName.fieldMetadata.flatEntityToCreate,
).toHaveLength(STAT_FIELD_UNIVERSAL_IDENTIFIERS.length);
expect(
validateBuildAndRunWorkspaceMigrationMock.mock.calls[0][0]
.allFlatEntityOperationByMetadataName.viewField.flatEntityToCreate,
).toHaveLength(CAMPAIGN_VIEW_FIELD_UNIVERSAL_IDENTIFIERS.length);
});
it('creates only missing pieces when rerun against a partially migrated workspace', async () => {
mockWorkspaceCache({
existingFields: [CAMPAIGN.fields.sentCount.universalIdentifier],
existingViews: [CAMPAIGN_VIEW_UNIVERSAL_IDENTIFIER],
existingViewFields: [
CAMPAIGN.views.allMessageCampaigns.viewFields.subject
.universalIdentifier,
],
});
await runOnWorkspace();
const payload =
validateBuildAndRunWorkspaceMigrationMock.mock.calls[0][0]
.allFlatEntityOperationByMetadataName;
expect(payload.fieldMetadata.flatEntityToCreate).toEqual(
expect.arrayContaining(
STAT_FIELD_UNIVERSAL_IDENTIFIERS.slice(1).map((universalIdentifier) =>
expect.objectContaining({ universalIdentifier }),
),
),
);
expect(payload.fieldMetadata.flatEntityToCreate).toHaveLength(
STAT_FIELD_UNIVERSAL_IDENTIFIERS.length - 1,
);
expect(payload.view.flatEntityToCreate).toEqual([]);
expect(payload.viewField.flatEntityToCreate).toHaveLength(
CAMPAIGN_VIEW_FIELD_UNIVERSAL_IDENTIFIERS.length - 1,
);
});
it('skips the metadata migration when everything already exists', async () => {
mockWorkspaceCache({
existingFields: STAT_FIELD_UNIVERSAL_IDENTIFIERS,
existingViews: [CAMPAIGN_VIEW_UNIVERSAL_IDENTIFIER],
existingViewFields: CAMPAIGN_VIEW_FIELD_UNIVERSAL_IDENTIFIERS,
});
await runOnWorkspace();
expect(validateBuildAndRunWorkspaceMigrationMock).not.toHaveBeenCalled();
});
it('does not write metadata in dry-run mode', async () => {
mockWorkspaceCache({});
await runOnWorkspace(true);
expect(validateBuildAndRunWorkspaceMigrationMock).not.toHaveBeenCalled();
});
it('skips workspaces where the messageCampaign object is absent', async () => {
getOrRecomputeMock.mockResolvedValue({
flatObjectMetadataMaps: buildByUniversalIdentifierMap([]),
flatFieldMetadataMaps: buildByUniversalIdentifierMap([]),
flatViewMaps: buildByUniversalIdentifierMap([]),
flatViewFieldMaps: buildByUniversalIdentifierMap([]),
});
await runOnWorkspace();
expect(findApplicationMock).not.toHaveBeenCalled();
expect(validateBuildAndRunWorkspaceMigrationMock).not.toHaveBeenCalled();
});
});
@@ -1,202 +0,0 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { type WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { CreateMessageListViewCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783083423001-create-message-list-view.command';
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { type WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
jest.mock(
'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant',
);
const computeTwentyStandardApplicationAllFlatEntityMapsMock =
computeTwentyStandardApplicationAllFlatEntityMaps as jest.Mock;
const WORKSPACE_ID = '20202020-0000-0000-0000-000000000001';
const STANDARD_APPLICATION = {
id: '20202020-0000-0000-0000-0000000000aa',
universalIdentifier: '20202020-0000-0000-0000-0000000000bb',
};
const LIST = STANDARD_OBJECTS.messageList;
const LIST_VIEW_UNIVERSAL_IDENTIFIER =
LIST.views.allMessageLists.universalIdentifier;
const LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = Object.values(
LIST.views.allMessageLists.viewFields,
).map((viewField) => viewField.universalIdentifier);
const buildByUniversalIdentifierMap = (universalIdentifiers: string[]) => ({
byUniversalIdentifier: Object.fromEntries(
universalIdentifiers.map((universalIdentifier) => [
universalIdentifier,
{ universalIdentifier },
]),
),
});
describe('CreateMessageListViewCommand', () => {
let command: CreateMessageListViewCommand;
let findApplicationMock: jest.Mock;
let getOrRecomputeMock: jest.Mock;
let validateBuildAndRunWorkspaceMigrationMock: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
findApplicationMock = jest.fn().mockResolvedValue({
twentyStandardFlatApplication: STANDARD_APPLICATION,
});
getOrRecomputeMock = jest.fn();
validateBuildAndRunWorkspaceMigrationMock = jest
.fn()
.mockResolvedValue({ status: 'success' });
computeTwentyStandardApplicationAllFlatEntityMapsMock.mockReturnValue({
allFlatEntityMaps: {
flatViewMaps: buildByUniversalIdentifierMap([
LIST_VIEW_UNIVERSAL_IDENTIFIER,
]),
flatViewFieldMaps: buildByUniversalIdentifierMap(
LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS,
),
},
});
command = new CreateMessageListViewCommand(
{} as WorkspaceIteratorService,
{
findWorkspaceTwentyStandardAndCustomApplicationOrThrow:
findApplicationMock,
} as unknown as ApplicationService,
{
getOrRecompute: getOrRecomputeMock,
} as unknown as WorkspaceCacheService,
{
validateBuildAndRunWorkspaceMigration:
validateBuildAndRunWorkspaceMigrationMock,
} as unknown as WorkspaceMigrationValidateBuildAndRunService,
);
});
const runOnWorkspace = (dryRun = false) =>
command.runOnWorkspace({
workspaceId: WORKSPACE_ID,
options: { dryRun },
index: 0,
total: 1,
});
const mockWorkspaceCache = ({
existingViews = [],
existingViewFields = [],
}: {
existingViews?: string[];
existingViewFields?: string[];
}) => {
getOrRecomputeMock.mockResolvedValue({
flatObjectMetadataMaps: buildByUniversalIdentifierMap([
LIST.universalIdentifier,
]),
flatViewMaps: buildByUniversalIdentifierMap(existingViews),
flatViewFieldMaps: buildByUniversalIdentifierMap(existingViewFields),
});
};
it('creates the message list view and all view fields when missing', async () => {
mockWorkspaceCache({});
await runOnWorkspace();
expect(validateBuildAndRunWorkspaceMigrationMock).toHaveBeenCalledWith({
isSystemBuild: true,
workspaceId: WORKSPACE_ID,
applicationUniversalIdentifier: STANDARD_APPLICATION.universalIdentifier,
allFlatEntityOperationByMetadataName: {
view: {
flatEntityToCreate: [
expect.objectContaining({
universalIdentifier: LIST_VIEW_UNIVERSAL_IDENTIFIER,
}),
],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: expect.arrayContaining(
LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS.map((universalIdentifier) =>
expect.objectContaining({ universalIdentifier }),
),
),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
});
expect(
validateBuildAndRunWorkspaceMigrationMock.mock.calls[0][0]
.allFlatEntityOperationByMetadataName.viewField.flatEntityToCreate,
).toHaveLength(LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS.length);
});
it('creates missing view fields when the view already exists', async () => {
mockWorkspaceCache({
existingViews: [LIST_VIEW_UNIVERSAL_IDENTIFIER],
existingViewFields: [
LIST.views.allMessageLists.viewFields.name.universalIdentifier,
],
});
await runOnWorkspace();
const payload =
validateBuildAndRunWorkspaceMigrationMock.mock.calls[0][0]
.allFlatEntityOperationByMetadataName;
expect(payload.view.flatEntityToCreate).toEqual([]);
expect(payload.viewField.flatEntityToCreate).toEqual(
expect.arrayContaining(
LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS.slice(1).map(
(universalIdentifier) =>
expect.objectContaining({ universalIdentifier }),
),
),
);
expect(payload.viewField.flatEntityToCreate).toHaveLength(
LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS.length - 1,
);
});
it('does nothing when the view and view fields already exist', async () => {
mockWorkspaceCache({
existingViews: [LIST_VIEW_UNIVERSAL_IDENTIFIER],
existingViewFields: LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS,
});
await runOnWorkspace();
expect(validateBuildAndRunWorkspaceMigrationMock).not.toHaveBeenCalled();
});
it('does not write metadata in dry-run mode', async () => {
mockWorkspaceCache({});
await runOnWorkspace(true);
expect(validateBuildAndRunWorkspaceMigrationMock).not.toHaveBeenCalled();
});
it('skips workspaces where the messageList object is absent', async () => {
getOrRecomputeMock.mockResolvedValue({
flatObjectMetadataMaps: buildByUniversalIdentifierMap([]),
flatViewMaps: buildByUniversalIdentifierMap([]),
flatViewFieldMaps: buildByUniversalIdentifierMap([]),
});
await runOnWorkspace();
expect(findApplicationMock).not.toHaveBeenCalled();
expect(validateBuildAndRunWorkspaceMigrationMock).not.toHaveBeenCalled();
});
});
@@ -1,6 +1,6 @@
import { type QueryRunner } from 'typeorm';
import { DropMetadataStandardOverridesColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1782991100000-drop-metadata-standard-overrides-column';
import { DropMetadataStandardOverridesColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1825000000000-drop-metadata-standard-overrides-column';
import { DROP_METADATA_STANDARD_OVERRIDES_COLUMN_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-20/drop-metadata-standard-overrides-column-upgrade-command-name.constant';
import { getRegisteredInstanceCommandMetadata } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
@@ -19,7 +19,7 @@ describe('DropMetadataStandardOverridesColumnFastInstanceCommand', () => {
expect(metadata).toEqual({
version: '2.20.0',
timestamp: 1782991100000,
timestamp: 1825000000000,
type: 'fast',
});
});
@@ -1,4 +1,4 @@
// Registered name of the 2.20 drop command. Wire to @WasRemovedInUpgrade on the
// standardOverrides columns once 2.20 is current.
export const DROP_METADATA_STANDARD_OVERRIDES_COLUMN_UPGRADE_COMMAND_NAME =
'2.20.0_DropMetadataStandardOverridesColumnFastInstanceCommand_1782991100000';
'2.20.0_DropMetadataStandardOverridesColumnFastInstanceCommand_1825000000000';
@@ -93,7 +93,7 @@ import { AddPendingQuestionMessageIdToAgentChatThreadFastInstanceCommand } from
import { AddWorkspaceDiscoverabilityToWorkspaceFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783004140000-add-workspace-discoverability-to-workspace';
import { AddStatusToFileFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783082964705-add-status-to-file';
import { AddPendingMimeCheckToFileFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783094691548-add-pending-mime-check-to-file';
import { DropMetadataStandardOverridesColumnFastInstanceCommand } from './2-20/2-20-instance-command-fast-1782991100000-drop-metadata-standard-overrides-column';
import { DropMetadataStandardOverridesColumnFastInstanceCommand } from './2-20/2-20-instance-command-fast-1825000000000-drop-metadata-standard-overrides-column';
import { AddLogoToApplicationRegistrationFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783069672191-add-logo-to-application-registration';
import { BackfillLogoOnApplicationRegistrationSlowInstanceCommand } from './2-19/2-19-instance-command-slow-1783069673191-backfill-logo-on-application-registration';
import { AddDisplayFieldsToApplicationRegistrationFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783073776590-add-display-fields-to-application-registration';
@@ -20,7 +20,6 @@ import { V2_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
import { V2_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-17/2-17-upgrade-version-command.module';
import { V2_18_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-18/2-18-upgrade-version-command.module';
import { V2_19_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-19/2-19-upgrade-version-command.module';
import { V2_20_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-20/2-20-upgrade-version-command.module';
@Module({
imports: [
@@ -44,7 +43,6 @@ import { V2_20_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
V2_17_UpgradeVersionCommandModule,
V2_18_UpgradeVersionCommandModule,
V2_19_UpgradeVersionCommandModule,
V2_20_UpgradeVersionCommandModule,
],
})
export class WorkspaceCommandProviderModule {}