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:
+4
-2
@@ -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,
|
||||
|
||||
+44
-18
@@ -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];
|
||||
|
||||
+43
-16
@@ -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),
|
||||
|
||||
+1
@@ -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';
|
||||
|
||||
-13
@@ -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}`,
|
||||
);
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+8
-4
@@ -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: [
|
||||
|
||||
+15
-16
@@ -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,
|
||||
|
||||
+4
-19
@@ -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,
|
||||
|
||||
+4
-19
@@ -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,
|
||||
|
||||
+4
-4
@@ -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 * * * *';
|
||||
|
||||
+2
-2
@@ -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> {
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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[],
|
||||
|
||||
+21
-18
@@ -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);
|
||||
|
||||
+21
-18
@@ -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);
|
||||
|
||||
+17
-17
@@ -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,
|
||||
|
||||
+20
-17
@@ -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,
|
||||
|
||||
+9
-10
@@ -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' },
|
||||
);
|
||||
});
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
+14
-20
@@ -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) {
|
||||
|
||||
+47
-16
@@ -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(
|
||||
|
||||
+13
-16
@@ -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,
|
||||
},
|
||||
|
||||
+2
-2
@@ -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'
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
+4
-4
@@ -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],
|
||||
|
||||
+112
-69
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+61
-33
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -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,
|
||||
|
||||
+53
-71
@@ -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(
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+26
-24
@@ -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,
|
||||
|
||||
+2
-4
@@ -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,
|
||||
|
||||
+2
-4
@@ -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,
|
||||
|
||||
+6
-2
@@ -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,
|
||||
|
||||
+13
-12
@@ -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,
|
||||
},
|
||||
|
||||
+12
-9
@@ -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],
|
||||
|
||||
+36
-4
@@ -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);
|
||||
}
|
||||
|
||||
+15
-13
@@ -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;
|
||||
|
||||
|
||||
+3
-9
@@ -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(
|
||||
|
||||
-77
@@ -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;
|
||||
}
|
||||
}
|
||||
-18
@@ -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 {}
|
||||
+3
-2
@@ -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,
|
||||
],
|
||||
|
||||
+31
-35
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-9
@@ -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> {
|
||||
|
||||
+178
-95
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+97
-36
@@ -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,
|
||||
|
||||
+1
-5
@@ -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[]>;
|
||||
}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
],
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+12
-2
@@ -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);
|
||||
|
||||
+37
-27
@@ -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,
|
||||
},
|
||||
|
||||
+3
-3
@@ -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(),
|
||||
),
|
||||
];
|
||||
|
||||
+57
-30
@@ -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];
|
||||
|
||||
+50
-11
@@ -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),
|
||||
|
||||
+9
-2
@@ -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],
|
||||
|
||||
+118
-73
@@ -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
|
||||
|
||||
|
||||
+59
-24
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-144
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+11
-8
@@ -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 {}
|
||||
|
||||
+62
-64
@@ -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) {
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
+11
-6
@@ -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(
|
||||
|
||||
-13
@@ -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}`,
|
||||
);
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+2
-4
@@ -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) =>
|
||||
|
||||
+3
-2
@@ -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: [
|
||||
|
||||
+4
-7
@@ -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 =
|
||||
|
||||
+5
-11
@@ -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>();
|
||||
|
||||
+4
-7
@@ -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 =
|
||||
|
||||
+7
-10
@@ -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[]>;
|
||||
};
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
+55
-49
@@ -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(
|
||||
|
||||
+41
-39
@@ -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
-1
@@ -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
-1
@@ -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', () => {
|
||||
|
||||
+16
-4
@@ -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
-1
@@ -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
-1
@@ -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', () => {
|
||||
|
||||
+2
-2
@@ -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]),
|
||||
);
|
||||
|
||||
+12
-9
@@ -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 = {
|
||||
|
||||
+4
-5
@@ -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
-1
@@ -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,
|
||||
|
||||
+12
-10
@@ -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(),
|
||||
|
||||
+4
-19
@@ -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,
|
||||
|
||||
+4
-22
@@ -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')
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+7
-5
@@ -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'
|
||||
|
||||
+7
-12
@@ -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(
|
||||
|
||||
+6
-11
@@ -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
-1
@@ -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
Reference in New Issue
Block a user