Skip to content
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
npx nx test twenty-front # Frontend unit tests
npx nx test twenty-server # Backend unit tests
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
# To run an indivual test or a pattern of tests, use the following command:
cd packages/{workspace} && npx jest "pattern or filename"

# Storybook
npx nx storybook:build twenty-front
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { InjectRepository } from '@nestjs/typeorm';

import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';

import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { getSubFlatEntityMapsByApplicationIdsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-ids-or-throw.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 { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';

@Command({
name: 'upgrade:1-18:backfill-message-channel-message-association-message-folder',
description:
'Backfill messageChannelMessageAssociationMessageFolder standard object and its relation fields',
})
export class BackfillMessageChannelMessageAssociationMessageFolderCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}

private addNewEntitiesToFlatEntityMaps<T extends SyncableFlatEntity>({
fromMaps,
standardBuilderMaps,
}: {
fromMaps: FlatEntityMaps<T>;
standardBuilderMaps: FlatEntityMaps<T>;
}): FlatEntityMaps<T> {
let toMaps = fromMaps;

for (const [universalIdentifier, entity] of Object.entries(
standardBuilderMaps.byUniversalIdentifier,
)) {
if (
!isDefined(entity) ||
isDefined(fromMaps.byUniversalIdentifier[universalIdentifier])
) {
continue;
}

toMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: entity,
flatEntityMaps: toMaps,
});
}

return toMaps;
}

override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;

this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of messageChannelMessageAssociationMessageFolder for workspace ${workspaceId}`,
);

const { flatObjectMetadataMaps, flatFieldMetadataMaps, featureFlagsMap } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'featureFlagsMap',
]);

const existingObject = findFlatEntityByUniversalIdentifier({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier:
STANDARD_OBJECTS.messageChannelMessageAssociationMessageFolder
.universalIdentifier,
});

if (existingObject) {
this.logger.log(
`messageChannelMessageAssociationMessageFolder object already exists for workspace ${workspaceId}, skipping`,
);

return;
}

if (isDryRun) {
this.logger.log(
`[DRY RUN] Would create messageChannelMessageAssociationMessageFolder object and relation fields for workspace ${workspaceId}`,
);

return;
}

const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);

const fromFlatObjectMetadataMaps =
getSubFlatEntityMapsByApplicationIdsOrThrow<FlatObjectMetadata>({
applicationIds: [twentyStandardFlatApplication.id],
flatEntityMaps: flatObjectMetadataMaps,
});

const fromFlatFieldMetadataMaps =
getSubFlatEntityMapsByApplicationIdsOrThrow<FlatFieldMetadata>({
applicationIds: [twentyStandardFlatApplication.id],
flatEntityMaps: flatFieldMetadataMaps,
});

const {
allFlatEntityMaps: standardAllFlatEntityMaps,
idByUniversalIdentifierByMetadataName,
} = computeTwentyStandardApplicationAllFlatEntityMaps({
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});

const toFlatObjectMetadataMaps =
this.addNewEntitiesToFlatEntityMaps<FlatObjectMetadata>({
fromMaps: fromFlatObjectMetadataMaps,
standardBuilderMaps: standardAllFlatEntityMaps.flatObjectMetadataMaps,
});

const toFlatFieldMetadataMaps =
this.addNewEntitiesToFlatEntityMaps<FlatFieldMetadata>({
fromMaps: fromFlatFieldMetadataMaps,
standardBuilderMaps: standardAllFlatEntityMaps.flatFieldMetadataMaps,
});

const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo(
{
buildOptions: {
isSystemBuild: true,
},
fromToAllFlatEntityMaps: {
flatObjectMetadataMaps: {
from: fromFlatObjectMetadataMaps,
to: toFlatObjectMetadataMaps,
},
flatFieldMetadataMaps: {
from: fromFlatFieldMetadataMaps,
to: toFlatFieldMetadataMaps,
},
},
workspaceId,
additionalCacheDataMaps: {
featureFlagsMap,
},
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
idByUniversalIdentifierByMetadataName,
},
);

if (isDefined(validateAndBuildResult)) {
this.logger.error(
`Failed to create messageChannelMessageAssociationMessageFolder:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create messageChannelMessageAssociationMessageFolder for workspace ${workspaceId}`,
);
}

this.logger.log(
`Successfully created messageChannelMessageAssociationMessageFolder for workspace ${workspaceId}`,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { BackfillFileSizeAndMimeTypeCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command';
import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-message-association-message-folder.command';
import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command';
import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command';
import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command';
Expand All @@ -15,6 +16,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';

Expand All @@ -33,19 +35,22 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
WorkspaceCacheModule,
FieldMetadataModule,
ApplicationModule,
WorkspaceMigrationModule,
FilesFieldModule,
],
providers: [
MigratePersonAvatarFilesCommand,
MigrateAttachmentFilesCommand,
BackfillFileSizeAndMimeTypeCommand,
MigrateActivityRichTextAttachmentFileIdsCommand,
BackfillMessageChannelMessageAssociationMessageFolderCommand,
],
exports: [
MigratePersonAvatarFilesCommand,
MigrateAttachmentFilesCommand,
BackfillFileSizeAndMimeTypeCommand,
MigrateActivityRichTextAttachmentFileIdsCommand,
BackfillMessageChannelMessageAssociationMessageFolderCommand,
],
})
export class V1_18_UpgradeVersionCommandModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/
import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command';
import { MigrateWorkflowCodeStepsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command';
import { BackfillFileSizeAndMimeTypeCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command';
import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-message-association-message-folder.command';
import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command';
import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command';
import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command';
Expand Down Expand Up @@ -59,6 +60,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly backfillFileSizeAndMimeTypeCommand: BackfillFileSizeAndMimeTypeCommand,
protected readonly migrateAttachmentFilesCommand: MigrateAttachmentFilesCommand,
protected readonly migrateActivityRichTextAttachmentFileIdsCommand: MigrateActivityRichTextAttachmentFileIdsCommand,
protected readonly backfillMessageChannelMessageAssociationMessageFolderCommand: BackfillMessageChannelMessageAssociationMessageFolderCommand,
) {
super(
workspaceRepository,
Expand Down Expand Up @@ -89,6 +91,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.migrateAttachmentFilesCommand,
this.migrateActivityRichTextAttachmentFileIdsCommand,
this.backfillFileSizeAndMimeTypeCommand,
this.backfillMessageChannelMessageAssociationMessageFolderCommand,
];

this.allCommands = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MessagingFolderSyncManagerModule } from 'src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module';

import { AuthResolver } from './auth.resolver';
Expand Down Expand Up @@ -100,6 +100,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
OnboardingModule,
WorkspaceDataSourceModule,
ConnectedAccountModule,
MessagingCommonModule,
MessagingFolderSyncManagerModule,
WorkspaceSSOModule,
FeatureFlagModule,
Expand Down Expand Up @@ -143,7 +144,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
// So far, it's not possible to have controllers in business modules
// which forces us to have these services in the auth module
// TODO: Move these calendar, message, and connected account services to the business modules once possible
MessageChannelSyncStatusService,
CalendarChannelSyncStatusService,
CreateMessageChannelService,
CreateCalendarChannelService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { buildConnectedAccountStandardFlatFieldMetadatas } from 'src/engine/work
import { buildDashboardStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util';
import { buildFavoriteFolderStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-favorite-folder-standard-flat-field-metadata.util';
import { buildFavoriteStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-favorite-standard-flat-field-metadata.util';
import { buildMessageChannelMessageAssociationMessageFolderStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-message-association-message-folder-standard-flat-field-metadata.util';
import { buildMessageChannelMessageAssociationStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-message-association-standard-flat-field-metadata.util';
import { buildMessageChannelStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-standard-flat-field-metadata.util';
import { buildMessageFolderStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-folder-standard-flat-field-metadata.util';
Expand Down Expand Up @@ -58,6 +59,8 @@ const STANDARD_FLAT_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME = {
messageChannel: buildMessageChannelStandardFlatFieldMetadatas,
messageChannelMessageAssociation:
buildMessageChannelMessageAssociationStandardFlatFieldMetadatas,
messageChannelMessageAssociationMessageFolder:
buildMessageChannelMessageAssociationMessageFolderStandardFlatFieldMetadatas,
messageFolder: buildMessageFolderStandardFlatFieldMetadatas,
messageParticipant: buildMessageParticipantStandardFlatFieldMetadatas,
messageThread: buildMessageThreadStandardFlatFieldMetadatas,
Expand Down
Loading
Loading