Messages Message Folder Association (#17398)
This PR adds Message folder association for message channel messages, Currently under testing phase, not ready yet. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+188
@@ -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-19: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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.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: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
DataSourceModule,
|
||||
WorkspaceCacheModule,
|
||||
ApplicationModule,
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
providers: [BackfillMessageChannelMessageAssociationMessageFolderCommand],
|
||||
exports: [BackfillMessageChannelMessageAssociationMessageFolderCommand],
|
||||
})
|
||||
export class V1_19_UpgradeVersionCommandModule {}
|
||||
+2
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { V1_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module';
|
||||
import { V1_18_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-18/1-18-upgrade-version-command.module';
|
||||
import { V1_19_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-19/1-19-upgrade-version-command.module';
|
||||
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
@@ -12,6 +13,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
V1_17_UpgradeVersionCommandModule,
|
||||
V1_18_UpgradeVersionCommandModule,
|
||||
V1_19_UpgradeVersionCommandModule,
|
||||
DataSourceModule,
|
||||
],
|
||||
providers: [UpgradeCommand],
|
||||
|
||||
+9
@@ -27,6 +27,7 @@ import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/comma
|
||||
import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command';
|
||||
import { MigrateWorkflowSendEmailAttachmentsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command';
|
||||
import { MigrateWorkspacePicturesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command';
|
||||
import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
@@ -67,6 +68,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly backfillStandardViewsAndFieldMetadataCommand: BackfillStandardViewsAndFieldMetadataCommand,
|
||||
protected readonly migrateWorkspacePicturesCommand: MigrateWorkspacePicturesCommand,
|
||||
protected readonly migrateWorkflowSendEmailAttachmentsCommand: MigrateWorkflowSendEmailAttachmentsCommand,
|
||||
|
||||
// 1.19 Commands
|
||||
protected readonly backfillMessageChannelMessageAssociationMessageFolderCommand: BackfillMessageChannelMessageAssociationMessageFolderCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -103,10 +107,15 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.backfillStandardViewsAndFieldMetadataCommand,
|
||||
];
|
||||
|
||||
const commands_1190: VersionCommands = [
|
||||
this.backfillMessageChannelMessageAssociationMessageFolderCommand,
|
||||
];
|
||||
|
||||
this.allCommands = {
|
||||
'1.16.0': commands_1160,
|
||||
'1.17.0': commands_1170,
|
||||
'1.18.0': commands_1180,
|
||||
'1.19.0': commands_1190,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,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';
|
||||
@@ -101,6 +101,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
OnboardingModule,
|
||||
WorkspaceDataSourceModule,
|
||||
ConnectedAccountModule,
|
||||
MessagingCommonModule,
|
||||
MessagingFolderSyncManagerModule,
|
||||
WorkspaceSSOModule,
|
||||
FeatureFlagModule,
|
||||
@@ -145,7 +146,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,
|
||||
|
||||
+861
-831
File diff suppressed because it is too large
Load Diff
+3
@@ -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';
|
||||
@@ -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,
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
DateDisplayFormat,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
RelationType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardFieldArgs,
|
||||
createStandardFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
|
||||
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
|
||||
|
||||
export const buildMessageChannelMessageAssociationMessageFolderStandardFlatFieldMetadatas =
|
||||
({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardFieldArgs<
|
||||
'messageChannelMessageAssociationMessageFolder',
|
||||
FieldMetadataType
|
||||
>,
|
||||
'context'
|
||||
>): Record<
|
||||
AllStandardObjectFieldName<'messageChannelMessageAssociationMessageFolder'>,
|
||||
FlatFieldMetadata
|
||||
> => ({
|
||||
id: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Id',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: 'uuid',
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
createdAt: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Creation date',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: 'now',
|
||||
settings: { displayFormat: DateDisplayFormat.RELATIVE },
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
updatedAt: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Last update',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: 'now',
|
||||
settings: { displayFormat: DateDisplayFormat.RELATIVE },
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
deletedAt: createStandardFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Deleted at',
|
||||
description: 'Date when the record was deleted',
|
||||
icon: 'IconCalendarMinus',
|
||||
isNullable: true,
|
||||
isUIReadOnly: true,
|
||||
settings: { displayFormat: DateDisplayFormat.RELATIVE },
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageChannelMessageAssociation: createStandardRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageChannelMessageAssociation',
|
||||
label: 'Message Channel Message Association',
|
||||
description: 'Message Channel Message Association',
|
||||
icon: 'IconMessage',
|
||||
isNullable: false,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageChannelMessageAssociation',
|
||||
targetFieldName: 'messageFolders',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'messageChannelMessageAssociationId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageFolder: createStandardRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageFolder',
|
||||
label: 'Message Folder',
|
||||
description: 'Message Folder',
|
||||
icon: 'IconFolder',
|
||||
isNullable: false,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageFolder',
|
||||
targetFieldName: 'messageChannelMessageAssociationMessageFolders',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'messageFolderId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
});
|
||||
+23
@@ -248,4 +248,27 @@ export const buildMessageChannelMessageAssociationStandardFlatFieldMetadatas =
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageFolders: createStandardRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageFolders',
|
||||
label: 'Message Folders',
|
||||
description: 'Message Folders (supports multiple folders/labels)',
|
||||
icon: 'IconFolders',
|
||||
isNullable: true,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageChannelMessageAssociationMessageFolder',
|
||||
targetFieldName: 'messageChannelMessageAssociation',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
});
|
||||
|
||||
+25
@@ -255,4 +255,29 @@ export const buildMessageFolderStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageChannelMessageAssociationMessageFolders:
|
||||
createStandardRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
type: FieldMetadataType.RELATION,
|
||||
morphId: null,
|
||||
fieldName: 'messageChannelMessageAssociationMessageFolders',
|
||||
label: 'Message Association Folders',
|
||||
description:
|
||||
'Message Association Folders (supports multiple folders/labels)',
|
||||
icon: 'IconFolders',
|
||||
isNullable: true,
|
||||
isUIReadOnly: true,
|
||||
targetObjectName: 'messageChannelMessageAssociationMessageFolder',
|
||||
targetFieldName: 'messageFolder',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
});
|
||||
|
||||
+3
@@ -12,6 +12,7 @@ import { buildCompanyStandardFlatIndexMetadatas } from 'src/engine/workspace-man
|
||||
import { buildConnectedAccountStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-connected-account-standard-flat-index-metadata.util';
|
||||
import { buildDashboardStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-dashboard-standard-flat-index-metadata.util';
|
||||
import { buildFavoriteStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-favorite-standard-flat-index-metadata.util';
|
||||
import { buildMessageChannelMessageAssociationMessageFolderStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-channel-message-association-message-folder-standard-flat-index-metadata.util';
|
||||
import { buildMessageChannelMessageAssociationStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-channel-message-association-standard-flat-index-metadata.util';
|
||||
import { buildMessageChannelStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-channel-standard-flat-index-metadata.util';
|
||||
import { buildMessageFolderStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-folder-standard-flat-index-metadata.util';
|
||||
@@ -51,6 +52,8 @@ const STANDARD_FLAT_INDEX_METADATA_BUILDERS_BY_OBJECT_NAME = {
|
||||
messageChannel: buildMessageChannelStandardFlatIndexMetadatas,
|
||||
messageChannelMessageAssociation:
|
||||
buildMessageChannelMessageAssociationStandardFlatIndexMetadatas,
|
||||
messageChannelMessageAssociationMessageFolder:
|
||||
buildMessageChannelMessageAssociationMessageFolderStandardFlatIndexMetadatas,
|
||||
messageFolder: buildMessageFolderStandardFlatIndexMetadatas,
|
||||
messageParticipant: buildMessageParticipantStandardFlatIndexMetadatas,
|
||||
note: buildNoteStandardFlatIndexMetadatas,
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type AllStandardObjectIndexName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-index-name.type';
|
||||
import {
|
||||
type CreateStandardIndexArgs,
|
||||
createStandardIndexFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/index/create-standard-index-flat-metadata.util';
|
||||
|
||||
export const buildMessageChannelMessageAssociationMessageFolderStandardFlatIndexMetadatas =
|
||||
({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardIndexArgs<'messageChannelMessageAssociationMessageFolder'>,
|
||||
'context'
|
||||
>): Record<
|
||||
AllStandardObjectIndexName<'messageChannelMessageAssociationMessageFolder'>,
|
||||
FlatIndexMetadata
|
||||
> => ({
|
||||
messageChannelMessageAssociationIdIndex: createStandardIndexFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'messageChannelMessageAssociationIdIndex',
|
||||
relatedFieldNames: ['messageChannelMessageAssociation'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageFolderIdIndex: createStandardIndexFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'messageFolderIdIndex',
|
||||
relatedFieldNames: ['messageFolder'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageChannelMessageAssociationIdMessageFolderIdUniqueIndex:
|
||||
createStandardIndexFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName:
|
||||
'messageChannelMessageAssociationIdMessageFolderIdUniqueIndex',
|
||||
relatedFieldNames: [
|
||||
'messageChannelMessageAssociation',
|
||||
'messageFolder',
|
||||
],
|
||||
isUnique: true,
|
||||
indexWhereClause: '"deletedAt" IS NULL',
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
});
|
||||
+33
@@ -416,6 +416,39 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageChannelMessageAssociationMessageFolder: ({
|
||||
now,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
twentyStandardApplicationId,
|
||||
dependencyFlatEntityMaps,
|
||||
}: Omit<
|
||||
CreateStandardObjectArgs<'messageChannelMessageAssociationMessageFolder'>,
|
||||
'context' | 'objectName'
|
||||
>) =>
|
||||
createStandardObjectFlatMetadata({
|
||||
objectName: 'messageChannelMessageAssociationMessageFolder',
|
||||
dependencyFlatEntityMaps,
|
||||
context: {
|
||||
universalIdentifier:
|
||||
STANDARD_OBJECTS.messageChannelMessageAssociationMessageFolder
|
||||
.universalIdentifier,
|
||||
nameSingular: 'messageChannelMessageAssociationMessageFolder',
|
||||
namePlural: 'messageChannelMessageAssociationMessageFolders',
|
||||
labelSingular: 'Message Channel Message Association Message Folder',
|
||||
labelPlural: 'Message Channel Message Association Message Folders',
|
||||
description:
|
||||
'Join table linking message channel message associations to message folders',
|
||||
icon: 'IconFolder',
|
||||
isSystem: true,
|
||||
isAuditLogged: false,
|
||||
labelIdentifierFieldMetadataName: 'id',
|
||||
},
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
messageParticipant: ({
|
||||
now,
|
||||
workspaceId,
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
|
||||
import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
|
||||
export class MessageChannelMessageAssociationMessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
messageChannelMessageAssociation: EntityRelation<MessageChannelMessageAssociationWorkspaceEntity>;
|
||||
messageChannelMessageAssociationId: string;
|
||||
messageFolder: EntityRelation<MessageFolderWorkspaceEntity>;
|
||||
messageFolderId: string;
|
||||
}
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
|
||||
import { type MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
|
||||
import { type MessageChannelMessageAssociationMessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association-message-folder.workspace-entity';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
@@ -12,4 +13,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
|
||||
messageChannelId: string;
|
||||
message: EntityRelation<MessageWorkspaceEntity> | null;
|
||||
messageId: string;
|
||||
messageFolders: EntityRelation<
|
||||
MessageChannelMessageAssociationMessageFolderWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
+4
@@ -2,6 +2,7 @@ import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
|
||||
import { type MessageChannelMessageAssociationMessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association-message-folder.workspace-entity';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
export enum MessageFolderPendingSyncAction {
|
||||
@@ -23,4 +24,7 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
externalId: string | null;
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
messageChannelId: string;
|
||||
messageChannelMessageAssociationMessageFolders: EntityRelation<
|
||||
MessageChannelMessageAssociationMessageFolderWorkspaceEntity[]
|
||||
>;
|
||||
}
|
||||
|
||||
+1
@@ -84,6 +84,7 @@ export const parseAndFormatGmailMessage = (
|
||||
participants,
|
||||
text: sanitizeString(textWithoutReplyQuotations),
|
||||
attachments,
|
||||
messageFolderExternalIds: labelIds,
|
||||
labelIds,
|
||||
};
|
||||
};
|
||||
|
||||
+14
-8
@@ -44,13 +44,11 @@ export class ImapGetMessagesService {
|
||||
const client = await this.imapClientProvider.getClient(connectedAccount);
|
||||
|
||||
try {
|
||||
const messages = await this.fetchFromAllFolders(
|
||||
return await this.fetchFromAllFolders(
|
||||
messagesByFolder,
|
||||
client,
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
return messages;
|
||||
} finally {
|
||||
await this.imapClientProvider.closeClient(client);
|
||||
}
|
||||
@@ -112,11 +110,16 @@ export class ImapGetMessagesService {
|
||||
);
|
||||
const startTime = Date.now();
|
||||
|
||||
const results = await this.messageParser.parseMessagesFromFolder(
|
||||
messageUids,
|
||||
folderPath,
|
||||
client,
|
||||
);
|
||||
const { messages: results, uidValidity } =
|
||||
await this.messageParser.parseMessagesFromFolder(
|
||||
messageUids,
|
||||
folderPath,
|
||||
client,
|
||||
);
|
||||
|
||||
const folderExternalId = uidValidity
|
||||
? `${folderPath}:${uidValidity}`
|
||||
: folderPath;
|
||||
|
||||
const messages: MessageWithParticipants[] = [];
|
||||
|
||||
@@ -141,6 +144,7 @@ export class ImapGetMessagesService {
|
||||
result.parsed,
|
||||
result.uid,
|
||||
folderPath,
|
||||
folderExternalId,
|
||||
connectedAccount,
|
||||
),
|
||||
);
|
||||
@@ -157,6 +161,7 @@ export class ImapGetMessagesService {
|
||||
parsed: ParsedMail,
|
||||
uid: number,
|
||||
folderPath: string,
|
||||
folderExternalId: string,
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
'handle' | 'handleAliases'
|
||||
@@ -179,6 +184,7 @@ export class ImapGetMessagesService {
|
||||
direction: computeMessageDirection(senderAddress, connectedAccount),
|
||||
attachments: this.extractAttachments(parsed),
|
||||
participants: this.extractParticipants(parsed),
|
||||
messageFolderExternalIds: [folderExternalId],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -9,6 +9,11 @@ export type MessageParseResult = {
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
export type FolderParseResult = {
|
||||
messages: MessageParseResult[];
|
||||
uidValidity: bigint | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ImapMessageParserService {
|
||||
private readonly logger = new Logger(ImapMessageParserService.name);
|
||||
@@ -17,14 +22,19 @@ export class ImapMessageParserService {
|
||||
messageUids: number[],
|
||||
folderPath: string,
|
||||
client: ImapFlow,
|
||||
): Promise<MessageParseResult[]> {
|
||||
): Promise<FolderParseResult> {
|
||||
if (!messageUids.length) {
|
||||
return [];
|
||||
return { messages: [], uidValidity: null };
|
||||
}
|
||||
|
||||
const lock = await client.getMailboxLock(folderPath);
|
||||
|
||||
try {
|
||||
const uidValidity =
|
||||
client.mailbox && typeof client.mailbox !== 'boolean'
|
||||
? client.mailbox.uidValidity
|
||||
: null;
|
||||
|
||||
const uidSet = messageUids.join(',');
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -52,13 +62,16 @@ export class ImapMessageParserService {
|
||||
`Fetched and parsed ${results.length} messages from ${folderPath} in ${Date.now() - startTime}ms`,
|
||||
);
|
||||
|
||||
return results;
|
||||
return { messages: results, uidValidity };
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to parse messages from folder ${folderPath}: ${error.message}`,
|
||||
);
|
||||
|
||||
return this.createErrorResults(messageUids, error as Error);
|
||||
return {
|
||||
messages: this.createErrorResults(messageUids, error as Error),
|
||||
uidValidity: null,
|
||||
};
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
|
||||
+9
@@ -109,6 +109,9 @@ describe('Microsoft get messages service', () => {
|
||||
},
|
||||
],
|
||||
attachments: [],
|
||||
messageFolderExternalIds: responseExample1.body.parentFolderId
|
||||
? [responseExample1.body.parentFolderId]
|
||||
: [],
|
||||
});
|
||||
|
||||
const responseExample2 =
|
||||
@@ -152,6 +155,9 @@ describe('Microsoft get messages service', () => {
|
||||
},
|
||||
],
|
||||
attachments: [],
|
||||
messageFolderExternalIds: responseExample2.body.parentFolderId
|
||||
? [responseExample2.body.parentFolderId]
|
||||
: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,6 +197,9 @@ describe('Microsoft get messages service', () => {
|
||||
},
|
||||
],
|
||||
attachments: [],
|
||||
messageFolderExternalIds: responseExample.body.parentFolderId
|
||||
? [responseExample.body.parentFolderId]
|
||||
: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
-1
@@ -1,8 +1,8 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type EmailAddress } from 'addressparser';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MessageParticipantRole } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
|
||||
@@ -151,6 +151,9 @@ export class MicrosoftGetMessagesService {
|
||||
: MessageDirection.INCOMING,
|
||||
participants,
|
||||
attachments: [],
|
||||
messageFolderExternalIds: response.parentFolderId
|
||||
? [response.parentFolderId]
|
||||
: [],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
+2
@@ -42,6 +42,7 @@ import { MessagingDeleteGroupEmailMessagesService } from 'src/modules/messaging/
|
||||
import { MessagingGetMessageListService } from 'src/modules/messaging/message-import-manager/services/messaging-get-message-list.service';
|
||||
import { MessagingGetMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-get-messages.service';
|
||||
import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
|
||||
import { MessagingMessageFolderAssociationService } from 'src/modules/messaging/message-import-manager/services/messaging-message-folder-association.service';
|
||||
import { MessagingMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service';
|
||||
import { MessagingMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-message.service';
|
||||
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
|
||||
@@ -93,6 +94,7 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
|
||||
MessagingMessageImportManagerMessageChannelListener,
|
||||
MessagingCleanCacheJob,
|
||||
MessagingMessageService,
|
||||
MessagingMessageFolderAssociationService,
|
||||
MessagingMessageListFetchService,
|
||||
MessagingMessagesImportService,
|
||||
MessagingSaveMessagesAndEnqueueContactCreationService,
|
||||
|
||||
+69
-28
@@ -1,12 +1,17 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import chunk from 'lodash.chunk';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type MessageChannelMessageAssociationMessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association-message-folder.workspace-entity';
|
||||
import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
|
||||
import { MessagingGetMessageListService } from 'src/modules/messaging/message-import-manager/services/messaging-get-message-list.service';
|
||||
|
||||
const BATCH_SIZE = 200;
|
||||
|
||||
@Injectable()
|
||||
export class MessagingDeleteFolderMessagesService {
|
||||
@@ -16,7 +21,7 @@ export class MessagingDeleteFolderMessagesService {
|
||||
|
||||
constructor(
|
||||
private readonly messagingMessageCleanerService: MessagingMessageCleanerService,
|
||||
private readonly messagingGetMessageListService: MessagingGetMessageListService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
async deleteFolderMessages(
|
||||
@@ -28,45 +33,81 @@ export class MessagingDeleteFolderMessagesService {
|
||||
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${messageFolder.id} - Deleting messages from folder: ${messageFolder.name}`,
|
||||
);
|
||||
|
||||
const messageLists =
|
||||
await this.messagingGetMessageListService.getMessageLists(
|
||||
messageChannel,
|
||||
[messageFolder],
|
||||
);
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
let totalDeletedCount = 0;
|
||||
|
||||
for (const messageList of messageLists) {
|
||||
const { messageExternalIds } = messageList;
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const messageFolderAssociationRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelMessageAssociationMessageFolderWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannelMessageAssociationMessageFolder',
|
||||
);
|
||||
|
||||
if (messageExternalIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const messageChannelMessageAssociationRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannelMessageAssociation',
|
||||
);
|
||||
|
||||
const messageExternalIdsChunks = chunk(messageExternalIds, 200);
|
||||
let hasMoreData = true;
|
||||
|
||||
for (const messageExternalIdsChunk of messageExternalIdsChunks) {
|
||||
const validExternalIds = messageExternalIdsChunk.filter(isDefined);
|
||||
while (hasMoreData) {
|
||||
const folderAssociations =
|
||||
await messageFolderAssociationRepository.find({
|
||||
where: {
|
||||
messageFolderId: messageFolder.id,
|
||||
},
|
||||
take: BATCH_SIZE,
|
||||
});
|
||||
|
||||
if (validExternalIds.length === 0) {
|
||||
if (folderAssociations.length === 0) {
|
||||
hasMoreData = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.messagingMessageCleanerService.deleteMessagesChannelMessageAssociationsAndRelatedOrphans(
|
||||
{
|
||||
workspaceId,
|
||||
messageExternalIds: validExternalIds,
|
||||
messageChannelId: messageChannel.id,
|
||||
},
|
||||
const folderAssociationIds = folderAssociations.map(
|
||||
(folderAssociation) => folderAssociation.id,
|
||||
);
|
||||
|
||||
totalDeletedCount += validExternalIds.length;
|
||||
|
||||
this.logger.debug(
|
||||
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${messageFolder.id} - Processed ${validExternalIds.length} message deletions`,
|
||||
const messageChannelMessageAssociationIds = folderAssociations.map(
|
||||
(folderAssociation) =>
|
||||
folderAssociation.messageChannelMessageAssociationId,
|
||||
);
|
||||
|
||||
const associations =
|
||||
await messageChannelMessageAssociationRepository.find({
|
||||
where: {
|
||||
id: In(messageChannelMessageAssociationIds),
|
||||
messageChannelId: messageChannel.id,
|
||||
},
|
||||
});
|
||||
|
||||
const messageExternalIds = associations
|
||||
.map((association) => association.messageExternalId)
|
||||
.filter(isDefined);
|
||||
|
||||
this.logger.log(
|
||||
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${messageFolder.id} - Deleting ${messageExternalIds.length} messages`,
|
||||
);
|
||||
|
||||
if (messageExternalIds.length > 0) {
|
||||
await this.messagingMessageCleanerService.deleteMessagesChannelMessageAssociationsAndRelatedOrphans(
|
||||
{
|
||||
workspaceId,
|
||||
messageExternalIds,
|
||||
messageChannelId: messageChannel.id,
|
||||
},
|
||||
);
|
||||
|
||||
totalDeletedCount += messageExternalIds.length;
|
||||
}
|
||||
|
||||
await messageFolderAssociationRepository.delete({
|
||||
id: In(folderAssociationIds),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, authContext);
|
||||
|
||||
this.logger.log(
|
||||
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${messageFolder.id} - Completed deleting ${totalDeletedCount} messages from folder: ${messageFolder.name}`,
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
import { MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
|
||||
import { type MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
|
||||
|
||||
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type MessageChannelMessageAssociationMessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association-message-folder.workspace-entity';
|
||||
|
||||
export type MessageChannelMessageAssociationFolderAssociation = {
|
||||
messageChannelMessageAssociationId: string;
|
||||
messageFolderIds: string[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MessagingMessageFolderAssociationService {
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
async saveMessageFolderAssociations(
|
||||
associations: MessageChannelMessageAssociationFolderAssociation[],
|
||||
workspaceId: string,
|
||||
transactionManager?: WorkspaceEntityManager,
|
||||
): Promise<void> {
|
||||
if (associations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
const repository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<MessageChannelMessageAssociationMessageFolderWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'messageChannelMessageAssociationMessageFolder',
|
||||
);
|
||||
|
||||
const records = associations.flatMap((association) =>
|
||||
association.messageFolderIds.map((folderId) => ({
|
||||
messageChannelMessageAssociationId:
|
||||
association.messageChannelMessageAssociationId,
|
||||
messageFolderId: folderId,
|
||||
})),
|
||||
);
|
||||
|
||||
if (records.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const associationIds = [
|
||||
...new Set(
|
||||
records.map((record) => record.messageChannelMessageAssociationId),
|
||||
),
|
||||
];
|
||||
|
||||
const existingRecords = await repository.find(
|
||||
{
|
||||
where: {
|
||||
messageChannelMessageAssociationId: In(associationIds),
|
||||
},
|
||||
},
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const existingKeys = new Set(
|
||||
existingRecords.map(
|
||||
(record) =>
|
||||
`${record.messageChannelMessageAssociationId}:${record.messageFolderId}`,
|
||||
),
|
||||
);
|
||||
|
||||
const recordsToInsert = records.filter(
|
||||
(record) =>
|
||||
!existingKeys.has(
|
||||
`${record.messageChannelMessageAssociationId}:${record.messageFolderId}`,
|
||||
),
|
||||
);
|
||||
|
||||
if (recordsToInsert.length > 0) {
|
||||
await repository.insert(recordsToInsert, transactionManager);
|
||||
}
|
||||
}, authContext);
|
||||
}
|
||||
}
|
||||
+24
-2
@@ -28,6 +28,7 @@ type MessageAccumulator = {
|
||||
threadToCreate?: Pick<MessageThreadWorkspaceEntity, 'id'>;
|
||||
messageChannelMessageAssociationToCreate?: Pick<
|
||||
MessageChannelMessageAssociationWorkspaceEntity,
|
||||
| 'id'
|
||||
| 'messageChannelId'
|
||||
| 'messageId'
|
||||
| 'messageExternalId'
|
||||
@@ -51,6 +52,10 @@ export class MessagingMessageService {
|
||||
): Promise<{
|
||||
createdMessages: Partial<MessageWorkspaceEntity>[];
|
||||
messageExternalIdsAndIdsMap: Map<string, string>;
|
||||
messageExternalIdToMessageChannelMessageAssociationIdMap: Map<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
}> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
@@ -176,15 +181,16 @@ export class MessagingMessageService {
|
||||
)
|
||||
) {
|
||||
messageAccumulator.messageChannelMessageAssociationToCreate = {
|
||||
id: v4(),
|
||||
messageChannelId,
|
||||
messageId: newOrExistingMessageId,
|
||||
messageExternalId: message.externalId,
|
||||
messageThreadExternalId: message.messageThreadExternalId,
|
||||
direction: message.direction,
|
||||
};
|
||||
|
||||
messageAccumulatorMap.set(message.externalId, messageAccumulator);
|
||||
}
|
||||
|
||||
messageAccumulatorMap.set(message.externalId, messageAccumulator);
|
||||
}
|
||||
|
||||
const messageThreadsToCreate = Array.from(
|
||||
@@ -219,6 +225,8 @@ export class MessagingMessageService {
|
||||
);
|
||||
|
||||
const messageExternalIdsAndIdsMap = new Map<string, string>();
|
||||
const messageExternalIdToMessageChannelMessageAssociationIdMap =
|
||||
new Map<string, string>();
|
||||
|
||||
for (const [
|
||||
externalId,
|
||||
@@ -237,11 +245,25 @@ export class MessagingMessageService {
|
||||
accumulator.existingMessageInDB.id,
|
||||
);
|
||||
}
|
||||
|
||||
const createdAssociationId =
|
||||
accumulator.messageChannelMessageAssociationToCreate?.id;
|
||||
const existingAssociationId =
|
||||
accumulator.existingMessageChannelMessageAssociationInDB?.id;
|
||||
const associationId = createdAssociationId ?? existingAssociationId;
|
||||
|
||||
if (isDefined(associationId)) {
|
||||
messageExternalIdToMessageChannelMessageAssociationIdMap.set(
|
||||
externalId,
|
||||
associationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createdMessages: messagesToCreate,
|
||||
messageExternalIdsAndIdsMap,
|
||||
messageExternalIdToMessageChannelMessageAssociationIdMap,
|
||||
};
|
||||
},
|
||||
authContext,
|
||||
|
||||
+19
@@ -124,6 +124,25 @@ export class MessagingMessagesImportService {
|
||||
messageChannel,
|
||||
);
|
||||
|
||||
// Map external folder IDs to internal folder IDs
|
||||
const messageFolders = messageChannel.messageFolders ?? [];
|
||||
const foldersWithExternalId = messageFolders.filter(
|
||||
(folder): folder is typeof folder & { externalId: string } =>
|
||||
isDefined(folder.externalId),
|
||||
);
|
||||
|
||||
const folderExternalToInternalMap = new Map<string, string>(
|
||||
foldersWithExternalId.map((folder) => [folder.externalId, folder.id]),
|
||||
);
|
||||
|
||||
for (const message of allMessages) {
|
||||
const externalFolderIds = message.messageFolderExternalIds ?? [];
|
||||
|
||||
message.messageFolderIds = externalFolderIds
|
||||
.map((externalId) => folderExternalToInternalMap.get(externalId))
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
const blocklist = await this.blocklistRepository.getByWorkspaceMemberId(
|
||||
connectedAccountWithFreshTokens.accountOwnerId,
|
||||
workspaceId,
|
||||
|
||||
+9
@@ -15,6 +15,7 @@ import {
|
||||
MessageChannelContactAutoCreationPolicy,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessagingMessageFolderAssociationService } from 'src/modules/messaging/message-import-manager/services/messaging-message-folder-association.service';
|
||||
import { MessagingMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-message.service';
|
||||
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
|
||||
import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
|
||||
@@ -151,6 +152,14 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => {
|
||||
saveMessageParticipants: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MessagingMessageFolderAssociationService,
|
||||
useValue: {
|
||||
saveMessageFolderAssociations: jest
|
||||
.fn()
|
||||
.mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: GlobalWorkspaceOrmManager,
|
||||
useValue: {
|
||||
|
||||
+46
-7
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FieldActorSource, MessageParticipantRole } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
@@ -21,6 +22,10 @@ import {
|
||||
type Participant,
|
||||
type ParticipantWithMessageId,
|
||||
} from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message.type';
|
||||
import {
|
||||
type MessageChannelMessageAssociationFolderAssociation,
|
||||
MessagingMessageFolderAssociationService,
|
||||
} from 'src/modules/messaging/message-import-manager/services/messaging-message-folder-association.service';
|
||||
import { MessagingMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-message.service';
|
||||
import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
|
||||
import { MessagingMessageParticipantService } from 'src/modules/messaging/message-participant-manager/services/messaging-message-participant.service';
|
||||
@@ -33,6 +38,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly messageService: MessagingMessageService,
|
||||
private readonly messageParticipantService: MessagingMessageParticipantService,
|
||||
private readonly messageFolderAssociationService: MessagingMessageFolderAssociationService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
@@ -53,13 +59,15 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
|
||||
return workspaceDataSource?.transaction(
|
||||
async (transactionManager: WorkspaceEntityManager) => {
|
||||
const { messageExternalIdsAndIdsMap } =
|
||||
await this.messageService.saveMessagesWithinTransaction(
|
||||
messagesToSave,
|
||||
messageChannel.id,
|
||||
transactionManager,
|
||||
workspaceId,
|
||||
);
|
||||
const {
|
||||
messageExternalIdsAndIdsMap,
|
||||
messageExternalIdToMessageChannelMessageAssociationIdMap,
|
||||
} = await this.messageService.saveMessagesWithinTransaction(
|
||||
messagesToSave,
|
||||
messageChannel.id,
|
||||
transactionManager,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const participantsWithMessageId: (ParticipantWithMessageId & {
|
||||
shouldCreateContact: boolean;
|
||||
@@ -112,6 +120,37 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const folderAssociations: MessageChannelMessageAssociationFolderAssociation[] =
|
||||
messagesToSave.flatMap((message) => {
|
||||
const messageFolderIds = message.messageFolderIds ?? [];
|
||||
|
||||
if (messageFolderIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const associationId =
|
||||
messageExternalIdToMessageChannelMessageAssociationIdMap.get(
|
||||
message.externalId,
|
||||
);
|
||||
|
||||
if (!isDefined(associationId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
messageChannelMessageAssociationId: associationId,
|
||||
messageFolderIds,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
await this.messageFolderAssociationService.saveMessageFolderAssociations(
|
||||
folderAssociations,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return participantsWithMessageId;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Message = Omit<
|
||||
| 'messageParticipants'
|
||||
| 'messageThread'
|
||||
| 'messageThreadId'
|
||||
| 'messageFolders'
|
||||
| 'id'
|
||||
> & {
|
||||
attachments: {
|
||||
@@ -19,6 +20,8 @@ export type Message = Omit<
|
||||
externalId: string;
|
||||
messageThreadExternalId: string;
|
||||
direction: MessageDirection;
|
||||
messageFolderIds?: string[];
|
||||
messageFolderExternalIds?: string[];
|
||||
labelIds?: string[];
|
||||
};
|
||||
|
||||
|
||||
@@ -780,6 +780,9 @@ export const STANDARD_OBJECTS = {
|
||||
direction: {
|
||||
universalIdentifier: '75c9b0f7-9e76-44d4-a2f9-47051e61eec7',
|
||||
},
|
||||
messageFolders: {
|
||||
universalIdentifier: '20202020-c3d4-e5f6-a7b8-901234567890',
|
||||
},
|
||||
},
|
||||
indexes: {
|
||||
messageChannelIdIndex: {
|
||||
@@ -793,6 +796,40 @@ export const STANDARD_OBJECTS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
messageChannelMessageAssociationMessageFolder: {
|
||||
universalIdentifier: '20202020-a1b0-40b0-8ab0-5b6c7d8e9f0a',
|
||||
fields: {
|
||||
id: {
|
||||
universalIdentifier: '20202020-a1b2-40b1-8ab1-6b7c8d9e0f1a',
|
||||
},
|
||||
createdAt: {
|
||||
universalIdentifier: '20202020-a1b3-40b2-9bb2-7c8d9e0f1a2b',
|
||||
},
|
||||
updatedAt: {
|
||||
universalIdentifier: '20202020-a1b4-40b3-8cb3-8d9e0f1a2b3c',
|
||||
},
|
||||
deletedAt: {
|
||||
universalIdentifier: '20202020-a1b5-40b4-9db4-9e0f1a2b3c4d',
|
||||
},
|
||||
messageChannelMessageAssociation: {
|
||||
universalIdentifier: '20202020-d4e5-f6a7-b8c9-012345678901',
|
||||
},
|
||||
messageFolder: {
|
||||
universalIdentifier: '20202020-e5f6-a7b8-c9d0-123456789012',
|
||||
},
|
||||
},
|
||||
indexes: {
|
||||
messageChannelMessageAssociationIdIndex: {
|
||||
universalIdentifier: '2b38d3e7-5779-4c1f-4e0f-78c9d70d1de9',
|
||||
},
|
||||
messageFolderIdIndex: {
|
||||
universalIdentifier: '3c49e4f8-6880-4d2a-5f1a-89d0e81e2ef0',
|
||||
},
|
||||
messageChannelMessageAssociationIdMessageFolderIdUniqueIndex: {
|
||||
universalIdentifier: '4d50f5a9-7991-4e3b-6a2b-90e1f92f3f01',
|
||||
},
|
||||
},
|
||||
},
|
||||
messageChannel: {
|
||||
universalIdentifier: '20202020-fe8c-40bc-a681-b80b771449b7',
|
||||
fields: {
|
||||
@@ -906,6 +943,9 @@ export const STANDARD_OBJECTS = {
|
||||
pendingSyncAction: {
|
||||
universalIdentifier: '20202020-4f97-4c79-9517-16387fe237f7',
|
||||
},
|
||||
messageChannelMessageAssociationMessageFolders: {
|
||||
universalIdentifier: '20202020-f6a7-b8c9-d0e1-234567890123',
|
||||
},
|
||||
},
|
||||
indexes: {
|
||||
messageChannelIdIndex: {
|
||||
|
||||
Reference in New Issue
Block a user