Remove connected account feature flag (#19286)

Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
neo773
2026-04-06 13:43:41 +05:30
committed by GitHub
parent 3a1e112b86
commit d3f0162cf5
246 changed files with 5216 additions and 6424 deletions
@@ -1,6 +1,8 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { BlocklistItemDeleteCalendarEventsJob } from 'src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job';
import { BlocklistReimportCalendarEventsJob } from 'src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job';
import { CalendarBlocklistListener } from 'src/modules/calendar/blocklist-manager/listeners/calendar-blocklist.listener';
@@ -11,7 +13,7 @@ import { CalendarCommonModule } from 'src/modules/calendar/common/calendar-commo
imports: [
CalendarEventCleanerModule,
CalendarCommonModule,
CalendarChannelDataAccessModule,
TypeOrmModule.forFeature([CalendarChannelEntity, UserWorkspaceEntity]),
],
providers: [
CalendarBlocklistListener,
@@ -1,13 +1,15 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { And, Any, ILike, In, Not, Or } from 'typeorm';
import { And, Any, ILike, In, Not, Or, type Repository } from 'typeorm';
import { type ObjectRecordCreateEvent } from 'twenty-shared/database-events';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
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 WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
@@ -26,7 +28,10 @@ export type BlocklistItemDeleteCalendarEventsJobData = WorkspaceEventBatch<
export class BlocklistItemDeleteCalendarEventsJob {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly calendarEventCleanerService: CalendarEventCleanerService,
) {}
@@ -78,6 +83,12 @@ export class BlocklistItemDeleteCalendarEventsJob {
'calendarChannelEventAssociation',
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workspaceMember',
);
for (const workspaceMemberId of handlesToDeleteByWorkspaceMemberIdMap.keys()) {
const handles =
handlesToDeleteByWorkspaceMemberIdMap.get(workspaceMemberId);
@@ -86,22 +97,37 @@ export class BlocklistItemDeleteCalendarEventsJob {
continue;
}
const calendarChannels =
await this.calendarChannelDataAccessService.find(workspaceId, {
select: {
id: true,
handle: true,
connectedAccount: {
handleAliases: true,
},
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: workspaceMemberId },
});
if (!workspaceMember) {
continue;
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: workspaceMember.userId, workspaceId },
select: ['id'],
});
if (!userWorkspace) {
continue;
}
const calendarChannels = await this.calendarChannelRepository.find({
select: {
id: true,
handle: true,
connectedAccount: {
handleAliases: true,
},
where: {
connectedAccount: {
accountOwnerId: workspaceMemberId,
},
},
relations: ['connectedAccount'],
});
},
where: {
connectedAccount: { userWorkspaceId: userWorkspace.id },
workspaceId,
},
relations: ['connectedAccount'],
});
for (const calendarChannel of calendarChannels) {
const calendarChannelHandles = [calendarChannel.handle];
@@ -1,18 +1,21 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Not } from 'typeorm';
import { Not, type Repository } from 'typeorm';
import { type ObjectRecordDeleteEvent } from 'twenty-shared/database-events';
import { CalendarChannelSyncStage } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
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 WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export type BlocklistReimportCalendarEventsJobData = WorkspaceEventBatch<
ObjectRecordDeleteEvent<BlocklistWorkspaceEntity>
@@ -25,7 +28,10 @@ export type BlocklistReimportCalendarEventsJobData = WorkspaceEventBatch<
export class BlocklistReimportCalendarEventsJob {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
) {}
@@ -36,22 +42,43 @@ export class BlocklistReimportCalendarEventsJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workspaceMember',
);
for (const eventPayload of data.events) {
const workspaceMemberId =
eventPayload.properties.before.workspaceMemberId;
const calendarChannels =
await this.calendarChannelDataAccessService.find(workspaceId, {
select: ['id'],
where: {
connectedAccount: {
accountOwnerId: workspaceMemberId,
},
syncStage: Not(
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
),
},
});
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: workspaceMemberId },
});
if (!isDefined(workspaceMember)) {
continue;
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: workspaceMember.userId, workspaceId },
select: ['id'],
});
if (!isDefined(userWorkspace)) {
continue;
}
const calendarChannels = await this.calendarChannelRepository.find({
select: ['id'],
where: {
connectedAccount: { userWorkspaceId: userWorkspace.id },
syncStage: Not(
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
),
workspaceId,
},
});
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
calendarChannels.map((calendarChannel) => calendarChannel.id),
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { CalendarChannelDeletionCleanupJob } from 'src/modules/calendar/calendar-event-cleaner/jobs/calendar-channel-deletion-cleanup.job';
import { DeleteConnectedAccountAssociatedCalendarDataJob } from 'src/modules/calendar/calendar-event-cleaner/jobs/delete-connected-account-associated-calendar-data.job';
import { CalendarEventCleanerCalendarChannelListener } from 'src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-calendar-channel.listener';
import { CalendarEventCleanerConnectedAccountListener } from 'src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener';
import { CalendarEventCleanerService } from 'src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service';
@@ -1,8 +1,5 @@
import { Logger, Scope } from '@nestjs/common';
import { FeatureFlagKey } from 'twenty-shared/types';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
@@ -22,20 +19,10 @@ export class CalendarChannelDeletionCleanupJob {
constructor(
private readonly calendarEventCleanerService: CalendarEventCleanerService,
private readonly featureFlagService: FeatureFlagService,
) {}
@Process(CalendarChannelDeletionCleanupJob.name)
async handle(data: CalendarChannelDeletionCleanupJobData): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
data.workspaceId,
);
if (!isMigrated) {
return;
}
this.logger.debug(
`WorkspaceId: ${data.workspaceId} Cleaning up calendar channel event associations for channel ${data.calendarChannelId}`,
);
@@ -6,7 +6,7 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import {
CalendarChannelDeletionCleanupJob,
type CalendarChannelDeletionCleanupJobData,
@@ -24,7 +24,7 @@ export class CalendarEventCleanerCalendarChannelListener {
@OnDatabaseBatchEvent('calendarChannel', DatabaseEventAction.DESTROYED)
async handleDestroyedEvent(
payload: WorkspaceEventBatch<
ObjectRecordDeleteEvent<CalendarChannelWorkspaceEntity>
ObjectRecordDeleteEvent<CalendarChannelEntity>
>,
) {
await Promise.all(
@@ -10,7 +10,7 @@ import {
DeleteConnectedAccountAssociatedCalendarDataJob,
type DeleteConnectedAccountAssociatedCalendarDataJobData,
} from 'src/modules/calendar/calendar-event-cleaner/jobs/delete-connected-account-associated-calendar-data.job';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
@@ -24,7 +24,7 @@ export class CalendarEventCleanerConnectedAccountListener {
@OnDatabaseBatchEvent('connectedAccount', DatabaseEventAction.DESTROYED)
async handleDestroyedEvent(
payload: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity>
ObjectRecordDeleteEvent<ConnectedAccountEntity>
>,
) {
await Promise.all(
@@ -7,8 +7,9 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
@@ -39,6 +40,7 @@ import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/cale
import { CalendarCommonModule } from 'src/modules/calendar/common/calendar-common.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 { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module';
import { RefreshTokensManagerModule } from 'src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module';
@Module({
@@ -49,6 +51,9 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres
FeatureFlagEntity,
WorkspaceEntity,
DataSourceEntity,
CalendarChannelEntity,
ConnectedAccountEntity,
UserWorkspaceEntity,
]),
WorkspaceDataSourceModule,
CalendarEventCleanerModule,
@@ -58,10 +63,9 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres
BillingModule,
RefreshTokensManagerModule,
ConnectedAccountModule,
EmailAliasManagerModule,
CalendarCommonModule,
MetricsModule,
CalendarChannelDataAccessModule,
ConnectedAccountDataAccessModule,
FeatureFlagModule,
],
providers: [
@@ -1,18 +1,20 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command, CommandRunner, Option } from 'nest-commander';
import { Repository } from 'typeorm';
import { CalendarChannelSyncStage } from 'twenty-shared/types';
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';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
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 {
CalendarEventListFetchJob,
type CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
type CalendarTriggerEventListFetchCommandOptions = {
workspaceId: string;
@@ -31,7 +33,8 @@ export class CalendarTriggerEventListFetchCommand extends CommandRunner {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
@InjectMessageQueue(MessageQueue.calendarQueue)
private readonly messageQueueService: MessageQueueService,
) {
@@ -51,17 +54,14 @@ export class CalendarTriggerEventListFetchCommand extends CommandRunner {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const calendarChannels = await this.calendarChannelDataAccessService.find(
workspaceId,
{
where: {
isSyncEnabled: true,
syncStage:
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
...(calendarChannelId ? { id: calendarChannelId } : {}),
},
const calendarChannels = await this.calendarChannelRepository.find({
where: {
isSyncEnabled: true,
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
...(calendarChannelId ? { id: calendarChannelId } : {}),
workspaceId,
},
);
});
if (calendarChannels.length === 0) {
this.logger.warn(
@@ -76,9 +76,8 @@ export class CalendarTriggerEventListFetchCommand extends CommandRunner {
);
for (const calendarChannel of calendarChannels) {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: calendarChannel.id },
await this.calendarChannelRepository.update(
{ id: calendarChannel.id, workspaceId },
{
syncStage:
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
@@ -1,24 +1,21 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import { CalendarChannelSyncStage } from 'twenty-shared/types';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import {
CalendarEventListFetchJob,
type CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export const CALENDAR_EVENT_LIST_FETCH_CRON_PATTERN = '*/5 * * * *';
@@ -34,7 +31,6 @@ export class CalendarEventListFetchCronJob {
private readonly exceptionHandlerService: ExceptionHandlerService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly featureFlagService: FeatureFlagService,
) {}
@Process(CalendarEventListFetchCronJob.name)
@@ -53,22 +49,11 @@ export class CalendarEventListFetchCronJob {
try {
const now = new Date().toISOString();
// TODO: remove workspace schema branch once IS_CONNECTED_ACCOUNT_MIGRATED feature flag is removed
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
activeWorkspace.id,
const [calendarChannels] = await this.coreDataSource.query(
`UPDATE core."calendarChannel" SET "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING}' RETURNING *`,
);
const [calendarChannels] = isMigrated
? await this.coreDataSource.query(
`UPDATE core."calendarChannel" SET "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING}' RETURNING *`,
)
: await this.coreDataSource.query(
`UPDATE ${getWorkspaceSchemaName(activeWorkspace.id)}."calendarChannel" SET "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "isSyncEnabled" = true AND "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING}' RETURNING *`,
);
for (const calendarChannel of calendarChannels) {
await this.messageQueueService.add<CalendarEventListFetchJobData>(
CalendarEventListFetchJob.name,
@@ -1,22 +1,19 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import { CalendarChannelSyncStage } from 'twenty-shared/types';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { type CalendarEventListFetchJobData } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import { CalendarEventsImportJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export const CALENDAR_EVENTS_IMPORT_CRON_PATTERN = '*/1 * * * *';
@@ -32,7 +29,6 @@ export class CalendarEventsImportCronJob {
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly featureFlagService: FeatureFlagService,
) {}
@Process(CalendarEventsImportCronJob.name)
@@ -51,22 +47,11 @@ export class CalendarEventsImportCronJob {
try {
const now = new Date().toISOString();
// TODO: remove workspace schema branch once IS_CONNECTED_ACCOUNT_MIGRATED feature flag is removed
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
activeWorkspace.id,
const [calendarChannels] = await this.coreDataSource.query(
`UPDATE core."calendarChannel" SET "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING}' RETURNING *`,
);
const [calendarChannels] = isMigrated
? await this.coreDataSource.query(
`UPDATE core."calendarChannel" SET "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING}' RETURNING *`,
)
: await this.coreDataSource.query(
`UPDATE ${getWorkspaceSchemaName(activeWorkspace.id)}."calendarChannel" SET "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "isSyncEnabled" = true AND "syncStage" = '${CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING}' RETURNING *`,
);
for (const calendarChannel of calendarChannels) {
await this.messageQueueService.add<CalendarEventListFetchJobData>(
CalendarEventsImportJob.name,
@@ -3,6 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
} from 'twenty-shared/types';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
@@ -16,10 +20,6 @@ import {
CalendarRelaunchFailedCalendarChannelJob,
type CalendarRelaunchFailedCalendarChannelJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job';
import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export const CALENDAR_RELAUNCH_FAILED_CALENDAR_CHANNELS_CRON_PATTERN =
'*/30 * * * *';
@@ -4,7 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { CalDAVClient } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class CalDavClientProvider {
@@ -14,7 +14,7 @@ export class CalDavClientProvider {
public async getCalDavCalendarClient(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'id' | 'provider' | 'connectionParameters' | 'handle'
>,
): Promise<CalDAVClient> {
@@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { CalDavClientProvider } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/providers/caldav.provider';
import { parseCalDAVError } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/utils/parse-caldav-error.util';
import { type GetCalendarEventsResponse } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class CalDavGetEventsService {
@@ -18,7 +18,7 @@ export class CalDavGetEventsService {
public async getCalendarEvents(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'provider' | 'id' | 'connectionParameters' | 'handle'
>,
syncCursor?: string,
@@ -9,7 +9,7 @@ import { parseGaxiosError } from 'src/modules/calendar/calendar-event-import-man
import { parseGoogleCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/parse-google-calendar-error.util';
import { type GetCalendarEventsResponse } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class GoogleCalendarGetEventsService {
@@ -21,7 +21,7 @@ export class GoogleCalendarGetEventsService {
public async getCalendarEvents(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'provider' | 'refreshToken' | 'id'
>,
syncCursor?: string,
@@ -9,7 +9,7 @@ import {
import { parseMicrosoftCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util';
import { type GetCalendarEventsResponse } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class MicrosoftCalendarGetEventsService {
@@ -19,7 +19,7 @@ export class MicrosoftCalendarGetEventsService {
public async getCalendarEvents(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'provider' | 'accessToken' | 'refreshToken' | 'id'
>,
syncCursor?: string,
@@ -6,7 +6,7 @@ import { formatMicrosoftCalendarEvents } from 'src/modules/calendar/calendar-eve
import { parseMicrosoftCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util';
import { type FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class MicrosoftCalendarImportEventsService {
@@ -16,7 +16,7 @@ export class MicrosoftCalendarImportEventsService {
public async getCalendarEvents(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'provider' | 'accessToken' | 'refreshToken' | 'id'
>,
changedEventIds: string[],
@@ -1,19 +1,18 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CalendarChannelSyncStage } from 'twenty-shared/types';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
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 { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import {
CalendarChannelSyncStage,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { isThrottled } from 'src/modules/connected-account/utils/is-throttled';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
export type CalendarEventListFetchJobData = {
calendarChannelId: string;
@@ -27,7 +26,8 @@ export type CalendarEventListFetchJobData = {
export class CalendarEventListFetchJob {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
private readonly calendarFetchEventsService: CalendarFetchEventsService,
) {}
@@ -39,14 +39,14 @@ export class CalendarEventListFetchJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const calendarChannel =
await this.calendarChannelDataAccessService.findOne(workspaceId, {
where: {
id: calendarChannelId,
isSyncEnabled: true,
},
relations: ['connectedAccount'],
});
const calendarChannel = await this.calendarChannelRepository.findOne({
where: {
id: calendarChannelId,
isSyncEnabled: true,
workspaceId,
},
relations: ['connectedAccount'],
});
if (!calendarChannel) {
return;
@@ -62,7 +62,10 @@ export class CalendarEventListFetchJob {
const syncStageStartedAt = calendarChannel.syncStageStartedAt;
if (
isThrottled(syncStageStartedAt, calendarChannel.throttleFailureCount)
isThrottled(
syncStageStartedAt?.toISOString() ?? null,
calendarChannel.throttleFailureCount,
)
) {
await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending(
[calendarChannel.id],
@@ -74,8 +77,8 @@ export class CalendarEventListFetchJob {
}
await this.calendarFetchEventsService.fetchCalendarEvents(
calendarChannel as unknown as CalendarChannelWorkspaceEntity,
calendarChannel.connectedAccount as unknown as ConnectedAccountWorkspaceEntity,
calendarChannel as unknown as CalendarChannelEntity,
calendarChannel.connectedAccount,
workspaceId,
);
}, authContext);
@@ -1,19 +1,18 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CalendarChannelSyncStage } from 'twenty-shared/types';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
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 { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import {
CalendarChannelSyncStage,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { isThrottled } from 'src/modules/connected-account/utils/is-throttled';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
export type CalendarEventsImportJobData = {
calendarChannelId: string;
@@ -29,7 +28,8 @@ export class CalendarEventsImportJob {
private readonly calendarEventsImportService: CalendarEventsImportService,
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
) {}
@Process(CalendarEventsImportJob.name)
@@ -39,14 +39,14 @@ export class CalendarEventsImportJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const calendarChannel =
await this.calendarChannelDataAccessService.findOne(workspaceId, {
where: {
id: calendarChannelId,
isSyncEnabled: true,
},
relations: ['connectedAccount'],
});
const calendarChannel = await this.calendarChannelRepository.findOne({
where: {
id: calendarChannelId,
isSyncEnabled: true,
workspaceId,
},
relations: ['connectedAccount'],
});
if (!calendarChannel?.isSyncEnabled) {
return;
@@ -62,7 +62,10 @@ export class CalendarEventsImportJob {
const syncStageStartedAt = calendarChannel.syncStageStartedAt;
if (
isThrottled(syncStageStartedAt, calendarChannel.throttleFailureCount)
isThrottled(
syncStageStartedAt?.toISOString() ?? null,
calendarChannel.throttleFailureCount,
)
) {
await this.calendarChannelSyncStatusService.markAsCalendarEventsImportPending(
[calendarChannel.id],
@@ -74,8 +77,8 @@ export class CalendarEventsImportJob {
}
await this.calendarEventsImportService.processCalendarEventsImport(
calendarChannel as unknown as CalendarChannelWorkspaceEntity,
calendarChannel.connectedAccount as unknown as ConnectedAccountWorkspaceEntity,
calendarChannel as unknown as CalendarChannelEntity,
calendarChannel.connectedAccount,
workspaceId,
);
}, authContext);
@@ -1,16 +1,17 @@
import { Logger, Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In } from 'typeorm';
import { In, Repository } from 'typeorm';
import { CalendarChannelSyncStage } from 'twenty-shared/types';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
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 { isSyncStale } from 'src/modules/calendar/calendar-event-import-manager/utils/is-sync-stale.util';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export type CalendarOngoingStaleJobData = {
workspaceId: string;
@@ -24,7 +25,8 @@ export class CalendarOngoingStaleJob {
private readonly logger = new Logger(CalendarOngoingStaleJob.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
) {}
@@ -35,24 +37,22 @@ export class CalendarOngoingStaleJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const calendarChannels = await this.calendarChannelDataAccessService.find(
workspaceId,
{
where: {
syncStage: In([
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING,
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING,
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED,
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
]),
},
const calendarChannels = await this.calendarChannelRepository.find({
where: {
syncStage: In([
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING,
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING,
CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED,
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
]),
workspaceId,
},
);
});
for (const calendarChannel of calendarChannels) {
const syncStageStartedAt = calendarChannel.syncStageStartedAt;
if (isSyncStale(syncStageStartedAt)) {
if (isSyncStale(syncStageStartedAt?.toISOString() ?? null)) {
await this.calendarChannelSyncStatusService.resetSyncStageStartedAt(
[calendarChannel.id],
workspaceId,
@@ -1,15 +1,18 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
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 {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
} from 'twenty-shared/types';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
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';
export type CalendarRelaunchFailedCalendarChannelJobData = {
workspaceId: string;
@@ -23,7 +26,8 @@ export type CalendarRelaunchFailedCalendarChannelJobData = {
export class CalendarRelaunchFailedCalendarChannelJob {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
) {}
@Process(CalendarRelaunchFailedCalendarChannelJob.name)
@@ -33,12 +37,12 @@ export class CalendarRelaunchFailedCalendarChannelJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const calendarChannel =
await this.calendarChannelDataAccessService.findOne(workspaceId, {
where: {
id: calendarChannelId,
},
});
const calendarChannel = await this.calendarChannelRepository.findOne({
where: {
id: calendarChannelId,
workspaceId,
},
});
if (
!calendarChannel ||
@@ -48,9 +52,8 @@ export class CalendarRelaunchFailedCalendarChannelJob {
return;
}
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: calendarChannelId },
await this.calendarChannelRepository.update(
{ id: calendarChannelId, workspaceId },
{
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
syncStatus: CalendarChannelSyncStatus.ACTIVE,
@@ -1,5 +1,5 @@
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service';
const mockCalendarChannelSyncStatusService = {
@@ -12,7 +12,7 @@ const mockGetCalendarEventsService = {
getCalendarEvents: jest.fn(),
};
const mockCalendarChannelDataAccessService = {
const mockCalendarChannelRepository = {
update: jest.fn(),
};
@@ -51,16 +51,16 @@ const baseConnectedAccount = {
refreshToken: 'refresh-token',
accessToken: 'access-token',
handle: 'test@example.com',
} as unknown as ConnectedAccountWorkspaceEntity;
} as unknown as ConnectedAccountEntity;
const createCalendarChannel = (
syncCursor: string | null,
): CalendarChannelWorkspaceEntity =>
): CalendarChannelEntity =>
({
id: 'channel-123',
syncCursor,
connectedAccountId: 'account-123',
}) as unknown as CalendarChannelWorkspaceEntity;
}) as unknown as CalendarChannelEntity;
describe('CalendarFetchEventsService', () => {
let service: CalendarFetchEventsService;
@@ -77,7 +77,7 @@ describe('CalendarFetchEventsService', () => {
service = new CalendarFetchEventsService(
mockCacheStorage as any,
mockGlobalWorkspaceOrmManager as any,
mockCalendarChannelDataAccessService as any,
mockCalendarChannelRepository as any,
mockCalendarChannelSyncStatusService as any,
mockGetCalendarEventsService as any,
mockCalendarEventImportErrorHandlerService as any,
@@ -147,9 +147,8 @@ describe('CalendarFetchEventsService', () => {
workspaceId,
);
expect(mockCalendarChannelDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: 'channel-123' },
expect(mockCalendarChannelRepository.update).toHaveBeenCalledWith(
{ id: 'channel-123', workspaceId },
{ syncCursor: 'new-cursor-abc' },
);
});
@@ -12,10 +12,10 @@ import {
ConnectedAccountRefreshTokensService,
type ConnectedAccountTokens,
} from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
interface ValidateAndRefreshConnectedAccountAuthenticationParams {
connectedAccount: ConnectedAccountWorkspaceEntity;
connectedAccount: ConnectedAccountEntity;
workspaceId: string;
calendarChannelId: string;
}
@@ -1,7 +1,9 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import {
type TwentyORMException,
TwentyORMExceptionCode,
@@ -16,7 +18,7 @@ import {
CalendarEventImportExceptionCode,
} from 'src/modules/calendar/calendar-event-import-manager/exceptions/calendar-event-import.exception';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
export enum CalendarEventImportSyncStep {
CALENDAR_EVENT_LIST_FETCH = 'CALENDAR_EVENT_LIST_FETCH',
CALENDAR_EVENTS_IMPORT = 'CALENDAR_EVENTS_IMPORT',
@@ -28,7 +30,8 @@ export class CalendarEventImportErrorHandlerService {
CalendarEventImportErrorHandlerService.name,
);
constructor(
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@@ -36,10 +39,7 @@ export class CalendarEventImportErrorHandlerService {
public async handleDriverException(
exception: CalendarEventImportDriverException | TwentyORMException,
syncStep: CalendarEventImportSyncStep,
calendarChannel: Pick<
CalendarChannelWorkspaceEntity,
'id' | 'throttleFailureCount'
>,
calendarChannel: Pick<CalendarChannelEntity, 'id' | 'throttleFailureCount'>,
workspaceId: string,
): Promise<void> {
switch (exception.code) {
@@ -81,7 +81,7 @@ export class CalendarEventImportErrorHandlerService {
}
private async handleSyncCursorErrorException(
calendarChannel: Pick<CalendarChannelWorkspaceEntity, 'id'>,
calendarChannel: Pick<CalendarChannelEntity, 'id'>,
workspaceId: string,
): Promise<void> {
this.logger.debug(
@@ -96,10 +96,7 @@ export class CalendarEventImportErrorHandlerService {
private async handleTemporaryException(
syncStep: CalendarEventImportSyncStep,
calendarChannel: Pick<
CalendarChannelWorkspaceEntity,
'id' | 'throttleFailureCount'
>,
calendarChannel: Pick<CalendarChannelEntity, 'id' | 'throttleFailureCount'>,
workspaceId: string,
): Promise<void> {
if (
@@ -132,11 +129,8 @@ export class CalendarEventImportErrorHandlerService {
throw calendarEventImportException;
}
await this.calendarChannelDataAccessService.increment(
workspaceId,
{
id: calendarChannel.id,
},
await this.calendarChannelRepository.increment(
{ id: calendarChannel.id, workspaceId },
'throttleFailureCount',
1,
);
@@ -164,7 +158,7 @@ export class CalendarEventImportErrorHandlerService {
}
private async handleInsufficientPermissionsException(
calendarChannel: Pick<CalendarChannelWorkspaceEntity, 'id'>,
calendarChannel: Pick<CalendarChannelEntity, 'id'>,
workspaceId: string,
): Promise<void> {
await this.calendarChannelSyncStatusService.markAsFailedInsufficientPermissionsAndFlushCalendarEventsToImport(
@@ -175,7 +169,7 @@ export class CalendarEventImportErrorHandlerService {
private async handleUnknownException(
exception: { message: string },
calendarChannel: Pick<CalendarChannelWorkspaceEntity, 'id'>,
calendarChannel: Pick<CalendarChannelEntity, 'id'>,
workspaceId: string,
): Promise<void> {
await this.calendarChannelSyncStatusService.markAsFailedUnknownAndFlushCalendarEventsToImport(
@@ -207,7 +201,7 @@ export class CalendarEventImportErrorHandlerService {
private async handleNotFoundException(
syncStep: CalendarEventImportSyncStep,
calendarChannel: Pick<CalendarChannelWorkspaceEntity, 'id'>,
calendarChannel: Pick<CalendarChannelEntity, 'id'>,
workspaceId: string,
): Promise<void> {
if (syncStep === CalendarEventImportSyncStep.CALENDAR_EVENT_LIST_FETCH) {
@@ -1,11 +1,14 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Any } from 'typeorm';
import { Any, Repository } from 'typeorm';
import { type CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
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';
@@ -26,9 +29,10 @@ import { CalendarSaveEventsService } from 'src/modules/calendar/calendar-event-i
import { filterEventsAndReturnCancelledEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/filter-events.util';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import { type CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { EmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/services/email-alias-manager.service';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class CalendarEventsImportService {
@@ -43,11 +47,14 @@ export class CalendarEventsImportService {
private readonly calendarSaveEventsService: CalendarSaveEventsService,
private readonly calendarEventImportErrorHandlerService: CalendarEventImportErrorHandlerService,
private readonly microsoftCalendarImportEventService: MicrosoftCalendarImportEventsService,
private readonly emailAliasManagerService: EmailAliasManagerService,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
public async processCalendarEventsImport(
calendarChannel: CalendarChannelWorkspaceEntity,
connectedAccount: ConnectedAccountWorkspaceEntity,
calendarChannel: CalendarChannelEntity,
connectedAccount: ConnectedAccountEntity,
workspaceId: string,
fetchedCalendarEvents?: FetchedCalendarEvent[],
): Promise<void> {
@@ -99,10 +106,39 @@ export class CalendarEventsImportService {
);
}
const blocklist = await this.blocklistRepository.getByWorkspaceMemberId(
connectedAccount.accountOwnerId,
workspaceId,
);
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: {
id: (connectedAccount as unknown as { userWorkspaceId: string })
.userWorkspaceId,
},
});
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const workspaceMember = userWorkspace
? await workspaceMemberRepository.findOne({
where: { userId: userWorkspace.userId },
})
: null;
const blocklist = workspaceMember
? await this.blocklistRepository.getByWorkspaceMemberId(
workspaceMember.id,
workspaceId,
)
: [];
const refreshedHandleAliases =
await this.emailAliasManagerService.refreshHandleAliases(
connectedAccount,
workspaceId,
);
connectedAccount.handleAliases = refreshedHandleAliases;
if (
!isDefined(connectedAccount.handleAliases) ||
@@ -116,10 +152,7 @@ export class CalendarEventsImportService {
const { filteredEvents, cancelledEvents } =
filterEventsAndReturnCancelledEvents(
[
calendarChannel.handle,
...connectedAccount.handleAliases.split(','),
],
[calendarChannel.handle, ...connectedAccount.handleAliases],
calendarEvents,
blocklist.map((blocklist) => blocklist.handle ?? ''),
);
@@ -148,9 +181,7 @@ export class CalendarEventsImportService {
await calendarChannelEventAssociationRepository.delete({
eventExternalId: Any(cancelledEventExternalIds),
calendarChannel: {
id: calendarChannel.id,
},
calendarChannelId: calendarChannel.id,
});
await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents(
@@ -1,9 +1,11 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
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 {
@@ -18,8 +20,8 @@ import {
import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service';
import { CalendarGetCalendarEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
@Injectable()
export class CalendarFetchEventsService {
@@ -28,7 +30,8 @@ export class CalendarFetchEventsService {
@InjectCacheStorage(CacheStorageNamespace.ModuleCalendar)
private readonly cacheStorage: CacheStorageService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
private readonly getCalendarEventsService: CalendarGetCalendarEventsService,
private readonly calendarEventImportErrorHandlerService: CalendarEventImportErrorHandlerService,
@@ -37,8 +40,8 @@ export class CalendarFetchEventsService {
) {}
public async fetchCalendarEvents(
calendarChannel: CalendarChannelWorkspaceEntity,
connectedAccount: ConnectedAccountWorkspaceEntity,
calendarChannel: CalendarChannelEntity,
connectedAccount: ConnectedAccountEntity,
workspaceId: string,
): Promise<void> {
this.logger.log(
@@ -84,11 +87,8 @@ export class CalendarFetchEventsService {
const nextSyncCursor = getCalendarEventsResponse.nextSyncCursor;
if (!calendarEvents || calendarEvents?.length === 0) {
await this.calendarChannelDataAccessService.update(
workspaceId,
{
id: calendarChannel.id,
},
await this.calendarChannelRepository.update(
{ id: calendarChannel.id, workspaceId },
{
syncCursor: nextSyncCursor,
},
@@ -100,11 +100,8 @@ export class CalendarFetchEventsService {
);
}
await this.calendarChannelDataAccessService.update(
workspaceId,
{
id: calendarChannel.id,
},
await this.calendarChannelRepository.update(
{ id: calendarChannel.id, workspaceId },
{
syncCursor: nextSyncCursor,
},
@@ -10,7 +10,7 @@ import {
CalendarEventImportExceptionCode,
} from 'src/modules/calendar/calendar-event-import-manager/exceptions/calendar-event-import.exception';
import { type FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
export type GetCalendarEventsResponse = {
fullEvents: boolean;
@@ -29,7 +29,7 @@ export class CalendarGetCalendarEventsService {
public async getCalendarEvents(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -3,15 +3,15 @@ import { Injectable } from '@nestjs/common';
import { Any } from 'typeorm';
import { v4 as uuid } from 'uuid';
import { type CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
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 { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service';
import { type CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { type FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
type FetchedCalendarEventWithDBEvent = {
fetchedCalendarEvent: FetchedCalendarEvent;
@@ -28,8 +28,8 @@ export class CalendarSaveEventsService {
public async saveCalendarEventsAndEnqueueContactCreationJob(
fetchedCalendarEvents: FetchedCalendarEvent[],
calendarChannel: CalendarChannelWorkspaceEntity,
connectedAccount: ConnectedAccountWorkspaceEntity,
calendarChannel: CalendarChannelEntity,
connectedAccount: ConnectedAccountEntity,
workspaceId: string,
): Promise<void> {
const authContext = buildSystemAuthContext(workspaceId);
@@ -6,16 +6,16 @@ import differenceWith from 'lodash.differencewith';
import { FieldActorSource } from 'twenty-shared/types';
import { Any } from 'typeorm';
import { type CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
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';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
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 CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
import { type FetchedCalendarEventParticipant } from 'src/modules/calendar/common/types/fetched-calendar-event';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import {
CreateCompanyAndContactJob,
type CreateCompanyAndContactJobData,
@@ -52,8 +52,8 @@ export class CalendarEventParticipantService {
participantsToCreate: FetchedCalendarEventParticipantWithCalendarEventId[];
participantsToUpdate: FetchedCalendarEventParticipantWithCalendarEventId[];
transactionManager?: WorkspaceEntityManager;
calendarChannel: CalendarChannelWorkspaceEntity;
connectedAccount: ConnectedAccountWorkspaceEntity;
calendarChannel: CalendarChannelEntity;
connectedAccount: ConnectedAccountEntity;
workspaceId: string;
}): Promise<void> {
const authContext = buildSystemAuthContext(workspaceId);
@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
@@ -12,10 +12,12 @@ import { ConnectedAccountModule } from 'src/modules/connected-account/connected-
@Module({
imports: [
WorkspaceDataSourceModule,
TypeOrmModule.forFeature([FeatureFlagEntity]),
TypeOrmModule.forFeature([
CalendarChannelEntity,
ConnectedAccountEntity,
UserWorkspaceEntity,
]),
ConnectedAccountModule,
CalendarChannelDataAccessModule,
ConnectedAccountDataAccessModule,
MetricsModule,
],
providers: [CalendarChannelSyncStatusService],
@@ -1,10 +1,13 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { CalendarChannelVisibility } from 'twenty-shared/types';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { ApplyCalendarEventsVisibilityRestrictionsService } from './apply-calendar-events-visibility-restrictions.service';
@@ -49,7 +52,15 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
findOneByOrFail: jest.fn(),
};
const mockConnectedAccountDataAccessService = {
const mockConnectedAccountRepository = {
find: jest.fn(),
};
const mockUserWorkspaceRepository = {
findOne: jest.fn(),
};
const mockCalendarChannelRepository = {
find: jest.fn(),
};
@@ -76,8 +87,16 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
useValue: mockGlobalWorkspaceOrmManager,
},
{
provide: ConnectedAccountDataAccessService,
useValue: mockConnectedAccountDataAccessService,
provide: getRepositoryToken(ConnectedAccountEntity),
useValue: mockConnectedAccountRepository,
},
{
provide: getRepositoryToken(UserWorkspaceEntity),
useValue: mockUserWorkspaceRepository,
},
{
provide: getRepositoryToken(CalendarChannelEntity),
useValue: mockCalendarChannelRepository,
},
],
}).compile();
@@ -95,17 +114,17 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
createMockCalendarEvent('1', 'Test Event', 'Test Description'),
];
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
calendarChannel: {
id: '1',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
},
calendarChannelId: '1',
},
]);
mockCalendarChannelRepository.find.mockResolvedValue([
{
id: '1',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
},
]);
@@ -123,7 +142,7 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
item.description !== FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
),
).toBe(true);
expect(mockConnectedAccountDataAccessService.find).not.toHaveBeenCalled();
expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled();
});
it('should return calendar event with obfuscated title and description if the visibility is METADATA', async () => {
@@ -134,18 +153,22 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
calendarChannel: {
id: '1',
visibility: CalendarChannelVisibility.METADATA,
},
calendarChannelId: '1',
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
mockCalendarChannelRepository.find.mockResolvedValue([
{
id: '1',
visibility: CalendarChannelVisibility.METADATA,
},
]);
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
calendarEvents,
@@ -170,18 +193,22 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
calendarChannel: {
id: '1',
visibility: CalendarChannelVisibility.METADATA,
},
calendarChannelId: '1',
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-account-owner-id',
mockCalendarChannelRepository.find.mockResolvedValue([
{
id: '1',
visibility: CalendarChannelVisibility.METADATA,
},
]);
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
mockConnectedAccountDataAccessService.find.mockResolvedValue([{ id: '1' }]);
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
calendarEvents,
@@ -207,17 +234,21 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
calendarChannel: {
id: '1',
},
calendarChannelId: '1',
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-not-account-owner-id',
mockCalendarChannelRepository.find.mockResolvedValue([
{
id: '1',
},
]);
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
calendarEvents,
@@ -235,35 +266,41 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
createMockCalendarEvent('3', 'Event 3', 'Description 3'),
];
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
calendarChannel: {
id: '1',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
},
calendarChannelId: '1',
},
{
calendarEventId: '2',
calendarChannel: {
id: '2',
visibility: CalendarChannelVisibility.METADATA,
},
calendarChannelId: '2',
},
{
calendarEventId: '3',
calendarChannel: {
id: '3',
visibility: CalendarChannelVisibility.METADATA,
},
calendarChannelId: '3',
},
]);
mockConnectedAccountDataAccessService.find
mockCalendarChannelRepository.find.mockResolvedValue([
{
id: '1',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
},
{
id: '2',
visibility: CalendarChannelVisibility.METADATA,
},
{
id: '3',
visibility: CalendarChannelVisibility.METADATA,
},
]);
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for calendar event 3
.mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2
@@ -291,37 +328,38 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
createMockCalendarEvent('3', 'Event 3', 'Description 3'),
];
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
calendarChannel: {
id: '1',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
},
calendarChannelId: '1',
},
{
calendarEventId: '2',
calendarChannel: {
id: '2',
visibility: CalendarChannelVisibility.METADATA,
},
calendarChannelId: '2',
},
{
calendarEventId: '3',
calendarChannel: {
id: '3',
visibility: CalendarChannelVisibility.METADATA,
},
calendarChannelId: '3',
},
]);
mockConnectedAccountDataAccessService.find
.mockResolvedValueOnce([]) // request for calendar event 3
.mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2
mockCalendarChannelRepository.find.mockResolvedValue([
{
id: '1',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
},
{
id: '2',
visibility: CalendarChannelVisibility.METADATA,
},
{
id: '3',
visibility: CalendarChannelVisibility.METADATA,
},
]);
// userId is undefined (api key request), so connected account check is skipped
// METADATA events should be obfuscated
const result = await service.applyCalendarEventsVisibilityRestrictions(
calendarEvents,
@@ -331,12 +369,17 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
expect(result).toEqual([
calendarEvents[0],
calendarEvents[1],
{
...calendarEvents[1],
title: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
description: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
{
...calendarEvents[2],
title: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
description: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
]);
expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled();
});
});
@@ -1,29 +1,36 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import groupBy from 'lodash.groupby';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { CalendarChannelVisibility } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { In, Repository } from 'typeorm';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
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 CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity';
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class ApplyCalendarEventsVisibilityRestrictionsService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
) {}
public async applyCalendarEventsVisibilityRestrictions(
calendarEvents: CalendarEventWorkspaceEntity[],
workspaceId: string,
userId?: string, // undefined when request is made with api key
userId?: string,
) {
const authContext = buildSystemAuthContext(workspaceId);
@@ -40,26 +47,42 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
where: {
calendarEventId: In(calendarEvents.map((event) => event.id)),
},
relations: ['calendarChannel'],
});
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const calendarChannelIds = [
...new Set(
calendarChannelCalendarEventsAssociations.map(
(association) => association.calendarChannelId,
),
),
];
const calendarChannelsFromCore =
calendarChannelIds.length > 0
? await this.calendarChannelRepository.find({
where: {
id: In(calendarChannelIds),
workspaceId,
},
})
: [];
const calendarChannelMap = new Map(
calendarChannelsFromCore.map((channel) => [channel.id, channel]),
);
for (let i = calendarEvents.length - 1; i >= 0; i--) {
const calendarChannelCalendarEventAssociations =
calendarChannelCalendarEventsAssociations.filter(
(association) =>
association.calendarEventId === calendarEvents[i].id,
);
const calendarChannels = calendarChannelCalendarEventAssociations.map(
(association) => association.calendarChannel,
const associations = calendarChannelCalendarEventsAssociations.filter(
(association) =>
association.calendarEventId === calendarEvents[i].id,
);
const calendarChannels = associations
.map((association) =>
calendarChannelMap.get(association.calendarChannelId),
)
.filter(isDefined);
const calendarChannelsGroupByVisibility = groupBy(
calendarChannels,
(channel) => channel.visibility,
@@ -74,21 +97,26 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
}
if (isDefined(userId)) {
const workspaceMember =
await workspaceMemberRepository.findOneByOrFail({
userId: userId,
});
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId, workspaceId },
select: ['id'],
});
const connectedAccounts =
await this.connectedAccountDataAccessService.find(workspaceId, {
calendarChannels: {
id: In(calendarChannels.map((channel) => channel.id)),
},
accountOwnerId: workspaceMember.id,
});
if (userWorkspace) {
const connectedAccounts =
await this.connectedAccountRepository.find({
where: {
calendarChannels: {
id: In(calendarChannels.map((channel) => channel.id)),
},
userWorkspaceId: userWorkspace.id,
workspaceId,
},
});
if (connectedAccounts.length > 0) {
continue;
if (connectedAccounts.length > 0) {
continue;
}
}
}
@@ -1,12 +1,21 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { CalendarEventFindManyPostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.post-query.hook';
import { CalendarEventFindOnePostQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.post-query.hook';
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
@Module({
imports: [ConnectedAccountDataAccessModule],
imports: [
TypeOrmModule.forFeature([
CalendarChannelEntity,
ConnectedAccountEntity,
UserWorkspaceEntity,
]),
],
providers: [
ApplyCalendarEventsVisibilityRestrictionsService,
CalendarEventFindOnePostQueryHook,
@@ -1,32 +1,36 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Any, In } from 'typeorm';
import { Any, In, Repository } from 'typeorm';
import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
} from 'twenty-shared/types';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
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 {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class CalendarChannelSyncStatusService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectCacheStorage(CacheStorageNamespace.ModuleCalendar)
private readonly cacheStorage: CacheStorageService,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly accountsToReconnectService: AccountsToReconnectService,
private readonly metricsService: MetricsService,
) {}
@@ -43,9 +47,8 @@ export class CalendarChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: In(calendarChannelIds) },
await this.calendarChannelRepository.update(
{ id: In(calendarChannelIds), workspaceId },
{
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
@@ -65,9 +68,8 @@ export class CalendarChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: In(calendarChannelIds) },
await this.calendarChannelRepository.update(
{ id: In(calendarChannelIds), workspaceId },
{
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING,
syncStatus: CalendarChannelSyncStatus.ONGOING,
@@ -94,9 +96,8 @@ export class CalendarChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: In(calendarChannelIds) },
await this.calendarChannelRepository.update(
{ id: In(calendarChannelIds), workspaceId },
{
syncCursor: '',
syncStageStartedAt: null,
@@ -122,9 +123,8 @@ export class CalendarChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: In(calendarChannelIds) },
await this.calendarChannelRepository.update(
{ id: In(calendarChannelIds), workspaceId },
{
syncStageStartedAt: null,
},
@@ -144,9 +144,8 @@ export class CalendarChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: In(calendarChannelIds) },
await this.calendarChannelRepository.update(
{ id: In(calendarChannelIds), workspaceId },
{
syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING,
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
@@ -166,9 +165,8 @@ export class CalendarChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: In(calendarChannelIds) },
await this.calendarChannelRepository.update(
{ id: In(calendarChannelIds), workspaceId },
{
syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING,
syncStatus: CalendarChannelSyncStatus.ONGOING,
@@ -189,9 +187,8 @@ export class CalendarChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: In(calendarChannelIds) },
await this.calendarChannelRepository.update(
{ id: In(calendarChannelIds), workspaceId },
{
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
syncStatus: CalendarChannelSyncStatus.ACTIVE,
@@ -230,9 +227,8 @@ export class CalendarChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: In(calendarChannelIds) },
await this.calendarChannelRepository.update(
{ id: In(calendarChannelIds), workspaceId },
{
syncStatus: CalendarChannelSyncStatus.FAILED_UNKNOWN,
syncStage: CalendarChannelSyncStage.FAILED,
@@ -263,30 +259,25 @@ export class CalendarChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: In(calendarChannelIds) },
await this.calendarChannelRepository.update(
{ id: In(calendarChannelIds), workspaceId },
{
syncStatus: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
syncStage: CalendarChannelSyncStage.FAILED,
},
);
const calendarChannels = await this.calendarChannelDataAccessService.find(
workspaceId,
{
select: ['id', 'connectedAccountId'],
where: { id: Any(calendarChannelIds) },
},
);
const calendarChannels = await this.calendarChannelRepository.find({
select: ['id', 'connectedAccountId'],
where: { id: Any(calendarChannelIds), workspaceId },
});
const connectedAccountIds = calendarChannels.map(
(calendarChannel) => calendarChannel.connectedAccountId,
);
await this.connectedAccountDataAccessService.update(
workspaceId,
{ id: Any(connectedAccountIds) },
await this.connectedAccountRepository.update(
{ id: Any(connectedAccountIds), workspaceId },
{
authFailedAt: new Date(),
},
@@ -312,42 +303,33 @@ export class CalendarChannelSyncStatusService {
return;
}
const calendarChannels = await this.calendarChannelDataAccessService.find(
workspaceId,
{
select: ['id', 'connectedAccountId'],
where: {
id: Any(calendarChannelIds),
},
},
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
const calendarChannels = await this.calendarChannelRepository.find({
select: ['id', 'connectedAccountId'],
where: {
id: Any(calendarChannelIds),
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
},
});
for (const calendarChannel of calendarChannels) {
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { id: calendarChannel.connectedAccountId },
});
const connectedAccount = await this.connectedAccountRepository.findOne({
where: { id: calendarChannel.connectedAccountId, workspaceId },
});
if (!connectedAccount) {
continue;
}
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: connectedAccount.accountOwnerId },
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { id: connectedAccount.userWorkspaceId },
select: ['userId'],
});
if (!workspaceMember) {
if (!userWorkspace) {
continue;
}
const userId = workspaceMember.userId;
const userId = userWorkspace.userId;
const connectedAccountId = connectedAccount.id;
await this.accountsToReconnectService.addAccountToReconnectByKey(
@@ -3,7 +3,6 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
const EVENT_EXTERNAL_ID_FIELD_NAME = 'eventExternalId';
@@ -14,7 +13,6 @@ export const SEARCH_FIELDS_FOR_CALENDAR_CHANNEL_EVENT_ASSOCIATION: FieldTypeAndN
export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspaceEntity {
eventExternalId: string | null;
recurringEventExternalId: string | null;
calendarChannel: EntityRelation<CalendarChannelWorkspaceEntity>;
calendarChannelId: string;
calendarEvent: EntityRelation<CalendarEventWorkspaceEntity>;
calendarEventId: string;
@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { ChannelSyncResolver } from 'src/modules/connected-account/channel-sync/channel-sync.resolver';
@@ -10,8 +11,7 @@ import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-co
@Module({
imports: [
CalendarChannelDataAccessModule,
MessageChannelDataAccessModule,
TypeOrmModule.forFeature([CalendarChannelEntity, MessageChannelEntity]),
PermissionsModule,
WorkspaceDataSourceModule,
MessagingCommonModule,
@@ -1,22 +1,25 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
MessageChannelSyncStage,
} from 'twenty-shared/types';
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';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 {
CalendarEventListFetchJob,
type CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessageListFetchJob,
type MessagingMessageListFetchJobData,
@@ -35,9 +38,11 @@ export class ChannelSyncService {
private readonly messageQueueService: MessageQueueService,
@InjectMessageQueue(MessageQueue.calendarQueue)
private readonly calendarQueueService: MessageQueueService,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
) {}
async startChannelSync(input: StartChannelSyncInput): Promise<void> {
@@ -54,13 +59,13 @@ export class ChannelSyncService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
{
const messageChannels = await this.messageChannelRepository.find({
where: {
connectedAccountId,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
workspaceId,
},
);
});
for (const messageChannel of messageChannels) {
await this.messageChannelSyncStatusService.markAsMessagesListFetchScheduled(
@@ -86,20 +91,17 @@ export class ChannelSyncService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const calendarChannels = await this.calendarChannelDataAccessService.find(
workspaceId,
{
where: {
connectedAccountId,
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
const calendarChannels = await this.calendarChannelRepository.find({
where: {
connectedAccountId,
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
workspaceId,
},
);
});
for (const calendarChannel of calendarChannels) {
await this.calendarChannelDataAccessService.update(
workspaceId,
{ id: calendarChannel.id },
await this.calendarChannelRepository.update(
{ id: calendarChannel.id, workspaceId },
{
syncStage:
CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED,
@@ -1,14 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { DeleteWorkspaceMemberConnectedAccountsCleanupJob } from 'src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job';
import { ConnectedAccountWorkspaceMemberListener } from 'src/modules/connected-account/listeners/connected-account-workspace-member.listener';
import { ConnectedAccountListener } from 'src/modules/connected-account/listeners/connected-account.listener';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
@Module({
imports: [ConnectedAccountDataAccessModule, UserVarsModule],
imports: [
TypeOrmModule.forFeature([ConnectedAccountEntity, UserWorkspaceEntity]),
UserVarsModule,
],
providers: [
AccountsToReconnectService,
ConnectedAccountListener,
@@ -4,7 +4,7 @@ import { google } from 'googleapis';
import { GmailEmailAliasErrorHandlerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-error-handler.service';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class GoogleEmailAliasManagerService {
@@ -13,9 +13,7 @@ export class GoogleEmailAliasManagerService {
private readonly gmailEmailAliasErrorHandlerService: GmailEmailAliasErrorHandlerService,
) {}
public async getHandleAliases(
connectedAccount: ConnectedAccountWorkspaceEntity,
) {
public async getHandleAliases(connectedAccount: ConnectedAccountEntity) {
const oAuth2Client =
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
connectedAccount,
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class MicrosoftEmailAliasManagerService {
@@ -11,9 +11,7 @@ export class MicrosoftEmailAliasManagerService {
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
) {}
public async getHandleAliases(
connectedAccount: ConnectedAccountWorkspaceEntity,
) {
public async getHandleAliases(connectedAccount: ConnectedAccountEntity) {
const microsoftClient =
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
connectedAccount,
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailEmailAliasErrorHandlerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-error-handler.service';
import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service';
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service';
@@ -8,7 +9,10 @@ import { EmailAliasManagerService } from 'src/modules/connected-account/email-al
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
@Module({
imports: [OAuth2ClientManagerModule, ConnectedAccountDataAccessModule],
imports: [
OAuth2ClientManagerModule,
TypeOrmModule.forFeature([ConnectedAccountEntity]),
],
providers: [
EmailAliasManagerService,
GoogleEmailAliasManagerService,
@@ -1,21 +1,21 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service';
import { microsoftGraphMeResponseWithProxyAddresses } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/mocks/microsoft-api-examples';
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { EmailAliasManagerService } from './email-alias-manager.service';
describe('Email Alias Manager Service', () => {
let emailAliasManagerService: EmailAliasManagerService;
let microsoftEmailAliasManagerService: MicrosoftEmailAliasManagerService;
const mockConnectedAccountDataAccessService = {
const mockConnectedAccountRepository = {
// @ts-expect-error legacy noImplicitAny
update: jest.fn().mockResolvedValue((arg) => arg),
};
@@ -33,8 +33,8 @@ describe('Email Alias Manager Service', () => {
},
EmailAliasManagerService,
{
provide: ConnectedAccountDataAccessService,
useValue: mockConnectedAccountDataAccessService,
provide: getRepositoryToken(ConnectedAccountEntity),
useValue: mockConnectedAccountRepository,
},
{
provide: GoogleEmailAliasManagerService,
@@ -73,19 +73,21 @@ describe('Email Alias Manager Service', () => {
describe('Refresh handle aliases for Microsoft', () => {
it('Should refresh Microsoft handle aliases successfully', async () => {
const mockConnectedAccount: Partial<ConnectedAccountWorkspaceEntity> = {
const mockConnectedAccount: Partial<ConnectedAccountEntity> = {
id: 'test-id',
provider: ConnectedAccountProvider.MICROSOFT,
refreshToken: 'test-refresh-token',
};
const expectedAliases =
'bertrand2@domain.onmicrosoft.com,bertrand3@otherdomain.com';
const expectedAliases = [
'bertrand2@domain.onmicrosoft.com',
'bertrand3@otherdomain.com',
];
jest.spyOn(microsoftEmailAliasManagerService, 'getHandleAliases');
await emailAliasManagerService.refreshHandleAliases(
mockConnectedAccount as ConnectedAccountWorkspaceEntity,
mockConnectedAccount as ConnectedAccountEntity,
'test-workspace-id',
);
@@ -93,9 +95,8 @@ describe('Email Alias Manager Service', () => {
microsoftEmailAliasManagerService.getHandleAliases,
).toHaveBeenCalledWith(mockConnectedAccount);
expect(mockConnectedAccountDataAccessService.update).toHaveBeenCalledWith(
'test-workspace-id',
{ id: mockConnectedAccount.id },
expect(mockConnectedAccountRepository.update).toHaveBeenCalledWith(
{ id: mockConnectedAccount.id, workspaceId: 'test-workspace-id' },
{
handleAliases: expectedAliases,
},
@@ -1,14 +1,15 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
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 { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service';
import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
export class EmailAliasManagerService {
@@ -16,13 +17,14 @@ export class EmailAliasManagerService {
private readonly googleEmailAliasManagerService: GoogleEmailAliasManagerService,
private readonly microsoftEmailAliasManagerService: MicrosoftEmailAliasManagerService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
) {}
public async refreshHandleAliases(
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
workspaceId: string,
) {
): Promise<string[]> {
let handleAliases: string[];
switch (connectedAccount.provider) {
@@ -53,13 +55,14 @@ export class EmailAliasManagerService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.connectedAccountDataAccessService.update(
workspaceId,
{ id: connectedAccount.id },
await this.connectedAccountRepository.update(
{ id: connectedAccount.id, workspaceId },
{
handleAliases: handleAliases.join(','), // TODO: modify handleAliases to be of fieldmetadatatype array
handleAliases,
},
);
}, authContext);
return handleAliases;
}
}
@@ -5,26 +5,32 @@ import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { CalendarChannelDataAccessModule } from 'src/engine/metadata-modules/calendar-channel/data-access/calendar-channel-data-access.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service';
import { MessagingFolderSyncManagerModule } from 'src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module';
@Module({
imports: [
TypeOrmModule.forFeature([ObjectMetadataEntity]),
TypeOrmModule.forFeature([
ObjectMetadataEntity,
CalendarChannelEntity,
ConnectedAccountEntity,
MessageChannelEntity,
UserWorkspaceEntity,
]),
MessageQueueModule,
WorkspaceEventEmitterModule,
TwentyConfigModule,
TwentyORMModule,
FeatureFlagModule,
AuthModule,
CalendarChannelDataAccessModule,
ConnectedAccountDataAccessModule,
MessageChannelDataAccessModule,
MessagingFolderSyncManagerModule,
],
providers: [ImapSmtpCalDavAPIService],
exports: [ImapSmtpCalDavAPIService],
@@ -1,9 +1,15 @@
import { InjectRepository } from '@nestjs/typeorm';
import { type Repository } from 'typeorm';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
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 WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type DeleteWorkspaceMemberConnectedAccountsCleanupJobData = {
workspaceId: string;
@@ -14,7 +20,10 @@ export type DeleteWorkspaceMemberConnectedAccountsCleanupJobData = {
export class DeleteWorkspaceMemberConnectedAccountsCleanupJob {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
@Process(DeleteWorkspaceMemberConnectedAccountsCleanupJob.name)
@@ -26,8 +35,31 @@ export class DeleteWorkspaceMemberConnectedAccountsCleanupJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.connectedAccountDataAccessService.delete(workspaceId, {
accountOwnerId: workspaceMemberId,
const workspaceMemberRepo =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const member = await workspaceMemberRepo.findOne({
where: { id: workspaceMemberId },
});
if (!member) {
return;
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: member.userId, workspaceId },
});
if (!userWorkspace) {
return;
}
await this.connectedAccountRepository.delete({
userWorkspaceId: userWorkspace.id,
workspaceId,
});
}, authContext);
}
@@ -1,27 +1,31 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type ObjectRecordDeleteEvent } from 'twenty-shared/database-events';
import { type Repository } from 'typeorm';
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
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 { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class ConnectedAccountListener {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly accountsToReconnectService: AccountsToReconnectService,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
@OnDatabaseBatchEvent('connectedAccount', DatabaseEventAction.DESTROYED)
async handleDestroyedEvent(
payload: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity>
ObjectRecordDeleteEvent<ConnectedAccountEntity>
>,
) {
const workspaceId = payload.workspaceId;
@@ -29,19 +33,17 @@ export class ConnectedAccountListener {
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
for (const eventPayload of payload.events) {
const workspaceMemberId = eventPayload.properties.before.accountOwnerId;
const userWorkspaceId = eventPayload.properties.before.userWorkspaceId;
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: { id: workspaceMemberId },
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { id: userWorkspaceId },
});
const userId = workspaceMember.userId;
if (!userWorkspace) {
continue;
}
const userId = userWorkspace.userId;
const connectedAccountId = eventPayload.properties.before.id;
@@ -7,7 +7,7 @@ import { CustomError, isDefined } from 'twenty-shared/utils';
import { GoogleOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { OAuth2ClientManagerExceptionCode } from 'src/modules/connected-account/oauth2-client-manager/exceptions/oauth2-client-manager.exceptions';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class OAuth2ClientManagerService {
@@ -17,10 +17,7 @@ export class OAuth2ClientManagerService {
) {}
public async getGoogleOAuth2Client(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken'
>,
connectedAccount: Pick<ConnectedAccountEntity, 'provider' | 'refreshToken'>,
): Promise<Auth.OAuth2Client> {
if (!isDefined(connectedAccount.refreshToken)) {
throw new CustomError(
@@ -35,10 +32,7 @@ export class OAuth2ClientManagerService {
}
public async getMicrosoftOAuth2Client(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'accessToken'
>,
connectedAccount: Pick<ConnectedAccountEntity, 'provider' | 'accessToken'>,
): Promise<Client> {
if (!isDefined(connectedAccount.accessToken)) {
throw new CustomError(
@@ -1,77 +0,0 @@
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
@WorkspaceQueryHook(`connectedAccount.destroyOne`)
export class ConnectedAccountDeleteOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async execute(
authContext: WorkspaceAuthContext,
_objectName: string,
payload: DeleteOneResolverArgs,
): Promise<DeleteOneResolverArgs> {
const connectedAccountId = payload.id;
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
const messageChannels =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
return this.messageChannelDataAccessService.find(workspace.id, {
connectedAccountId,
});
},
authContext,
);
const { flatObjectMetadataMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId: workspace.id,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
);
const flatObjectMetadata = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.messageChannel.universalIdentifier,
});
// TODO: handle cascade events for delete
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'messageChannel',
action: DatabaseEventAction.DESTROYED,
objectMetadata: flatObjectMetadata,
events: messageChannels.map((messageChannel) => ({
recordId: messageChannel.id,
properties: {
before: messageChannel,
},
})),
workspaceId: workspace.id,
});
return payload;
}
}
@@ -1,18 +0,0 @@
import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ConnectedAccountDeleteOnePreQueryHook } from 'src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook';
@Module({
imports: [
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
MessageChannelDataAccessModule,
],
providers: [ConnectedAccountDeleteOnePreQueryHook],
})
export class ConnectedAccountQueryHookModule {}
@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/google-api-refresh-access-token.module';
import { MicrosoftAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/microsoft-api-refresh-access-token.module';
import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
@@ -9,7 +10,7 @@ import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-acco
@Module({
imports: [
JwtModule,
ConnectedAccountDataAccessModule,
TypeOrmModule.forFeature([ConnectedAccountEntity]),
GoogleAPIRefreshAccessTokenModule,
MicrosoftAPIRefreshAccessTokenModule,
],
@@ -1,8 +1,9 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service';
import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service';
@@ -10,7 +11,6 @@ import {
ConnectedAccountRefreshAccessTokenException,
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { ConnectedAccountRefreshTokensService } from './connected-account-refresh-tokens.service';
@@ -18,7 +18,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
let service: ConnectedAccountRefreshTokensService;
let googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService;
let microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService;
let connectedAccountDataAccessService: ConnectedAccountDataAccessService;
let connectedAccountRepository: { update: jest.Mock };
const mockWorkspaceId = 'workspace-123';
const mockConnectedAccountId = 'account-456';
@@ -52,7 +52,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
},
},
{
provide: ConnectedAccountDataAccessService,
provide: getRepositoryToken(ConnectedAccountEntity),
useValue: {
update: jest.fn(),
},
@@ -71,10 +71,9 @@ describe('ConnectedAccountRefreshTokensService', () => {
module.get<MicrosoftAPIRefreshAccessTokenService>(
MicrosoftAPIRefreshAccessTokenService,
);
connectedAccountDataAccessService =
module.get<ConnectedAccountDataAccessService>(
ConnectedAccountDataAccessService,
);
connectedAccountRepository = module.get(
getRepositoryToken(ConnectedAccountEntity),
);
});
afterEach(() => {
@@ -89,7 +88,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
lastCredentialsRefreshedAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const result = await service.refreshAndSaveTokens(
connectedAccount,
@@ -103,7 +102,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
expect(
microsoftAPIRefreshAccessTokenService.refreshTokens,
).not.toHaveBeenCalled();
expect(connectedAccountDataAccessService.update).not.toHaveBeenCalled();
expect(connectedAccountRepository.update).not.toHaveBeenCalled();
});
it('should refresh and save new Microsoft token when expired (lastCredentialsRefreshedAt is old)', async () => {
@@ -113,7 +112,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const newTokens = {
accessToken: mockNewAccessToken,
@@ -133,9 +132,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
expect(
microsoftAPIRefreshAccessTokenService.refreshTokens,
).toHaveBeenCalledWith(mockRefreshToken);
expect(connectedAccountDataAccessService.update).toHaveBeenCalledWith(
mockWorkspaceId,
{ id: mockConnectedAccountId },
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
{ id: mockConnectedAccountId, workspaceId: mockWorkspaceId },
expect.objectContaining({
...newTokens,
lastCredentialsRefreshedAt: expect.any(Date),
@@ -150,7 +148,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const newTokens = {
accessToken: mockNewAccessToken,
@@ -170,9 +168,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
expect(
googleAPIRefreshAccessTokenService.refreshTokens,
).toHaveBeenCalledWith(mockRefreshToken);
expect(connectedAccountDataAccessService.update).toHaveBeenCalledWith(
mockWorkspaceId,
{ id: mockConnectedAccountId },
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
{ id: mockConnectedAccountId, workspaceId: mockWorkspaceId },
expect.objectContaining({
...newTokens,
lastCredentialsRefreshedAt: expect.any(Date),
@@ -187,7 +184,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
lastCredentialsRefreshedAt: null,
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const newTokens = {
accessToken: mockNewAccessToken,
@@ -207,9 +204,8 @@ describe('ConnectedAccountRefreshTokensService', () => {
expect(
microsoftAPIRefreshAccessTokenService.refreshTokens,
).toHaveBeenCalledWith(mockRefreshToken);
expect(connectedAccountDataAccessService.update).toHaveBeenCalledWith(
mockWorkspaceId,
{ id: mockConnectedAccountId },
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
{ id: mockConnectedAccountId, workspaceId: mockWorkspaceId },
expect.objectContaining({
...newTokens,
lastCredentialsRefreshedAt: expect.any(Date),
@@ -224,7 +220,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
accessToken: mockAccessToken,
refreshToken: null,
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
} as unknown as ConnectedAccountWorkspaceEntity;
} as unknown as ConnectedAccountEntity;
await expect(
service.refreshAndSaveTokens(connectedAccount, mockWorkspaceId),
@@ -243,7 +239,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const invalidGrantError = new ConnectedAccountRefreshAccessTokenException(
'Microsoft OAuth error: invalid_grant - Token has been revoked',
@@ -271,7 +267,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const networkError = new ConnectedAccountRefreshAccessTokenException(
'Google refresh token network error: ECONNRESET - Network error',
@@ -296,7 +292,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
id: mockConnectedAccountId,
provider: ConnectedAccountProvider.MICROSOFT,
lastCredentialsRefreshedAt: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const result = await service.isAccessTokenStillValid(connectedAccount);
@@ -308,7 +304,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
id: mockConnectedAccountId,
provider: ConnectedAccountProvider.GOOGLE,
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const result = await service.isAccessTokenStillValid(connectedAccount);
@@ -320,7 +316,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
id: mockConnectedAccountId,
provider: ConnectedAccountProvider.MICROSOFT,
lastCredentialsRefreshedAt: null,
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const result = await service.isAccessTokenStillValid(connectedAccount);
@@ -332,7 +328,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
id: mockConnectedAccountId,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
lastCredentialsRefreshedAt: null,
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const result = await service.isAccessTokenStillValid(connectedAccount);
@@ -344,7 +340,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
id: mockConnectedAccountId,
provider: ConnectedAccountProvider.OIDC,
lastCredentialsRefreshedAt: null,
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const result = await service.isAccessTokenStillValid(connectedAccount);
@@ -356,7 +352,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
id: mockConnectedAccountId,
provider: ConnectedAccountProvider.SAML,
lastCredentialsRefreshedAt: null,
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const result = await service.isAccessTokenStillValid(connectedAccount);
@@ -372,7 +368,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
lastCredentialsRefreshedAt: null,
} as unknown as ConnectedAccountWorkspaceEntity;
} as unknown as ConnectedAccountEntity;
const result = await service.refreshAndSaveTokens(
connectedAccount,
@@ -389,7 +385,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
expect(
microsoftAPIRefreshAccessTokenService.refreshTokens,
).not.toHaveBeenCalled();
expect(connectedAccountDataAccessService.update).not.toHaveBeenCalled();
expect(connectedAccountRepository.update).not.toHaveBeenCalled();
});
it('should reuse existing tokens for SAML without attempting a refresh', async () => {
@@ -399,7 +395,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
accessToken: mockAccessToken,
refreshToken: mockRefreshToken,
lastCredentialsRefreshedAt: null,
} as unknown as ConnectedAccountWorkspaceEntity;
} as unknown as ConnectedAccountEntity;
const result = await service.refreshAndSaveTokens(
connectedAccount,
@@ -416,7 +412,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
expect(
microsoftAPIRefreshAccessTokenService.refreshTokens,
).not.toHaveBeenCalled();
expect(connectedAccountDataAccessService.update).not.toHaveBeenCalled();
expect(connectedAccountRepository.update).not.toHaveBeenCalled();
});
});
});
@@ -1,9 +1,11 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
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 { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service';
@@ -12,7 +14,6 @@ import {
ConnectedAccountRefreshAccessTokenException,
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type ConnectedAccountTokens = {
accessToken: string;
@@ -31,11 +32,12 @@ export class ConnectedAccountRefreshTokensService {
private readonly googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService,
private readonly microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
) {}
async refreshAndSaveTokens(
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
workspaceId: string,
): Promise<ConnectedAccountTokens> {
const { refreshToken, accessToken } = connectedAccount;
@@ -80,9 +82,8 @@ export class ConnectedAccountRefreshTokensService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.connectedAccountDataAccessService.update(
workspaceId,
{ id: connectedAccount.id },
await this.connectedAccountRepository.update(
{ id: connectedAccount.id, workspaceId },
{
...connectedAccountTokens,
lastCredentialsRefreshedAt: new Date(),
@@ -94,7 +95,7 @@ export class ConnectedAccountRefreshTokensService {
}
async isAccessTokenStillValid(
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<boolean> {
switch (connectedAccount.provider) {
case ConnectedAccountProvider.GOOGLE:
@@ -126,7 +127,7 @@ export class ConnectedAccountRefreshTokensService {
}
async refreshTokens(
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
refreshToken: string,
workspaceId: string,
): Promise<ConnectedAccountTokens> {
@@ -1,18 +1,18 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service';
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
import { type EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
jest.mock('uuid', () => ({
v4: jest.fn(() => 'mocked-uuid'),
@@ -21,21 +21,38 @@ jest.mock('uuid', () => ({
describe('ImapSmtpCalDavAPIService', () => {
let service: ImapSmtpCalDavAPIService;
const mockConnectedAccountDataAccessService = {
const mockTransactionManagerSave = jest.fn();
const mockTransactionManager = {
getRepository: jest
.fn()
.mockReturnValue({ save: mockTransactionManagerSave }),
};
const mockConnectedAccountRepository = {
findOne: jest.fn(),
save: jest.fn(),
manager: {
transaction: jest.fn((callback) => callback(mockTransactionManager)),
},
};
const mockMessageChannelDataAccessService = {
const mockMessageChannelRepository = {
findOne: jest.fn(),
};
const mockCalendarChannelDataAccessService = {
const mockCalendarChannelRepository = {
findOne: jest.fn(),
};
const mockWorkspaceDataSource = {
transaction: jest.fn((callback) => callback({})),
const mockUserWorkspaceRepository = {
findOne: jest.fn().mockResolvedValue({ id: 'user-workspace-id' }),
};
const mockWorkspaceMemberRepository = {
findOne: jest.fn().mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
}),
};
const mockCreateMessageChannelService = {
@@ -53,9 +70,14 @@ describe('ImapSmtpCalDavAPIService', () => {
{
provide: GlobalWorkspaceOrmManager,
useValue: {
getGlobalWorkspaceDataSource: jest
getRepository: jest
.fn()
.mockResolvedValue(mockWorkspaceDataSource),
.mockImplementation((_workspaceId, entity) => {
if (entity === 'workspaceMember')
return mockWorkspaceMemberRepository;
return {};
}),
executeInWorkspaceContext: jest
.fn()
@@ -71,16 +93,26 @@ describe('ImapSmtpCalDavAPIService', () => {
useValue: mockCreateCalendarChannelService,
},
{
provide: ConnectedAccountDataAccessService,
useValue: mockConnectedAccountDataAccessService,
provide: getRepositoryToken(ConnectedAccountEntity),
useValue: mockConnectedAccountRepository,
},
{
provide: MessageChannelDataAccessService,
useValue: mockMessageChannelDataAccessService,
provide: getRepositoryToken(MessageChannelEntity),
useValue: mockMessageChannelRepository,
},
{
provide: CalendarChannelDataAccessService,
useValue: mockCalendarChannelDataAccessService,
provide: getRepositoryToken(CalendarChannelEntity),
useValue: mockCalendarChannelRepository,
},
{
provide: getRepositoryToken(UserWorkspaceEntity),
useValue: mockUserWorkspaceRepository,
},
{
provide: SyncMessageFoldersService,
useValue: {
syncMessageFolders: jest.fn().mockResolvedValue([]),
},
},
],
}).compile();
@@ -113,23 +145,27 @@ describe('ImapSmtpCalDavAPIService', () => {
};
it('should create new account with message channel when account does not exist and IMAP is configured', async () => {
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null);
mockMessageChannelDataAccessService.findOne.mockResolvedValue(null);
mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null);
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
await service.processAccount(baseInput);
expect(mockConnectedAccountDataAccessService.save).toHaveBeenCalledWith(
'workspace-id',
{
id: 'mocked-uuid',
handle: 'test@example.com',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
connectionParameters: baseInput.connectionParameters,
accountOwnerId: 'workspace-member-id',
},
{},
);
expect(mockTransactionManagerSave).toHaveBeenCalledWith({
id: 'mocked-uuid',
handle: 'test@example.com',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
connectionParameters: baseInput.connectionParameters,
userWorkspaceId: 'user-workspace-id',
workspaceId: 'workspace-id',
});
expect(
mockCreateMessageChannelService.createMessageChannel,
@@ -137,7 +173,7 @@ describe('ImapSmtpCalDavAPIService', () => {
workspaceId: 'workspace-id',
connectedAccountId: 'mocked-uuid',
handle: 'test@example.com',
manager: {},
transactionManager: mockTransactionManager,
});
expect(
@@ -149,29 +185,34 @@ describe('ImapSmtpCalDavAPIService', () => {
const existingAccount = {
id: 'existing-account-id',
handle: 'test@example.com',
accountOwnerId: 'workspace-member-id',
userWorkspaceId: 'user-workspace-id',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
const existingMessageChannel = {
id: 'existing-message-channel-id',
connectedAccountId: 'existing-account-id',
} as MessageChannelWorkspaceEntity;
} as MessageChannelEntity;
const existingCalendarChannel = {
id: 'existing-calendar-channel-id',
connectedAccountId: 'existing-account-id',
} as CalendarChannelWorkspaceEntity;
} as CalendarChannelEntity;
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(
existingAccount,
);
mockMessageChannelDataAccessService.findOne.mockResolvedValue(
mockConnectedAccountRepository.findOne.mockResolvedValue(existingAccount);
mockMessageChannelRepository.findOne.mockResolvedValue(
existingMessageChannel,
);
mockCalendarChannelDataAccessService.findOne.mockResolvedValue(
mockCalendarChannelRepository.findOne.mockResolvedValue(
existingCalendarChannel,
);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
const inputWithConnectedAccountId = {
...baseInput,
@@ -180,17 +221,14 @@ describe('ImapSmtpCalDavAPIService', () => {
await service.processAccount(inputWithConnectedAccountId);
expect(mockConnectedAccountDataAccessService.save).toHaveBeenCalledWith(
'workspace-id',
{
id: 'existing-account-id',
handle: 'test@example.com',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
connectionParameters: baseInput.connectionParameters,
accountOwnerId: 'workspace-member-id',
},
{},
);
expect(mockTransactionManagerSave).toHaveBeenCalledWith({
id: 'existing-account-id',
handle: 'test@example.com',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
connectionParameters: baseInput.connectionParameters,
userWorkspaceId: 'user-workspace-id',
workspaceId: 'workspace-id',
});
expect(
mockCreateMessageChannelService.createMessageChannel,
@@ -213,9 +251,16 @@ describe('ImapSmtpCalDavAPIService', () => {
} as EmailAccountConnectionParameters,
};
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null);
mockMessageChannelDataAccessService.findOne.mockResolvedValue(null);
mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null);
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
await service.processAccount(imapOnlyInput);
@@ -241,9 +286,16 @@ describe('ImapSmtpCalDavAPIService', () => {
} as EmailAccountConnectionParameters,
};
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null);
mockMessageChannelDataAccessService.findOne.mockResolvedValue(null);
mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null);
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
await service.processAccount(caldavOnlyInput);
@@ -275,9 +327,16 @@ describe('ImapSmtpCalDavAPIService', () => {
} as EmailAccountConnectionParameters,
};
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null);
mockMessageChannelDataAccessService.findOne.mockResolvedValue(null);
mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null);
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
await service.processAccount(imapSmtpInput);
@@ -316,9 +375,16 @@ describe('ImapSmtpCalDavAPIService', () => {
} as EmailAccountConnectionParameters,
};
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null);
mockMessageChannelDataAccessService.findOne.mockResolvedValue(null);
mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null);
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
await service.processAccount(fullConfigInput);
@@ -334,39 +400,42 @@ describe('ImapSmtpCalDavAPIService', () => {
const existingAccount = {
id: 'existing-account-id',
handle: 'test@example.com',
accountOwnerId: 'workspace-member-id',
userWorkspaceId: 'user-workspace-id',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
} as ConnectedAccountWorkspaceEntity;
} as ConnectedAccountEntity;
mockConnectedAccountDataAccessService.findOne.mockResolvedValueOnce(
mockConnectedAccountRepository.findOne.mockResolvedValueOnce(
existingAccount,
);
mockMessageChannelDataAccessService.findOne.mockResolvedValue(null);
mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
await service.processAccount(baseInput);
expect(
mockConnectedAccountDataAccessService.findOne,
).toHaveBeenCalledWith('workspace-id', {
expect(mockConnectedAccountRepository.findOne).toHaveBeenCalledWith({
where: {
handle: 'test@example.com',
accountOwnerId: 'workspace-member-id',
userWorkspaceId: 'user-workspace-id',
workspaceId: 'workspace-id',
},
});
expect(mockConnectedAccountDataAccessService.save).toHaveBeenCalledWith(
'workspace-id',
{
id: 'existing-account-id',
handle: 'test@example.com',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
connectionParameters: baseInput.connectionParameters,
accountOwnerId: 'workspace-member-id',
},
{},
);
expect(mockTransactionManagerSave).toHaveBeenCalledWith({
id: 'existing-account-id',
handle: 'test@example.com',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
connectionParameters: baseInput.connectionParameters,
userWorkspaceId: 'user-workspace-id',
workspaceId: 'workspace-id',
});
});
it('should not create channels when neither IMAP nor CALDAV is configured', async () => {
@@ -383,9 +452,16 @@ describe('ImapSmtpCalDavAPIService', () => {
} as EmailAccountConnectionParameters,
};
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null);
mockMessageChannelDataAccessService.findOne.mockResolvedValue(null);
mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null);
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
await service.processAccount(smtpOnlyInput);
@@ -398,15 +474,22 @@ describe('ImapSmtpCalDavAPIService', () => {
});
it('should handle transaction correctly', async () => {
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(null);
mockMessageChannelDataAccessService.findOne.mockResolvedValue(null);
mockCalendarChannelDataAccessService.findOne.mockResolvedValue(null);
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
});
await service.processAccount(baseInput);
expect(mockWorkspaceDataSource.transaction).toHaveBeenCalledWith(
expect.any(Function),
);
expect(
mockConnectedAccountRepository.manager.transaction,
).toHaveBeenCalledWith(expect.any(Function));
});
});
});
@@ -1,45 +1,58 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { EntityManager, Repository } from 'typeorm';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service';
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
import { type EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.service';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
@Injectable()
export class ImapSmtpCalDavAPIService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(CalendarChannelEntity)
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly createMessageChannelService: CreateMessageChannelService,
private readonly createCalendarChannelService: CreateCalendarChannelService,
private readonly syncMessageFoldersService: SyncMessageFoldersService,
) {}
async getImapSmtpCaldavConnectedAccount(
workspaceId: string,
id: string,
): Promise<ConnectedAccountWorkspaceEntity | null> {
): Promise<ConnectedAccountEntity | null> {
const authContext = buildSystemAuthContext(workspaceId);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { id, provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV },
});
const connectedAccount = await this.connectedAccountRepository.findOne({
where: {
id,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
workspaceId,
},
});
return connectedAccount as ConnectedAccountWorkspaceEntity | null;
return connectedAccount;
},
authContext,
);
@@ -59,29 +72,59 @@ export class ImapSmtpCalDavAPIService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
// Resolve userWorkspaceId from workspaceMemberId
const workspaceMemberRepo =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const member = await workspaceMemberRepo.findOne({
where: { id: workspaceMemberId },
});
if (!member) {
throw new NotFoundError(
`Workspace member with id ${workspaceMemberId} not found`,
);
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: member.userId, workspaceId },
});
if (!userWorkspace) {
throw new NotFoundError(
`UserWorkspace not found for userId ${member.userId} in workspace ${workspaceId}`,
);
}
const userWorkspaceId = userWorkspace.id;
const existingAccount = connectedAccountId
? await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { id: connectedAccountId },
? await this.connectedAccountRepository.findOne({
where: { id: connectedAccountId, workspaceId },
})
: await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { handle, accountOwnerId: workspaceMemberId },
: await this.connectedAccountRepository.findOne({
where: {
handle,
userWorkspaceId,
workspaceId,
},
});
const newOrExistingAccountId =
existingAccount?.id ?? connectedAccountId ?? v4();
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
const existingMessageChannel = existingAccount
? await this.messageChannelDataAccessService.findOne(workspaceId, {
where: { connectedAccountId: existingAccount.id },
? await this.messageChannelRepository.findOne({
where: { connectedAccountId: existingAccount.id, workspaceId },
})
: null;
const existingCalendarChannel = existingAccount
? await this.calendarChannelDataAccessService.findOne(workspaceId, {
where: { connectedAccountId: existingAccount.id },
? await this.calendarChannelRepository.findOne({
where: { connectedAccountId: existingAccount.id, workspaceId },
})
: null;
@@ -93,26 +136,25 @@ export class ImapSmtpCalDavAPIService {
!isDefined(existingCalendarChannel) &&
Boolean(input.connectionParameters.CALDAV);
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
await this.connectedAccountDataAccessService.save(
workspaceId,
{
await this.connectedAccountRepository.manager.transaction(
async (transactionManager: EntityManager) => {
await transactionManager
.getRepository(ConnectedAccountEntity)
.save({
id: newOrExistingAccountId,
handle,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
connectionParameters: input.connectionParameters,
accountOwnerId: workspaceMemberId,
},
manager,
);
userWorkspaceId,
workspaceId,
});
if (shouldCreateMessageChannel) {
await this.createMessageChannelService.createMessageChannel({
workspaceId,
connectedAccountId: newOrExistingAccountId,
handle,
manager,
transactionManager,
});
}
@@ -121,12 +163,31 @@ export class ImapSmtpCalDavAPIService {
workspaceId,
connectedAccountId: newOrExistingAccountId,
handle,
manager,
transactionManager,
});
}
},
);
if (shouldCreateMessageChannel) {
const newMessageChannel = await this.messageChannelRepository.findOne(
{
where: {
connectedAccountId: newOrExistingAccountId,
workspaceId,
},
relations: ['connectedAccount', 'messageFolders'],
},
);
if (isDefined(newMessageChannel)) {
await this.syncMessageFoldersService.syncMessageFolders({
messageChannel: newMessageChannel,
workspaceId,
});
}
}
return newOrExistingAccountId;
},
authContext,
@@ -7,8 +7,6 @@ import { type ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-cal
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
const HANDLE_FIELD_NAME = 'handle';
@@ -25,11 +23,9 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
lastCredentialsRefreshedAt: Date | null;
lastSyncHistoryId: string | null;
authFailedAt: Date | null;
handleAliases: string | null;
handleAliases: string[] | null;
scopes: string[] | null;
connectionParameters: ImapSmtpCaldavParams | null;
accountOwner: EntityRelation<WorkspaceMemberWorkspaceEntity>;
accountOwnerId: string;
messageChannels: EntityRelation<MessageChannelWorkspaceEntity[]>;
calendarChannels: EntityRelation<CalendarChannelWorkspaceEntity[]>;
}
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@@ -13,7 +14,7 @@ import { CreatePersonService } from 'src/modules/contact-creation-manager/servic
@Module({
imports: [
WorkspaceDataSourceModule,
TypeOrmModule.forFeature([FeatureFlagEntity]),
TypeOrmModule.forFeature([FeatureFlagEntity, UserWorkspaceEntity]),
TypeOrmModule.forFeature([ObjectMetadataEntity, FieldMetadataEntity]),
SecureHttpClientModule,
],
@@ -3,12 +3,12 @@ import { type FieldActorSource } from 'twenty-shared/types';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { CreateCompanyAndPersonService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service';
export type CreateCompanyAndContactJobData = {
workspaceId: string;
connectedAccount: ConnectedAccountWorkspaceEntity;
connectedAccount: ConnectedAccountEntity;
contactsToCreate: {
displayName: string;
handle: string;
@@ -1,10 +1,12 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { FieldActorSource } from 'twenty-shared/types';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { CreateCompanyAndPersonService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service';
import { CreateCompanyService } from 'src/modules/contact-creation-manager/services/create-company.service';
import { CreatePersonService } from 'src/modules/contact-creation-manager/services/create-person.service';
@@ -19,7 +21,7 @@ describe('CreateCompanyAndPersonService', () => {
accountOwner: {
id: 'workspace-member-1',
},
} as unknown as ConnectedAccountWorkspaceEntity;
} as unknown as ConnectedAccountEntity;
beforeEach(async () => {
const mockCreateCompaniesService = {
@@ -48,6 +50,12 @@ describe('CreateCompanyAndPersonService', () => {
provide: ExceptionHandlerService,
useValue: {},
},
{
provide: getRepositoryToken(UserWorkspaceEntity),
useValue: {
findOne: jest.fn(),
},
},
],
}).compile();
@@ -106,6 +114,7 @@ describe('CreateCompanyAndPersonService', () => {
mockExistingPeople,
FieldActorSource.CALENDAR,
mockConnectedAccount,
null,
);
expect(result.contactsThatNeedPersonCreate).toHaveLength(1);
@@ -121,6 +130,7 @@ describe('CreateCompanyAndPersonService', () => {
mockExistingPeople,
FieldActorSource.CALENDAR,
mockConnectedAccount,
null,
);
expect(result.contactsThatNeedPersonRestore).toHaveLength(2);
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import chunk from 'lodash.chunk';
@@ -8,13 +9,14 @@ import {
type FieldActorSource,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type DeepPartial } from 'typeorm';
import { type DeepPartial, type Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
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 ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { CONTACTS_CREATION_BATCH_SIZE } from 'src/modules/contact-creation-manager/constants/contacts-creation-batch-size.constant';
import { CreateCompanyService } from 'src/modules/contact-creation-manager/services/create-company.service';
import { CreatePersonService } from 'src/modules/contact-creation-manager/services/create-person.service';
@@ -36,13 +38,16 @@ export class CreateCompanyAndPersonService {
private readonly createCompaniesService: CreateCompanyService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly exceptionHandlerService: ExceptionHandlerService,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
async createCompaniesAndPeople(
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
contactsToCreate: Contact[],
workspaceId: string,
source: FieldActorSource,
accountOwner: WorkspaceMemberWorkspaceEntity | null,
): Promise<DeepPartial<PersonWorkspaceEntity>[]> {
if (!contactsToCreate || contactsToCreate.length === 0) {
return [];
@@ -105,6 +110,7 @@ export class CreateCompanyAndPersonService {
alreadyCreatedPeople,
source,
connectedAccount,
accountOwner,
);
const companiesMap =
@@ -117,7 +123,7 @@ export class CreateCompanyAndPersonService {
contactsToCreate: contactsThatNeedPersonCreate,
createdBy: {
source: source,
workspaceMember: connectedAccount.accountOwner,
workspaceMember: accountOwner,
context: {
provider: connectedAccount.provider,
},
@@ -148,7 +154,7 @@ export class CreateCompanyAndPersonService {
}
async createCompaniesAndPeopleAndUpdateParticipants(
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
contactsToCreate: Contact[],
workspaceId: string,
source: FieldActorSource,
@@ -160,29 +166,31 @@ export class CreateCompanyAndPersonService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
if (!connectedAccount.accountOwner) {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
WorkspaceMemberWorkspaceEntity,
);
const accountOwner =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { id: connectedAccount.userWorkspaceId },
});
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
id: connectedAccount.accountOwnerId,
},
});
if (!userWorkspace) {
throw new Error(
`UserWorkspace with id ${connectedAccount.userWorkspaceId} not found`,
);
}
if (!workspaceMember) {
throw new Error(
`Workspace member with id ${connectedAccount.accountOwnerId} not found in workspace ${workspaceId}`,
);
}
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
WorkspaceMemberWorkspaceEntity,
);
connectedAccount.accountOwner = workspaceMember;
}
}, authContext);
return workspaceMemberRepository.findOne({
where: { userId: userWorkspace.userId },
});
},
authContext,
);
for (const contactsBatch of contactsBatches) {
try {
@@ -191,6 +199,7 @@ export class CreateCompanyAndPersonService {
contactsBatch,
workspaceId,
source,
accountOwner,
);
} catch (error) {
this.exceptionHandlerService.captureExceptions([error], {
@@ -206,7 +215,8 @@ export class CreateCompanyAndPersonService {
uniqueContacts: Contact[],
alreadyCreatedPeople: PersonWorkspaceEntity[],
source: FieldActorSource,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
accountOwner: WorkspaceMemberWorkspaceEntity | null,
) {
const shouldCreateOrRestorePeopleByHandleMap = new Map<
string,
@@ -288,7 +298,7 @@ export class CreateCompanyAndPersonService {
return {
domainName: companyDomainName,
createdBySource: source,
createdByWorkspaceMember: connectedAccount.accountOwner,
createdByWorkspaceMember: accountOwner,
createdByContext: {
provider: connectedAccount.provider,
},
@@ -1,6 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type Contact } from 'src/modules/contact-creation-manager/types/contact.type';
import { getDomainNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-domain-name-from-handle.util';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -8,7 +8,7 @@ import { isWorkDomain } from 'src/utils/is-work-email';
export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
contacts: Contact[],
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
workspaceMembers: WorkspaceMemberWorkspaceEntity[],
): Contact[] {
if (!isDefined(connectedAccount.handle)) {
@@ -20,7 +20,7 @@ export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
const allHandles = [
connectedAccount.handle.toLowerCase(),
...(connectedAccount.handleAliases?.split(',') || []).map((handle) =>
...(connectedAccount.handleAliases || []).map((handle) =>
handle.toLowerCase(),
),
];
@@ -1,31 +1,24 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type ObjectRecordCreateEvent } from 'twenty-shared/database-events';
import { MessageParticipantRole } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
And,
Any,
type FindManyOptions,
ILike,
In,
IsNull,
Not,
Or,
} from 'typeorm';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { And, Any, ILike, In, Not, Or, Repository } from 'typeorm';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type BlocklistItemDeleteMessagesJobData = WorkspaceEventBatch<
ObjectRecordCreateEvent<BlocklistWorkspaceEntity>
@@ -39,7 +32,12 @@ export class BlocklistItemDeleteMessagesJob {
constructor(
private readonly threadCleanerService: MessagingMessageCleanerService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
@Process(BlocklistItemDeleteMessagesJob.name)
@@ -90,6 +88,12 @@ export class BlocklistItemDeleteMessagesJob {
'messageChannelMessageAssociation',
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
for (const workspaceMemberId of handlesToDeleteByWorkspaceMemberIdMap.keys()) {
const handles =
handlesToDeleteByWorkspaceMemberIdMap.get(workspaceMemberId);
@@ -103,23 +107,46 @@ export class BlocklistItemDeleteMessagesJob {
MessageParticipantRole.TO,
] as const;
const messageChannels =
await this.messageChannelDataAccessService.findMany(workspaceId, {
select: {
id: true,
handle: true,
connectedAccount: {
handleAliases: true,
},
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: workspaceMemberId },
});
if (!workspaceMember) {
continue;
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: workspaceMember.userId, workspaceId },
});
if (!userWorkspace) {
continue;
}
const connectedAccounts = await this.connectedAccountRepository.find({
where: { userWorkspaceId: userWorkspace.id, workspaceId },
});
const connectedAccountIds = connectedAccounts.map((ca) => ca.id);
if (connectedAccountIds.length === 0) {
continue;
}
const messageChannels = await this.messageChannelRepository.find({
select: {
id: true,
handle: true,
connectedAccount: {
handleAliases: true,
},
where: {
connectedAccount: {
accountOwnerId: workspaceMemberId,
deletedAt: IsNull(),
},
},
relations: ['connectedAccount'],
} as FindManyOptions<MessageChannelWorkspaceEntity>);
},
where: {
connectedAccountId: In(connectedAccountIds),
workspaceId,
},
relations: { connectedAccount: true },
});
for (const messageChannel of messageChannels) {
const messageChannelHandles = [messageChannel.handle];
@@ -1,18 +1,22 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Not } from 'typeorm';
import { In, Not, Repository } from 'typeorm';
import { type ObjectRecordDeleteEvent } from 'twenty-shared/database-events';
import { MessageChannelSyncStage } from 'twenty-shared/types';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type BlocklistReimportMessagesJobData = WorkspaceEventBatch<
ObjectRecordDeleteEvent<BlocklistWorkspaceEntity>
@@ -25,7 +29,12 @@ export type BlocklistReimportMessagesJobData = WorkspaceEventBatch<
export class BlocklistReimportMessagesJob {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly messagingChannelSyncStatusService: MessageChannelSyncStatusService,
) {}
@@ -36,19 +45,49 @@ export class BlocklistReimportMessagesJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
for (const eventPayload of data.events) {
const workspaceMemberId =
eventPayload.properties.before.workspaceMemberId;
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
{
connectedAccount: {
accountOwnerId: workspaceMemberId,
},
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: workspaceMemberId },
});
if (!workspaceMember) {
continue;
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: workspaceMember.userId, workspaceId },
});
if (!userWorkspace) {
continue;
}
const connectedAccounts = await this.connectedAccountRepository.find({
where: { userWorkspaceId: userWorkspace.id, workspaceId },
});
const connectedAccountIds = connectedAccounts.map((ca) => ca.id);
if (connectedAccountIds.length === 0) {
continue;
}
const messageChannels = await this.messageChannelRepository.find({
where: {
connectedAccountId: In(connectedAccountIds),
syncStage: Not(MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING),
workspaceId,
},
);
});
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
messageChannels.map((messageChannel) => messageChannel.id),
@@ -1,6 +1,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { BlocklistItemDeleteMessagesJob } from 'src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job';
import { BlocklistReimportMessagesJob } from 'src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job';
import { MessagingBlocklistListener } from 'src/modules/messaging/blocklist-manager/listeners/messaging-blocklist.listener';
@@ -11,7 +14,11 @@ import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cle
imports: [
MessagingCommonModule,
MessagingMessageCleanerModule,
MessageChannelDataAccessModule,
TypeOrmModule.forFeature([
MessageChannelEntity,
ConnectedAccountEntity,
UserWorkspaceEntity,
]),
],
providers: [
MessagingBlocklistListener,
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
@@ -13,11 +13,13 @@ import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/se
@Module({
imports: [
WorkspaceDataSourceModule,
TypeOrmModule.forFeature([FeatureFlagEntity]),
TypeOrmModule.forFeature([
MessageChannelEntity,
MessageFolderEntity,
ConnectedAccountEntity,
UserWorkspaceEntity,
]),
ConnectedAccountModule,
ConnectedAccountDataAccessModule,
MessageChannelDataAccessModule,
MessageFolderDataAccessModule,
MetricsModule,
],
providers: [MessageChannelSyncStatusService],
@@ -1,10 +1,13 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelVisibility } from 'twenty-shared/types';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
import { ApplyMessagesVisibilityRestrictionsService } from './apply-messages-visibility-restrictions.service';
@@ -39,7 +42,15 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
findOneByOrFail: jest.fn(),
};
const mockConnectedAccountDataAccessService = {
const mockConnectedAccountRepository = {
find: jest.fn(),
};
const mockUserWorkspaceRepository = {
findOne: jest.fn(),
};
const mockMessageChannelRepository = {
find: jest.fn(),
};
@@ -66,8 +77,16 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
useValue: mockGlobalWorkspaceOrmManager,
},
{
provide: ConnectedAccountDataAccessService,
useValue: mockConnectedAccountDataAccessService,
provide: getRepositoryToken(ConnectedAccountEntity),
useValue: mockConnectedAccountRepository,
},
{
provide: getRepositoryToken(UserWorkspaceEntity),
useValue: mockUserWorkspaceRepository,
},
{
provide: getRepositoryToken(MessageChannelEntity),
useValue: mockMessageChannelRepository,
},
],
}).compile();
@@ -87,10 +106,14 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
messageChannelId: 'messageChannelId',
},
]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
]);
@@ -107,7 +130,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
item.subject === 'Test Subject' && item.text === 'Test Message',
),
).toBe(true);
expect(mockConnectedAccountDataAccessService.find).not.toHaveBeenCalled();
expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled();
});
it('should return message without obfuscated subject and with obfuscated text if the visibility is SUBJECT', async () => {
@@ -118,17 +141,27 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.SUBJECT,
},
messageChannelId: 'messageChannelId',
},
]);
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
visibility: MessageChannelVisibility.SUBJECT,
},
]);
mockConnectedAccountRepository.find.mockResolvedValue([]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
const result = await service.applyMessagesVisibilityRestrictions(
@@ -153,17 +186,27 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
messageChannelId: 'messageChannelId',
},
]);
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
]);
mockConnectedAccountRepository.find.mockResolvedValue([]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
const result = await service.applyMessagesVisibilityRestrictions(
@@ -189,18 +232,28 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
messageChannelId: 'messageChannelId',
},
]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-account-owner-id',
userId: 'user-id',
});
mockConnectedAccountDataAccessService.find.mockResolvedValue([{ id: '1' }]);
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
const result = await service.applyMessagesVisibilityRestrictions(
messages,
@@ -225,17 +278,27 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
},
messageChannelId: 'messageChannelId',
},
]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-not-account-owner-id',
userId: 'user-id',
});
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyMessagesVisibilityRestrictions(
messages,
@@ -254,34 +317,28 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: '1',
messageChannel: {
id: '1',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
},
{
messageId: '2',
messageChannel: {
id: '2',
visibility: MessageChannelVisibility.SUBJECT,
},
},
{
messageId: '3',
messageChannel: {
id: '3',
visibility: MessageChannelVisibility.METADATA,
},
},
{ messageId: '1', messageChannelId: '1' },
{ messageId: '2', messageChannelId: '2' },
{ messageId: '3', messageChannelId: '3' },
]);
mockMessageChannelRepository.find.mockResolvedValue([
{ id: '1', visibility: MessageChannelVisibility.SHARE_EVERYTHING },
{ id: '2', visibility: MessageChannelVisibility.SUBJECT },
{ id: '3', visibility: MessageChannelVisibility.METADATA },
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockConnectedAccountDataAccessService.find
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for message 3
.mockResolvedValueOnce([]); // request for message 2
@@ -313,34 +370,22 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: '1',
messageChannel: {
id: '1',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
},
{
messageId: '2',
messageChannel: {
id: '2',
visibility: MessageChannelVisibility.SUBJECT,
},
},
{
messageId: '3',
messageChannel: {
id: '3',
visibility: MessageChannelVisibility.METADATA,
},
},
{ messageId: '1', messageChannelId: '1' },
{ messageId: '2', messageChannelId: '2' },
{ messageId: '3', messageChannelId: '3' },
]);
mockMessageChannelRepository.find.mockResolvedValue([
{ id: '1', visibility: MessageChannelVisibility.SHARE_EVERYTHING },
{ id: '2', visibility: MessageChannelVisibility.SUBJECT },
{ id: '3', visibility: MessageChannelVisibility.METADATA },
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockConnectedAccountDataAccessService.find
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for message 3
.mockResolvedValueOnce([]); // request for message 2
@@ -1,16 +1,19 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import groupBy from 'lodash.groupby';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { MessageChannelVisibility } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { In, Repository } from 'typeorm';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -18,7 +21,12 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
export class ApplyMessagesVisibilityRestrictionsService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
) {}
public async applyMessagesVisibilityRestrictions(
@@ -41,9 +49,28 @@ export class ApplyMessagesVisibilityRestrictionsService {
where: {
messageId: In(messages.map((message) => message.id)),
},
relations: ['messageChannel'],
});
const messageChannelIds = [
...new Set(
messageChannelMessagesAssociations.map((a) => a.messageChannelId),
),
];
const messageChannelsFromCore =
messageChannelIds.length > 0
? await this.messageChannelRepository.find({
where: {
id: In(messageChannelIds),
workspaceId,
},
})
: [];
const messageChannelMap = new Map(
messageChannelsFromCore.map((ch) => [ch.id, ch]),
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
@@ -51,17 +78,15 @@ export class ApplyMessagesVisibilityRestrictionsService {
);
for (let i = messages.length - 1; i >= 0; i--) {
const messageChannelMessageAssociations =
messageChannelMessagesAssociations.filter(
(association) => association.messageId === messages[i].id,
);
const associations = messageChannelMessagesAssociations.filter(
(association) => association.messageId === messages[i].id,
);
const messageChannels = messageChannelMessageAssociations
.map((association) => association.messageChannel)
.filter(
(channel): channel is NonNullable<typeof channel> =>
channel !== null,
);
const messageChannels = associations
.map((association) =>
messageChannelMap.get(association.messageChannelId),
)
.filter(isDefined);
if (messageChannels.length === 0) {
throw new NotFoundError('Associated message channels not found');
@@ -86,16 +111,26 @@ export class ApplyMessagesVisibilityRestrictionsService {
userId,
});
const connectedAccounts =
await this.connectedAccountDataAccessService.find(workspaceId, {
messageChannels: {
id: In(messageChannels.map((channel) => channel.id)),
},
accountOwnerId: workspaceMember.id,
});
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: workspaceMember.userId, workspaceId },
});
if (connectedAccounts.length > 0) {
continue;
if (userWorkspace) {
const connectedAccounts =
await this.connectedAccountRepository.find({
where: {
userWorkspaceId: userWorkspace.id,
workspaceId,
messageChannels: {
id: In(messageChannels.map((channel) => channel.id)),
},
},
relations: { messageChannels: true },
});
if (connectedAccounts.length > 0) {
continue;
}
}
}
@@ -1,144 +0,0 @@
import { Logger } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { Not } from 'typeorm';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import {
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
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 {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
const ONGOING_SYNC_STAGES = [
MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
];
@WorkspaceQueryHook(`messageChannel.updateOne`)
export class MessageChannelUpdateOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
private readonly logger = new Logger(
MessageChannelUpdateOnePreQueryHook.name,
);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
) {}
async execute(
authContext: WorkspaceAuthContext,
_objectName: string,
payload: UpdateOneResolverArgs<MessageChannelWorkspaceEntity>,
): Promise<UpdateOneResolverArgs<MessageChannelWorkspaceEntity>> {
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
const systemAuthContext = buildSystemAuthContext(workspace.id);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const messageChannel =
await this.messageChannelDataAccessService.findOne(workspace.id, {
where: { id: payload.id },
});
if (!isDefined(messageChannel)) {
throw new WorkspaceQueryRunnerException(
'Message channel not found',
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
{
userFriendlyMessage: msg`Message channel not found`,
},
);
}
const messageChannelWorkspace =
messageChannel as unknown as MessageChannelWorkspaceEntity;
const isSyncOngoing = ONGOING_SYNC_STAGES.includes(
messageChannelWorkspace.syncStage,
);
const messageFoldersWithPendingAction =
await this.messageFolderDataAccessService.find(workspace.id, {
messageChannelId: messageChannel.id,
pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE),
});
const messageFoldersWithPendingActionCount =
messageFoldersWithPendingAction.length;
const hasPendingFolderActions =
messageFoldersWithPendingActionCount > 0;
const hasPendingGroupEmailsAction =
messageChannelWorkspace.pendingGroupEmailsAction !==
MessageChannelPendingGroupEmailsAction.NONE;
if (
isSyncOngoing &&
(hasPendingFolderActions || hasPendingGroupEmailsAction)
) {
throw new WorkspaceQueryRunnerException(
'Cannot update message channel while sync is ongoing with pending actions',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{
userFriendlyMessage: msg`Cannot update message channel while sync is ongoing. Please wait for the sync to complete.`,
},
);
}
const hasCompletedConfiguration =
messageChannelWorkspace.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION;
if (!hasCompletedConfiguration) {
this.logger.log(
`MessageChannelId: ${messageChannelWorkspace.id} - Skipping pending action for message channel in PENDING_CONFIGURATION state`,
);
return payload;
}
const excludeGroupEmailsChanged =
isDefined(payload.data.excludeGroupEmails) &&
payload.data.excludeGroupEmails !==
messageChannelWorkspace.excludeGroupEmails;
if (excludeGroupEmailsChanged) {
await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction(
messageChannelWorkspace,
workspace.id,
payload.data.excludeGroupEmails
? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION
: MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT,
);
}
return payload;
},
systemAuthContext,
);
}
}
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
import { MessageChannelUpdateOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook';
import { MessageFindManyPostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.post-query.hook';
import { MessageFindOnePostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.post-query.hook';
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
@@ -12,15 +13,17 @@ import { MessagingImportManagerModule } from 'src/modules/messaging/message-impo
@Module({
imports: [
MessagingImportManagerModule,
ConnectedAccountDataAccessModule,
MessageChannelDataAccessModule,
MessageFolderDataAccessModule,
TypeOrmModule.forFeature([
ConnectedAccountEntity,
MessageChannelEntity,
MessageFolderEntity,
UserWorkspaceEntity,
]),
],
providers: [
ApplyMessagesVisibilityRestrictionsService,
MessageFindOnePostQueryHook,
MessageFindManyPostQueryHook,
MessageChannelUpdateOnePreQueryHook,
],
})
export class MessagingQueryHookModule {}
@@ -1,25 +1,27 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Any, In } from 'typeorm';
import { Any, In, Repository } from 'typeorm';
import {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
MessageChannelSyncStatus,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
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 { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
import {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
MessageChannelSyncStatus,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
@@ -28,9 +30,14 @@ export class MessageChannelSyncStatusService {
@InjectCacheStorage(CacheStorageNamespace.ModuleMessaging)
private readonly cacheStorage: CacheStorageService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly accountsToReconnectService: AccountsToReconnectService,
private readonly metricsService: MetricsService,
) {}
@@ -47,9 +54,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
@@ -70,9 +76,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING,
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
@@ -98,9 +103,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncCursor: '',
syncStageStartedAt: null,
@@ -110,9 +114,8 @@ export class MessageChannelSyncStatusService {
},
);
await this.messageFolderDataAccessService.update(
workspaceId,
{ messageChannelId: In(messageChannelIds) },
await this.messageFolderRepository.update(
{ messageChannelId: In(messageChannelIds), workspaceId },
{
syncCursor: '',
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
@@ -134,9 +137,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{ syncStageStartedAt: null },
);
}, authContext);
@@ -153,9 +155,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
syncStatus: MessageChannelSyncStatus.ONGOING,
@@ -176,9 +177,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
syncStatus: MessageChannelSyncStatus.ONGOING,
@@ -199,9 +199,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStatus: MessageChannelSyncStatus.ACTIVE,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
@@ -230,9 +229,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED,
},
@@ -251,9 +249,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING,
syncStatus: MessageChannelSyncStatus.ONGOING,
@@ -277,9 +274,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.FAILED,
syncStatus: syncStatus,
@@ -300,18 +296,16 @@ export class MessageChannelSyncStatusService {
if (
syncStatus === MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS
) {
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
{ id: In(messageChannelIds) },
);
const messageChannels = await this.messageChannelRepository.find({
where: { id: In(messageChannelIds), workspaceId },
});
const connectedAccountIds = messageChannels.map(
(messageChannel) => messageChannel.connectedAccountId,
);
await this.connectedAccountDataAccessService.update(
workspaceId,
{ id: Any(connectedAccountIds) },
await this.connectedAccountRepository.update(
{ id: Any(connectedAccountIds), workspaceId },
{
authFailedAt: new Date(),
},
@@ -333,12 +327,9 @@ export class MessageChannelSyncStatusService {
return;
}
const messageChannels = await this.messageChannelDataAccessService.findMany(
workspaceId,
{
where: { id: In(messageChannelIds) },
},
);
const messageChannels = await this.messageChannelRepository.find({
where: { id: In(messageChannelIds), workspaceId },
});
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
@@ -348,17 +339,24 @@ export class MessageChannelSyncStatusService {
);
for (const messageChannel of messageChannels) {
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { id: messageChannel.connectedAccountId },
});
const connectedAccount = await this.connectedAccountRepository.findOne({
where: { id: messageChannel.connectedAccountId, workspaceId },
});
if (!connectedAccount) {
continue;
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { id: connectedAccount.userWorkspaceId },
});
if (!userWorkspace) {
continue;
}
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: connectedAccount.accountOwnerId },
where: { userId: userWorkspace.userId },
});
if (!workspaceMember) {
@@ -5,7 +5,6 @@ import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/util
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';
const MESSAGE_EXTERNAL_ID_FIELD_NAME = 'messageExternalId';
@@ -17,7 +16,6 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
messageExternalId: string | null;
messageThreadExternalId: string | null;
direction: MessageDirection;
messageChannel: EntityRelation<MessageChannelWorkspaceEntity> | null;
messageChannelId: string;
message: EntityRelation<MessageWorkspaceEntity> | null;
messageId: string;
@@ -9,7 +9,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
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 { MessageFolderPendingSyncAction };
@@ -25,7 +24,6 @@ export const SEARCH_FIELDS_FOR_MESSAGE_FOLDER: FieldTypeAndNameMetadata[] = [
export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
name: string | null;
messageChannel: EntityRelation<MessageChannelWorkspaceEntity>;
syncCursor: string | null;
isSentFolder: boolean;
isSynced: boolean;
@@ -1,9 +1,11 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command, CommandRunner, Option } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
@@ -24,7 +26,8 @@ export class MessagingResetChannelCommand extends CommandRunner {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly messagingChannelSyncStatusService: MessageChannelSyncStatusService,
private readonly messagingMessageCleanerService: MessagingMessageCleanerService,
) {
@@ -44,10 +47,12 @@ export class MessagingResetChannelCommand extends CommandRunner {
`No message channel ID provided, resetting all message channels in workspace ${workspaceId}`,
);
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
isDefined(messageChannelId) ? { id: messageChannelId } : {},
);
const messageChannels = await this.messageChannelRepository.find({
where: {
...(isDefined(messageChannelId) ? { id: messageChannelId } : {}),
workspaceId,
},
});
if (messageChannels.length === 0) {
this.logger.log(
@@ -1,8 +1,5 @@
import { Logger, Scope } from '@nestjs/common';
import { FeatureFlagKey } from 'twenty-shared/types';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
@@ -24,22 +21,12 @@ export class MessagingMessageChannelDeletionCleanupJob {
constructor(
private readonly messageCleanerService: MessagingMessageCleanerService,
private readonly featureFlagService: FeatureFlagService,
) {}
@Process(MessagingMessageChannelDeletionCleanupJob.name)
async handle(
data: MessagingMessageChannelDeletionCleanupJobData,
): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
data.workspaceId,
);
if (!isMigrated) {
return;
}
this.logger.debug(
`WorkspaceId: ${data.workspaceId} Cleaning up message channel message associations for channel ${data.messageChannelId}`,
);
@@ -6,7 +6,7 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import {
MessagingConnectedAccountDeletionCleanupJob,
type MessagingConnectedAccountDeletionCleanupJobData,
@@ -24,7 +24,7 @@ export class MessagingMessageCleanerConnectedAccountListener {
@OnDatabaseBatchEvent('connectedAccount', DatabaseEventAction.DESTROYED)
async handleDestroyedEvent(
payload: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity>
ObjectRecordDeleteEvent<ConnectedAccountEntity>
>,
) {
await Promise.all(
@@ -6,7 +6,7 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import {
MessagingMessageChannelDeletionCleanupJob,
type MessagingMessageChannelDeletionCleanupJobData,
@@ -23,9 +23,7 @@ export class MessagingMessageCleanerMessageChannelListener {
@OnDatabaseBatchEvent('messageChannel', DatabaseEventAction.DESTROYED)
async handleDestroyedEvent(
payload: WorkspaceEventBatch<
ObjectRecordDeleteEvent<MessageChannelWorkspaceEntity>
>,
payload: WorkspaceEventBatch<ObjectRecordDeleteEvent<MessageChannelEntity>>,
) {
await Promise.all(
payload.events.map((eventPayload) =>
@@ -5,7 +5,7 @@ import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/w
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MessagingMessageCleanerRemoveOrphansCommand } from 'src/modules/messaging/message-cleaner/commands/messaging-message-clearner-remove-orphans.command';
import { MessagingResetChannelCommand } from 'src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command';
@@ -13,6 +13,7 @@ import { MessagingConnectedAccountDeletionCleanupJob } from 'src/modules/messagi
import { MessagingMessageChannelDeletionCleanupJob } from 'src/modules/messaging/message-cleaner/jobs/messaging-message-channel-deletion-cleanup.job';
import { MessagingMessageCleanerConnectedAccountListener } from 'src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener';
import { MessagingMessageCleanerMessageChannelListener } from 'src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-message-channel.listener';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
@Module({
@@ -21,7 +22,7 @@ import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cl
DataSourceModule,
FeatureFlagModule,
MessagingCommonModule,
MessageChannelDataAccessModule,
TypeOrmModule.forFeature([MessageChannelEntity]),
WorkspaceIteratorModule,
],
providers: [
@@ -7,9 +7,9 @@ import {
MessageFolderDriver,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailFoldersErrorHandlerService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-folders-error-handler.service';
import { extractGmailFolderName } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/extract-gmail-folder-name.util';
import { getGmailFolderParentId } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/get-gmail-folder-parent-id.util';
@@ -27,13 +27,10 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
async getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'provider' | 'refreshToken' | 'accessToken' | 'id' | 'handle'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
try {
const oAuth2Client =
@@ -8,8 +8,8 @@ import {
MessageFolderDriver,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { shouldCreateFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-create-folder-by-default.util';
import { shouldSyncFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-sync-folder-by-default.util';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
@@ -27,13 +27,10 @@ export class ImapGetAllFoldersService implements MessageFolderDriver {
public async getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'id' | 'provider' | 'connectionParameters' | 'handle'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
try {
const client = await this.imapClientProvider.getClient(connectedAccount);
@@ -62,10 +59,7 @@ export class ImapGetAllFoldersService implements MessageFolderDriver {
private async filterAndMapFolders(
client: ImapFlow,
mailboxList: ListResponse[],
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
const folders: DiscoveredMessageFolder[] = [];
const pathToExternalIdMap = new Map<string, string>();
@@ -7,9 +7,9 @@ import {
MessageFolderDriver,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { shouldCreateFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-create-folder-by-default.util';
import { shouldSyncFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-sync-folder-by-default.util';
import { MicrosoftMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-message-list-fetch-error-handler.service';
@@ -37,13 +37,10 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
async getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'accessToken' | 'refreshToken' | 'id' | 'handle' | 'provider'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
try {
const microsoftClient =
@@ -1,14 +1,14 @@
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.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 { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
export type DiscoveredMessageFolder = Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
'name' | 'isSynced' | 'isSentFolder' | 'externalId' | 'parentFolderId'
>;
export type MessageFolder = Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
| 'name'
| 'isSynced'
| 'isSentFolder'
@@ -22,7 +22,7 @@ export type MessageFolder = Pick<
export type MessageFolderDriver = {
getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -30,9 +30,6 @@ export type MessageFolderDriver = {
| 'handle'
| 'connectionParameters'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]>;
};
@@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
import { GmailFoldersErrorHandlerService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-folders-error-handler.service';
@@ -21,8 +21,7 @@ import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-im
FeatureFlagModule,
WorkspaceDataSourceModule,
DataSourceModule,
MessageFolderDataAccessModule,
TypeOrmModule.forFeature([WorkspaceEntity]),
TypeOrmModule.forFeature([WorkspaceEntity, MessageFolderEntity]),
OAuth2ClientManagerModule,
MessagingGmailDriverModule,
MessagingMicrosoftDriverModule,
@@ -1,29 +1,27 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { In } from 'typeorm';
import { type DiscoveredMessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import {
ConnectedAccountProvider,
MessageChannelContactAutoCreationPolicy,
MessageChannelType,
MessageChannelVisibility,
MessageFolderImportPolicy,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
} from 'twenty-shared/types';
import { In } from 'typeorm';
import { type DiscoveredMessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
type SyncedMessageFolder = Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
| 'id'
| 'name'
| 'isSynced'
@@ -111,7 +109,7 @@ describe('SyncMessageFoldersService', () => {
let service: SyncMessageFoldersService;
let gmailGetAllFoldersService: jest.Mocked<GmailGetAllFoldersService>;
let mockMessageFolderDataAccessService: {
let mockMessageFolderRepository: {
delete: jest.Mock;
update: jest.Mock;
save: jest.Mock;
@@ -119,7 +117,7 @@ describe('SyncMessageFoldersService', () => {
};
let createdFolderRecords: Array<
Partial<MessageFolderWorkspaceEntity> & {
Partial<MessageFolderEntity> & {
id: string;
externalId: string;
}
@@ -128,10 +126,10 @@ describe('SyncMessageFoldersService', () => {
beforeEach(async () => {
createdFolderRecords = [];
mockMessageFolderDataAccessService = {
mockMessageFolderRepository = {
delete: jest.fn(),
update: jest.fn().mockResolvedValue(undefined),
save: jest.fn().mockImplementation(async (_workspaceId, folder) => {
save: jest.fn().mockImplementation(async (folder) => {
createdFolderRecords.push({
...folder,
id: `new-folder-${createdFolderRecords.length}-${Math.random().toString(36).substring(7)}`,
@@ -141,7 +139,7 @@ describe('SyncMessageFoldersService', () => {
externalId: folder.externalId as string,
});
}),
find: jest.fn().mockImplementation(async (_workspaceId, where) => {
find: jest.fn().mockImplementation(async ({ where }) => {
if (!where?.externalId) {
return [];
}
@@ -171,8 +169,8 @@ describe('SyncMessageFoldersService', () => {
},
},
{
provide: MessageFolderDataAccessService,
useValue: mockMessageFolderDataAccessService,
provide: getRepositoryToken(MessageFolderEntity),
useValue: mockMessageFolderRepository,
},
{
provide: GmailGetAllFoldersService,
@@ -228,18 +226,18 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith(
workspaceId,
expect(mockMessageFolderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId,
name: 'INBOX',
externalId: 'inbox-ext',
messageChannelId: 'channel-123',
isSentFolder: false,
}),
);
expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith(
workspaceId,
expect(mockMessageFolderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId,
name: 'Sent',
externalId: 'sent-ext',
messageChannelId: 'channel-123',
@@ -275,9 +273,9 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith(
workspaceId,
expect(mockMessageFolderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId,
name: 'Projects',
parentFolderId: 'parent-folder-id',
}),
@@ -311,9 +309,8 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: 'folder-1', messageChannelId: 'channel-123' },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{ id: 'folder-1', messageChannelId: 'channel-123', workspaceId },
expect.objectContaining({ name: 'Primary Inbox' }),
);
expect(result).toContainEqual(
@@ -325,11 +322,16 @@ describe('SyncMessageFoldersService', () => {
});
it('should update folder when parent folder changes', async () => {
const newParentFolder = createMockExistingFolder({
id: 'new-parent-uuid',
externalId: 'new-parent-id',
name: 'New Parent',
});
const existingFolder = createMockExistingFolder({
id: 'folder-1',
externalId: 'child-ext',
name: 'Projects',
parentFolderId: 'old-parent-id',
parentFolderId: 'old-parent-uuid',
});
const discoveredFolders = [
createMockDiscoveredFolder({
@@ -337,9 +339,13 @@ describe('SyncMessageFoldersService', () => {
name: 'Projects',
parentFolderId: 'new-parent-id',
}),
createMockDiscoveredFolder({
externalId: 'new-parent-id',
name: 'New Parent',
}),
];
const messageChannel = createMockMessageChannel({
messageFolders: [existingFolder],
messageFolders: [existingFolder, newParentFolder],
});
gmailGetAllFoldersService.getAllMessageFolders.mockResolvedValue(
@@ -351,11 +357,10 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: 'folder-1', messageChannelId: 'channel-123' },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{ id: 'folder-1', messageChannelId: 'channel-123', workspaceId },
expect.objectContaining({
parentFolderId: 'new-parent-id',
parentFolderId: 'new-parent-uuid',
}),
);
});
@@ -389,9 +394,7 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(
mockMessageFolderDataAccessService.update,
).not.toHaveBeenCalled();
expect(mockMessageFolderRepository.update).not.toHaveBeenCalled();
});
});
@@ -428,9 +431,8 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: In(['folder-2']) },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{ id: In(['folder-2']), workspaceId },
expect.objectContaining({
pendingSyncAction: 'FOLDER_DELETION',
}),
@@ -491,21 +493,25 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: In(['folder-to-delete']) },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{ id: In(['folder-to-delete']), workspaceId },
expect.objectContaining({
pendingSyncAction: 'FOLDER_DELETION',
}),
);
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: 'folder-to-update', messageChannelId: 'channel-123' },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{
id: 'folder-to-update',
messageChannelId: 'channel-123',
workspaceId,
},
expect.objectContaining({ name: 'New Name' }),
);
expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith(
workspaceId,
expect.objectContaining({ externalId: 'new-ext' }),
expect(mockMessageFolderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId,
externalId: 'new-ext',
}),
);
expect(result).toHaveLength(4);
expect(result).toContainEqual(
@@ -1,20 +1,23 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import {
ConnectedAccountProvider,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { In, Repository } from 'typeorm';
import {
DiscoveredMessageFolder,
MessageFolder,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
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 { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service';
@@ -27,7 +30,8 @@ import { computeUpdatedFolders } from 'src/modules/messaging/message-folder-mana
export class SyncMessageFoldersService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
private readonly gmailGetAllFoldersService: GmailGetAllFoldersService,
private readonly microsoftGetAllFoldersService: MicrosoftGetAllFoldersService,
private readonly imapGetAllFoldersService: ImapGetAllFoldersService,
@@ -38,11 +42,11 @@ export class SyncMessageFoldersService {
workspaceId,
}: {
messageChannel: Pick<
MessageChannelWorkspaceEntity,
MessageChannelEntity,
'id' | 'messageFolderImportPolicy'
> & {
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -72,7 +76,7 @@ export class SyncMessageFoldersService {
async discoverAllFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'accessToken'
| 'refreshToken'
| 'id'
@@ -80,10 +84,7 @@ export class SyncMessageFoldersService {
| 'provider'
| 'connectionParameters'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
switch (connectedAccount.provider) {
case ConnectedAccountProvider.GOOGLE:
@@ -114,6 +115,12 @@ export class SyncMessageFoldersService {
messageChannelId: string,
workspaceId: string,
): Promise<MessageFolder[]> {
const externalIdToUuidMap = new Map(
existingFolders
.filter((folder) => isDefined(folder.externalId))
.map((folder) => [folder.externalId as string, folder.id]),
);
const foldersToCreate = computeFoldersToCreate({
discoveredFolders,
existingFolders,
@@ -123,6 +130,7 @@ export class SyncMessageFoldersService {
const foldersToUpdate = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap,
});
const folderIdsToDelete = computeFolderIdsToDelete({
@@ -132,16 +140,11 @@ export class SyncMessageFoldersService {
const authContext = buildSystemAuthContext(workspaceId);
// TODO: Restore transaction wrapper once migration is complete — folder
// sync operations (create/update/delete) are no longer atomic since
// the data access layer routes writes across workspace and core schemas.
// Acceptable during transition as sync is idempotent and self-corrects.
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
if (folderIdsToDelete.length > 0) {
await this.messageFolderDataAccessService.update(
workspaceId,
{ id: In(folderIdsToDelete) },
await this.messageFolderRepository.update(
{ id: In(folderIdsToDelete), workspaceId },
{
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
},
@@ -150,32 +153,34 @@ export class SyncMessageFoldersService {
if (foldersToUpdate.size > 0) {
for (const [id, data] of foldersToUpdate.entries()) {
await this.messageFolderDataAccessService.update(
workspaceId,
{ id, messageChannelId },
data,
await this.messageFolderRepository.update(
{ id, messageChannelId, workspaceId },
data as Record<string, unknown>,
);
}
}
if (foldersToCreate.length > 0) {
for (const folderToCreate of foldersToCreate) {
await this.messageFolderDataAccessService.save(
await this.messageFolderRepository.save({
...folderToCreate,
workspaceId,
folderToCreate,
);
});
}
}
const createdFolders =
foldersToCreate.length > 0
? await this.messageFolderDataAccessService.find(workspaceId, {
messageChannelId,
externalId: In(
foldersToCreate
.map((folder) => folder.externalId)
.filter(isDefined),
),
? await this.messageFolderRepository.find({
where: {
messageChannelId,
externalId: In(
foldersToCreate
.map((folder) => folder.externalId)
.filter(isDefined),
),
workspaceId,
},
})
: [];
@@ -185,10 +190,7 @@ export class SyncMessageFoldersService {
folderIdsToDelete,
});
return [
...updatedExistingFolders,
...(createdFolders as MessageFolder[]),
];
return [...updatedExistingFolders, ...createdFolders];
},
authContext,
);
@@ -1,4 +1,4 @@
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { computeFolderIdsToDelete } from 'src/modules/messaging/message-folder-manager/utils/compute-folder-ids-to-delete.util';
describe('computeFolderIdsToDelete', () => {
@@ -1,4 +1,4 @@
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { computeFoldersToCreate } from 'src/modules/messaging/message-folder-manager/utils/compute-folders-to-create.util';
describe('computeFoldersToCreate', () => {
@@ -1,7 +1,9 @@
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { computeFoldersToUpdate } from 'src/modules/messaging/message-folder-manager/utils/compute-folders-to-update.util';
describe('computeFoldersToUpdate', () => {
const emptyMap = new Map<string, string>();
it('should detect folder rename from provider', () => {
const discoveredFolders = [
{
@@ -29,19 +31,22 @@ describe('computeFoldersToUpdate', () => {
const result = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap: emptyMap,
});
expect(result.get('folder-id')?.name).toBe('Work Emails');
});
it('should detect folder moved to different parent', () => {
const existingParentUuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
const discoveredFolders = [
{
name: 'Subfolder',
externalId: 'sub-1',
isSynced: true,
isSentFolder: false,
parentFolderId: 'new-parent-id',
parentFolderId: 'new-parent-ext',
},
];
@@ -52,18 +57,23 @@ describe('computeFoldersToUpdate', () => {
externalId: 'sub-1',
isSynced: true,
isSentFolder: false,
parentFolderId: 'old-parent-id',
parentFolderId: 'old-parent-uuid',
syncCursor: 'cursor',
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
},
];
const externalIdToUuidMap = new Map([
['new-parent-ext', existingParentUuid],
]);
const result = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap,
});
expect(result.get('folder-id')?.parentFolderId).toBe('new-parent-id');
expect(result.get('folder-id')?.parentFolderId).toBe(existingParentUuid);
});
it('should not flag unchanged folders for update', () => {
@@ -88,6 +98,7 @@ describe('computeFoldersToUpdate', () => {
const result = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap: emptyMap,
});
expect(result.size).toBe(0);
@@ -120,6 +131,7 @@ describe('computeFoldersToUpdate', () => {
const result = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap: emptyMap,
});
expect(result.size).toBe(0);
@@ -1,4 +1,4 @@
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { computeUpdatedFolders } from 'src/modules/messaging/message-folder-manager/utils/compute-updated-folders.util';
describe('computeUpdatedFolders', () => {
@@ -1,4 +1,4 @@
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { shouldSyncFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-sync-folder-by-default.util';
describe('shouldSyncFolderByDefault', () => {
@@ -5,7 +5,7 @@ import {
type MessageFolder,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
export const computeFoldersToCreate = ({
discoveredFolders,
@@ -15,7 +15,7 @@ export const computeFoldersToCreate = ({
discoveredFolders: DiscoveredMessageFolder[];
existingFolders: MessageFolder[];
messageChannelId: string;
}): Partial<MessageFolderWorkspaceEntity>[] => {
}): Partial<MessageFolderEntity>[] => {
const existingFoldersByExternalId = new Map(
existingFolders.map((folder) => [folder.externalId, folder]),
);
@@ -6,23 +6,22 @@ import {
type MessageFolder,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
export const computeFoldersToUpdate = ({
discoveredFolders,
existingFolders,
externalIdToUuidMap,
}: {
discoveredFolders: DiscoveredMessageFolder[];
existingFolders: MessageFolder[];
}): Map<string, Partial<MessageFolderWorkspaceEntity>> => {
externalIdToUuidMap: Map<string, string>;
}): Map<string, Partial<MessageFolderEntity>> => {
const existingFoldersByExternalId = new Map(
existingFolders.map((folder) => [folder.externalId, folder]),
);
const foldersToUpdate = new Map<
string,
Partial<MessageFolderWorkspaceEntity>
>();
const foldersToUpdate = new Map<string, Partial<MessageFolderEntity>>();
for (const discoveredFolder of discoveredFolders) {
const existingFolder = existingFoldersByExternalId.get(
@@ -33,12 +32,16 @@ export const computeFoldersToUpdate = ({
continue;
}
const resolvedParentFolderId = isNonEmptyString(
discoveredFolder.parentFolderId,
)
? (externalIdToUuidMap.get(discoveredFolder.parentFolderId) ?? null)
: null;
const discoveredFolderData = {
name: discoveredFolder.name,
isSentFolder: discoveredFolder.isSentFolder,
parentFolderId: isNonEmptyString(discoveredFolder.parentFolderId)
? discoveredFolder.parentFolderId
: null,
parentFolderId: resolvedParentFolderId,
};
const existingFolderData = {
@@ -1,9 +1,8 @@
import { type MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
export const computeUpdatedFolders = ({
existingFolders,
@@ -11,7 +10,7 @@ export const computeUpdatedFolders = ({
folderIdsToDelete,
}: {
existingFolders: MessageFolder[];
foldersToUpdate: Map<string, Partial<MessageFolderWorkspaceEntity>>;
foldersToUpdate: Map<string, Partial<MessageFolderEntity>>;
folderIdsToDelete: string[];
}): MessageFolder[] => {
return existingFolders.map((existingFolder) => {
@@ -1,4 +1,4 @@
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
export const shouldSyncFolderByDefault = (
messageFolderImportPolicy: MessageFolderImportPolicy,
@@ -1,18 +1,20 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command, CommandRunner, Option } from 'nest-commander';
import { Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
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';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 {
MessagingMessageListFetchJob,
type MessagingMessageListFetchJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
type MessagingTriggerMessageListFetchCommandOptions = {
workspaceId: string;
@@ -31,7 +33,8 @@ export class MessagingTriggerMessageListFetchCommand extends CommandRunner {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
) {
@@ -51,14 +54,14 @@ export class MessagingTriggerMessageListFetchCommand extends CommandRunner {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
{
const messageChannels = await this.messageChannelRepository.find({
where: {
isSyncEnabled: true,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
...(messageChannelId ? { id: messageChannelId } : {}),
workspaceId,
},
);
});
if (messageChannels.length === 0) {
this.logger.warn(
@@ -73,9 +76,8 @@ export class MessagingTriggerMessageListFetchCommand extends CommandRunner {
);
for (const messageChannel of messageChannels) {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: messageChannel.id },
await this.messageChannelRepository.update(
{ id: messageChannel.id, workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
syncStageStartedAt: new Date().toISOString(),
@@ -1,20 +1,17 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessageListFetchJob,
type MessagingMessageListFetchJobData,
@@ -32,7 +29,6 @@ export class MessagingMessageListFetchCronJob {
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly featureFlagService: FeatureFlagService,
) {}
@Process(MessagingMessageListFetchCronJob.name)
@@ -51,22 +47,11 @@ export class MessagingMessageListFetchCronJob {
try {
const now = new Date().toISOString();
// TODO: remove workspace schema branch once IS_CONNECTED_ACCOUNT_MIGRATED feature flag is removed
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
activeWorkspace.id,
const [messageChannels] = await this.coreDataSource.query(
`UPDATE core."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING}' RETURNING *`,
);
const [messageChannels] = isMigrated
? await this.coreDataSource.query(
`UPDATE core."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING}' RETURNING *`,
)
: await this.coreDataSource.query(
`UPDATE ${getWorkspaceSchemaName(activeWorkspace.id)}."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING}' RETURNING *`,
);
for (const messageChannel of messageChannels) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
@@ -1,13 +1,12 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
@@ -18,8 +17,6 @@ import {
DataSourceException,
DataSourceExceptionCode,
} from 'src/engine/metadata-modules/data-source/data-source.exception';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessagesImportJob,
type MessagingMessagesImportJobData,
@@ -37,7 +34,6 @@ export class MessagingMessagesImportCronJob {
private readonly exceptionHandlerService: ExceptionHandlerService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly featureFlagService: FeatureFlagService,
) {}
@Process(MessagingMessagesImportCronJob.name)
@@ -56,22 +52,11 @@ export class MessagingMessagesImportCronJob {
try {
const now = new Date().toISOString();
// TODO: remove workspace schema branch once IS_CONNECTED_ACCOUNT_MIGRATED feature flag is removed
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
activeWorkspace.id,
const [messageChannels] = await this.coreDataSource.query(
`UPDATE core."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_PENDING}' RETURNING *`,
);
const [messageChannels] = isMigrated
? await this.coreDataSource.query(
`UPDATE core."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_PENDING}' RETURNING *`,
)
: await this.coreDataSource.query(
`UPDATE ${getWorkspaceSchemaName(activeWorkspace.id)}."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_PENDING}' RETURNING *`,
);
for (const messageChannel of messageChannels) {
await this.messageQueueService.add<MessagingMessagesImportJobData>(
MessagingMessagesImportJob.name,
@@ -82,9 +67,6 @@ export class MessagingMessagesImportCronJob {
);
}
} catch (error) {
// We had issues with the workspace schema not being found, due
// to users deleting their workspaces in the middle of the cron job
// We only throw an error when the workspace is found & schema not found
if (
error.code === '42P01' &&
error.message.includes('messageChannel" does not exist')
@@ -3,6 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
} from 'twenty-shared/types';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
@@ -12,10 +16,6 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingRelaunchFailedMessageChannelJob,
type MessagingRelaunchFailedMessageChannelJobData,
@@ -1,14 +1,16 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { google } from 'googleapis';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import {
ConnectedAccountProvider,
MessageFolderImportPolicy,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
import { type MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailGetHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service';
import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service';
import { GmailMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-message-list-fetch-error-handler.service';
@@ -31,7 +33,7 @@ describe('GmailGetMessageListService', () => {
let oAuth2ClientManagerService: OAuth2ClientManagerService;
const mockConnectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -3,13 +3,11 @@ import { Injectable, Logger } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { google } from 'googleapis';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelWorkspaceEntity,
MessageFolderImportPolicy,
} 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 { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
@@ -32,17 +30,14 @@ export class GmailGetMessageListService {
private async getMessageListWithoutCursor(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'provider' | 'accessToken' | 'refreshToken' | 'id' | 'handle'
>,
messageFolders: Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
'name' | 'externalId' | 'isSynced' | 'parentFolderId'
>[],
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<GetMessageListsResponse> {
const oAuth2Client =
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
@@ -5,12 +5,10 @@ import { isNonEmptyString } from '@sniptt/guards';
import { type gmail_v1 as gmailV1, google } from 'googleapis';
import { isDefined } from 'twenty-shared/utils';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
type MessageChannelWorkspaceEntity,
MessageFolderImportPolicy,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailMessagesImportErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service';
import { filterGmailMessagesByFolderPolicy } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/filter-gmail-messages-by-folder-policy.util';
import { parseAndFormatGmailMessage } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-and-format-gmail-message.util';
@@ -28,7 +26,7 @@ export class GmailGetMessagesService {
async getMessages(
messageIds: string[],
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -37,7 +35,7 @@ export class GmailGetMessagesService {
| 'handleAliases'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
MessageChannelEntity,
'messageFolders' | 'messageFolderImportPolicy'
>,
): Promise<MessageWithParticipants[]> {
@@ -169,10 +167,7 @@ export class GmailGetMessagesService {
private async fetchMessages(
gmailClient: gmailV1.Gmail,
messageIds: string[],
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'handle' | 'handleAliases'
>,
connectedAccount: Pick<ConnectedAccountEntity, 'handle' | 'handleAliases'>,
): Promise<MessageWithParticipants[]> {
const results = await Promise.all(
messageIds.map((messageId) =>
@@ -1,4 +1,4 @@
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { computeGmailExcludeSearchFilter } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-gmail-exclude-search-filter.util';
describe('computeGmailExcludeSearchFilter', () => {

Some files were not shown because too many files have changed in this diff Show More