Skip to content

Commit 0b7271a

Browse files
authored
refactor reconnect account logic (#15584)
1 parent 5c3eaf7 commit 0b7271a

20 files changed

+663
-312
lines changed

packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export const CONNECTED_ACCOUNT_STANDARD_FIELD_IDS = {
149149
accountOwner: '20202020-3517-4896-afac-b1d0aa362af6',
150150
lastSyncHistoryId: '20202020-115c-4a87-b50f-ac4367a971b9',
151151
authFailedAt: '20202020-d268-4c6b-baff-400d402b430a',
152+
lastCredentialsRefreshedAt: '20202020-aa5e-4e85-903b-fdf90a941941',
152153
messageChannels: '20202020-24f7-4362-8468-042204d1e445',
153154
calendarChannels: '20202020-af4a-47bb-99ec-51911c1d3977',
154155
handleAliases: '20202020-8a3d-46be-814f-6228af16c47b',

packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos
1010
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
1111
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
1212
import { CalendarEventCleanerModule } from 'src/modules/calendar/calendar-event-cleaner/calendar-event-cleaner.module';
13-
import { CalendarRelaunchFailedCalendarChannelsCommand } from 'src/modules/calendar/calendar-event-import-manager/commands/calendar-relaunch-failed-calendar-channels.command';
1413
import { CalendarEventListFetchCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-event-list-fetch.cron.command';
1514
import { CalendarEventsImportCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-import.cron.command';
1615
import { CalendarOngoingStaleCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-ongoing-stale.cron.command';
16+
import { CalendarRelaunchFailedCalendarChannelsCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-relaunch-failed-calendar-channels.cron.command';
1717
import { CalendarEventListFetchCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job';
1818
import { CalendarEventsImportCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job';
1919
import { CalendarOngoingStaleCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-ongoing-stale.cron.job';
20+
import { CalendarRelaunchFailedCalendarChannelsCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-relaunch-failed-calendar-channels.cron.job';
2021
import { CalDavDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/caldav-driver.module';
2122
import { GoogleCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module';
2223
import { MicrosoftCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/microsoft-calendar-driver.module';
2324
import { CalendarEventListFetchJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
2425
import { CalendarEventsImportJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job';
2526
import { CalendarOngoingStaleJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job';
27+
import { CalendarRelaunchFailedCalendarChannelJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job';
2628
import { CalendarAccountAuthenticationService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-account-authentication.service';
2729
import { CalendarEventImportErrorHandlerService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service';
2830
import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service';
@@ -71,15 +73,18 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres
7173
CalendarEventsImportJob,
7274
CalendarOngoingStaleCronJob,
7375
CalendarOngoingStaleCronCommand,
74-
CalendarRelaunchFailedCalendarChannelsCommand,
7576
CalendarOngoingStaleJob,
77+
CalendarRelaunchFailedCalendarChannelsCronJob,
78+
CalendarRelaunchFailedCalendarChannelsCronCommand,
79+
CalendarRelaunchFailedCalendarChannelJob,
7680
],
7781
exports: [
7882
CalendarEventsImportService,
7983
CalendarFetchEventsService,
8084
CalendarEventListFetchCronCommand,
8185
CalendarEventsImportCronCommand,
8286
CalendarOngoingStaleCronCommand,
87+
CalendarRelaunchFailedCalendarChannelsCronCommand,
8388
],
8489
})
8590
export class CalendarEventImportManagerModule {}

packages/twenty-server/src/modules/calendar/calendar-event-import-manager/commands/calendar-relaunch-failed-calendar-channels.command.ts

Lines changed: 0 additions & 85 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Command, CommandRunner } from 'nest-commander';
2+
3+
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
4+
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
5+
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
6+
import {
7+
CALENDAR_RELAUNCH_FAILED_CALENDAR_CHANNELS_CRON_PATTERN,
8+
CalendarRelaunchFailedCalendarChannelsCronJob,
9+
} from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-relaunch-failed-calendar-channels.cron.job';
10+
11+
@Command({
12+
name: 'cron:calendar:relaunch-failed-calendar-channels',
13+
description:
14+
'Starts a cron job to relaunch failed calendar channels every 30 minutes',
15+
})
16+
export class CalendarRelaunchFailedCalendarChannelsCronCommand extends CommandRunner {
17+
constructor(
18+
@InjectMessageQueue(MessageQueue.cronQueue)
19+
private readonly messageQueueService: MessageQueueService,
20+
) {
21+
super();
22+
}
23+
24+
async run(): Promise<void> {
25+
await this.messageQueueService.addCron<undefined>({
26+
jobName: CalendarRelaunchFailedCalendarChannelsCronJob.name,
27+
data: undefined,
28+
options: {
29+
repeat: {
30+
pattern: CALENDAR_RELAUNCH_FAILED_CALENDAR_CHANNELS_CRON_PATTERN,
31+
},
32+
},
33+
});
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
2+
3+
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
4+
import { DataSource, Repository } from 'typeorm';
5+
6+
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
7+
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
8+
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
9+
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
10+
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
11+
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
12+
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
13+
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
14+
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
15+
import {
16+
CalendarRelaunchFailedCalendarChannelJob,
17+
type CalendarRelaunchFailedCalendarChannelJobData,
18+
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job';
19+
import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
20+
21+
export const CALENDAR_RELAUNCH_FAILED_CALENDAR_CHANNELS_CRON_PATTERN =
22+
'*/30 * * * *';
23+
24+
@Processor(MessageQueue.cronQueue)
25+
export class CalendarRelaunchFailedCalendarChannelsCronJob {
26+
constructor(
27+
@InjectRepository(WorkspaceEntity)
28+
private readonly workspaceRepository: Repository<WorkspaceEntity>,
29+
@InjectMessageQueue(MessageQueue.calendarQueue)
30+
private readonly messageQueueService: MessageQueueService,
31+
@InjectDataSource()
32+
private readonly coreDataSource: DataSource,
33+
private readonly exceptionHandlerService: ExceptionHandlerService,
34+
) {}
35+
36+
@Process(CalendarRelaunchFailedCalendarChannelsCronJob.name)
37+
@SentryCronMonitor(
38+
CalendarRelaunchFailedCalendarChannelsCronJob.name,
39+
CALENDAR_RELAUNCH_FAILED_CALENDAR_CHANNELS_CRON_PATTERN,
40+
)
41+
async handle(): Promise<void> {
42+
const activeWorkspaces = await this.workspaceRepository.find({
43+
where: {
44+
activationStatus: WorkspaceActivationStatus.ACTIVE,
45+
},
46+
});
47+
48+
for (const activeWorkspace of activeWorkspaces) {
49+
try {
50+
const schemaName = getWorkspaceSchemaName(activeWorkspace.id);
51+
52+
const failedCalendarChannels = await this.coreDataSource.query(
53+
`SELECT * FROM ${schemaName}."calendarChannel" WHERE "syncStage" = '${CalendarChannelSyncStage.FAILED}'`,
54+
);
55+
56+
for (const calendarChannel of failedCalendarChannels) {
57+
await this.messageQueueService.add<CalendarRelaunchFailedCalendarChannelJobData>(
58+
CalendarRelaunchFailedCalendarChannelJob.name,
59+
{
60+
workspaceId: activeWorkspace.id,
61+
calendarChannelId: calendarChannel.id,
62+
},
63+
);
64+
}
65+
} catch (error) {
66+
this.exceptionHandlerService.captureExceptions([error], {
67+
workspace: {
68+
id: activeWorkspace.id,
69+
},
70+
});
71+
}
72+
}
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Scope } from '@nestjs/common';
2+
3+
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
4+
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
5+
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
6+
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
7+
import {
8+
CalendarChannelSyncStage,
9+
CalendarChannelSyncStatus,
10+
CalendarChannelWorkspaceEntity,
11+
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
12+
13+
export type CalendarRelaunchFailedCalendarChannelJobData = {
14+
workspaceId: string;
15+
calendarChannelId: string;
16+
};
17+
18+
@Processor({
19+
queueName: MessageQueue.calendarQueue,
20+
scope: Scope.REQUEST,
21+
})
22+
export class CalendarRelaunchFailedCalendarChannelJob {
23+
constructor(
24+
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
25+
) {}
26+
27+
@Process(CalendarRelaunchFailedCalendarChannelJob.name)
28+
async handle(data: CalendarRelaunchFailedCalendarChannelJobData) {
29+
const { workspaceId, calendarChannelId } = data;
30+
31+
const calendarChannelRepository =
32+
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
33+
workspaceId,
34+
'calendarChannel',
35+
{ shouldBypassPermissionChecks: true },
36+
);
37+
38+
const calendarChannel = await calendarChannelRepository.findOne({
39+
where: {
40+
id: calendarChannelId,
41+
},
42+
relations: {
43+
connectedAccount: {
44+
accountOwner: true,
45+
},
46+
},
47+
});
48+
49+
if (
50+
!calendarChannel ||
51+
calendarChannel.syncStage !== CalendarChannelSyncStage.FAILED
52+
) {
53+
return;
54+
}
55+
56+
await calendarChannelRepository.update(calendarChannelId, {
57+
syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
58+
syncStatus: CalendarChannelSyncStatus.ACTIVE,
59+
});
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Injectable } from '@nestjs/common';
22

3-
import { isDefined } from 'class-validator';
3+
import { GaxiosError } from 'gaxios';
44
import { google } from 'googleapis';
5+
import { isDefined } from 'twenty-shared/utils';
56

67
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
78
import {
@@ -23,19 +24,32 @@ export class GoogleAPIRefreshAccessTokenService {
2324
oAuth2Client.setCredentials({
2425
refresh_token: refreshToken,
2526
});
26-
27-
const { token } = await oAuth2Client.getAccessToken();
28-
29-
if (!isDefined(token)) {
30-
throw new ConnectedAccountRefreshAccessTokenException(
31-
'Failed to refresh google access token',
32-
ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED,
33-
);
27+
try {
28+
const { token } = await oAuth2Client.getAccessToken();
29+
30+
if (!isDefined(token)) {
31+
throw new ConnectedAccountRefreshAccessTokenException(
32+
'Error refreshing Google tokens: Invalid refresh token',
33+
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
34+
);
35+
}
36+
37+
return {
38+
accessToken: token,
39+
refreshToken,
40+
};
41+
} catch (error) {
42+
if (
43+
error instanceof GaxiosError &&
44+
error.response?.data?.error === 'invalid_grant'
45+
) {
46+
throw new ConnectedAccountRefreshAccessTokenException(
47+
'Error refreshing Google tokens: Invalid refresh token',
48+
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
49+
);
50+
}
51+
52+
throw error;
3453
}
35-
36-
return {
37-
accessToken: token as string,
38-
refreshToken,
39-
};
4054
}
4155
}

0 commit comments

Comments
 (0)