Refactoring the reconnect service (#12089)

following qrqc #3 : refactoring the reconnect service

Fixes https://github.com/twentyhq/twenty/issues/12064
This commit is contained in:
Guillim
2025-05-17 13:47:01 +02:00
committed by GitHub
parent e83baa5438
commit d93024fd02
13 changed files with 1286 additions and 437 deletions

View File

@ -12,12 +12,19 @@ import { MicrosoftAPIsAuthController } from 'src/engine/core-modules/auth/contro
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller';
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service';
import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service';
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
import { CreateMessageFolderService } from 'src/engine/core-modules/auth/services/create-message-folder.service';
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service';
import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service';
import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service';
import { ResetMessageFolderService } from 'src/engine/core-modules/auth/services/reset-message-folder.service';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
@ -114,11 +121,20 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
RefreshTokenService,
LoginTokenService,
ResetPasswordService,
// So far, it's not possible to have controllers in business modules
// which forces us to have these services in the auth module
// TODO: Move these calendar, message, and connected account services to the business modules once possible
ResetMessageChannelService,
ResetCalendarChannelService,
ResetMessageFolderService,
CreateMessageChannelService,
CreateCalendarChannelService,
CreateMessageFolderService,
CreateConnectedAccountService,
UpdateConnectedAccountOnReconnectService,
TransientTokenService,
ApiKeyService,
AuthSsoService,
// reenable when working on: https://github.com/twentyhq/twenty/issues/9143
// OAuthService,
],
exports: [AccessTokenService, LoginTokenService, RefreshTokenService],
})

View File

@ -0,0 +1,85 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import {
CalendarChannelVisibility,
CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export type CreateCalendarChannelInput = {
workspaceId: string;
connectedAccountId: string;
handle: string;
calendarVisibility?: CalendarChannelVisibility;
manager: WorkspaceEntityManager;
};
@Injectable()
export class CreateCalendarChannelService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
async createCalendarChannel(
input: CreateCalendarChannelInput,
): Promise<string> {
const {
workspaceId,
connectedAccountId,
handle,
calendarVisibility,
manager,
} = input;
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const newCalendarChannel = await calendarChannelRepository.save(
{
id: v4(),
connectedAccountId,
handle,
visibility:
calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING,
},
{},
manager,
);
const calendarChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'calendarChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'calendarChannel',
action: DatabaseEventAction.CREATED,
events: [
{
recordId: newCalendarChannel.id,
objectMetadata: calendarChannelMetadata,
properties: {
after: newCalendarChannel,
},
},
],
workspaceId,
});
return newCalendarChannel.id;
}
}

View File

@ -0,0 +1,90 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type CreateConnectedAccountInput = {
workspaceId: string;
connectedAccountId: string;
handle: string;
provider: ConnectedAccountProvider;
accessToken: string;
refreshToken: string;
accountOwnerId: string;
scopes: string[];
manager: WorkspaceEntityManager;
};
@Injectable()
export class CreateConnectedAccountService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
async createConnectedAccount(
input: CreateConnectedAccountInput,
): Promise<void> {
const {
workspaceId,
connectedAccountId,
handle,
provider,
accessToken,
refreshToken,
accountOwnerId,
scopes,
manager,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const newConnectedAccount = await connectedAccountRepository.save(
{
id: connectedAccountId,
handle,
provider,
accessToken,
refreshToken,
accountOwnerId,
scopes,
},
{},
manager,
);
const connectedAccountMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'connectedAccount', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'connectedAccount',
action: DatabaseEventAction.CREATED,
events: [
{
recordId: newConnectedAccount.id,
objectMetadata: connectedAccountMetadata,
properties: {
after: newConnectedAccount,
},
},
],
workspaceId,
});
}
}

View File

@ -0,0 +1,89 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import {
MessageChannelSyncStatus,
MessageChannelType,
MessageChannelVisibility,
MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
export type CreateMessageChannelInput = {
workspaceId: string;
connectedAccountId: string;
handle: string;
messageVisibility?: MessageChannelVisibility;
manager: WorkspaceEntityManager;
};
@Injectable()
export class CreateMessageChannelService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
async createMessageChannel(
input: CreateMessageChannelInput,
): Promise<string> {
const {
workspaceId,
connectedAccountId,
handle,
messageVisibility,
manager,
} = input;
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const newMessageChannel = await messageChannelRepository.save(
{
id: v4(),
connectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.ONGOING,
},
{},
manager,
);
const messageChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'messageChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'messageChannel',
action: DatabaseEventAction.CREATED,
events: [
{
recordId: newMessageChannel.id,
objectMetadata: messageChannelMetadata,
properties: {
after: newMessageChannel,
},
},
],
workspaceId,
});
return newMessageChannel.id;
}
}

View File

@ -0,0 +1,53 @@
import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/microsoft/types/folders';
export type CreateMessageFoldersInput = {
workspaceId: string;
messageChannelId: string;
manager: WorkspaceEntityManager;
};
@Injectable()
export class CreateMessageFolderService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async createMessageFolders(input: CreateMessageFoldersInput): Promise<void> {
const { workspaceId, messageChannelId, manager } = input;
const messageFolderRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageFolderWorkspaceEntity>(
workspaceId,
'messageFolder',
);
await messageFolderRepository.save(
{
id: v4(),
messageChannelId,
name: MessageFolderName.INBOX,
syncCursor: '',
},
{},
manager,
);
await messageFolderRepository.save(
{
id: v4(),
messageChannelId,
name: MessageFolderName.SENT_ITEMS,
syncCursor: '',
},
{},
manager,
);
}
}

View File

@ -0,0 +1,251 @@
import { Test, 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 { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service';
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service';
import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service';
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import {
CalendarChannelSyncStage,
CalendarChannelVisibility,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
jest.mock('uuid', () => ({
v4: jest.fn(() => 'mocked-uuid'),
}));
describe('GoogleAPIsService', () => {
let service: GoogleAPIsService;
let resetCalendarChannelService: ResetCalendarChannelService;
let resetMessageChannelService: ResetMessageChannelService;
let createMessageChannelService: CreateMessageChannelService;
const mockConnectedAccountRepository = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockCalendarChannelRepository = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockMessageChannelRepository = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockWorkspaceMemberRepository = {
findOneOrFail: jest.fn(),
};
const mockWorkspaceDataSource = {
transaction: jest.fn((callback) => callback({})),
};
const mockTwentyConfigService = {
get: jest.fn(),
};
const mockMessageQueueService = {
add: jest.fn(),
};
const mockCalendarQueueService = {
add: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
GoogleAPIsService,
{
provide: TwentyORMGlobalManager,
useValue: {
getRepositoryForWorkspace: jest
.fn()
.mockImplementation((workspaceId, entity) => {
if (entity === 'connectedAccount')
return mockConnectedAccountRepository;
if (entity === 'calendarChannel')
return mockCalendarChannelRepository;
if (entity === 'messageChannel')
return mockMessageChannelRepository;
if (entity === 'workspaceMember')
return mockWorkspaceMemberRepository;
return {};
}),
getDataSourceForWorkspace: jest
.fn()
.mockImplementation(() => mockWorkspaceDataSource),
},
},
{
provide: getRepositoryToken(ObjectMetadataEntity, 'metadata'),
useValue: {
findOneOrFail: jest.fn(),
},
},
{
provide: TwentyConfigService,
useValue: mockTwentyConfigService,
},
{
provide: ResetCalendarChannelService,
useValue: {
resetCalendarChannels: jest.fn(),
},
},
{
provide: ResetMessageChannelService,
useValue: {
resetMessageChannels: jest.fn(),
},
},
{
provide: CreateConnectedAccountService,
useValue: {
createConnectedAccount: jest.fn(),
},
},
{
provide: CreateMessageChannelService,
useValue: {
createMessageChannel: jest.fn(),
},
},
{
provide: CreateCalendarChannelService,
useValue: {
createCalendarChannel: jest.fn(),
},
},
{
provide: UpdateConnectedAccountOnReconnectService,
useValue: {
updateConnectedAccountOnReconnect: jest.fn(),
},
},
{
provide: AccountsToReconnectService,
useValue: {
removeAccountToReconnect: jest.fn(),
},
},
{
provide: WorkspaceEventEmitter,
useValue: {
emitDatabaseBatchEvent: jest.fn(),
},
},
{
provide: getQueueToken(MessageQueue.messagingQueue),
useValue: mockMessageQueueService,
},
{
provide: getQueueToken(MessageQueue.calendarQueue),
useValue: mockCalendarQueueService,
},
],
}).compile();
service = module.get<GoogleAPIsService>(GoogleAPIsService);
resetCalendarChannelService = module.get<ResetCalendarChannelService>(
ResetCalendarChannelService,
);
resetMessageChannelService = module.get<ResetMessageChannelService>(
ResetMessageChannelService,
);
createMessageChannelService = module.get<CreateMessageChannelService>(
CreateMessageChannelService,
);
});
describe('refreshGoogleRefreshToken', () => {
it('should reset calendar channels with FAILED_UNKNOWN syncStatus and FAILED syncStage', async () => {
mockTwentyConfigService.get.mockImplementation((key) => {
if (key === 'CALENDAR_PROVIDER_GOOGLE_ENABLED') return true;
if (key === 'MESSAGING_PROVIDER_GMAIL_ENABLED') return true;
return false;
});
const existingConnectedAccount = {
id: 'existing-account-id',
handle: 'test@example.com',
accountOwnerId: 'workspace-member-id',
provider: ConnectedAccountProvider.GOOGLE,
} as ConnectedAccountWorkspaceEntity;
mockConnectedAccountRepository.findOne.mockResolvedValue(
existingConnectedAccount,
);
mockWorkspaceMemberRepository.findOneOrFail.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
const failedCalendarChannel = {
id: 'calendar-channel-id',
connectedAccountId: 'existing-account-id',
syncStatus: 'FAILED_UNKNOWN',
syncStage: CalendarChannelSyncStage.FAILED,
};
mockCalendarChannelRepository.find.mockResolvedValue([
failedCalendarChannel,
]);
mockMessageChannelRepository.find.mockResolvedValue([]);
await service.refreshGoogleRefreshToken({
handle: 'test@example.com',
workspaceMemberId: 'workspace-member-id',
workspaceId: 'workspace-id',
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING,
messageVisibility: MessageChannelVisibility.SHARE_EVERYTHING,
});
expect(
resetCalendarChannelService.resetCalendarChannels,
).toHaveBeenCalledWith({
workspaceId: 'workspace-id',
connectedAccountId: 'existing-account-id',
manager: expect.any(Object),
});
expect(
resetMessageChannelService.resetMessageChannels,
).toHaveBeenCalledWith({
workspaceId: 'workspace-id',
connectedAccountId: 'existing-account-id',
manager: expect.any(Object),
});
expect(
createMessageChannelService.createMessageChannel,
).not.toHaveBeenCalled();
});
});
});

View File

@ -5,7 +5,12 @@ import { ConnectedAccountProvider } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service';
import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service';
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service';
import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service';
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
import { getGoogleApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes';
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';
@ -20,16 +25,12 @@ import {
CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import {
CalendarChannelSyncStage,
CalendarChannelVisibility,
CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
MessageChannelType,
MessageChannelVisibility,
MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@ -49,6 +50,12 @@ export class GoogleAPIsService {
private readonly calendarQueueService: MessageQueueService,
private readonly twentyConfigService: TwentyConfigService,
private readonly accountsToReconnectService: AccountsToReconnectService,
private readonly resetMessageChannelService: ResetMessageChannelService,
private readonly resetCalendarChannelService: ResetCalendarChannelService,
private readonly createMessageChannelService: CreateMessageChannelService,
private readonly createCalendarChannelService: CreateCalendarChannelService,
private readonly createConnectedAccountService: CreateConnectedAccountService,
private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@ -110,145 +117,47 @@ export class GoogleAPIsService {
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
const newConnectedAccount = await connectedAccountRepository.save(
{
id: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.GOOGLE,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
},
{},
manager,
);
const connectedAccountMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'connectedAccount', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'connectedAccount',
action: DatabaseEventAction.CREATED,
events: [
{
recordId: newConnectedAccount.id,
objectMetadata: connectedAccountMetadata,
properties: {
after: newConnectedAccount,
},
},
],
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.GOOGLE,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
const newMessageChannel = await messageChannelRepository.save(
{
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.ONGOING,
},
{},
manager,
);
const messageChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'messageChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'messageChannel',
action: DatabaseEventAction.CREATED,
events: [
{
recordId: newMessageChannel.id,
objectMetadata: messageChannelMetadata,
properties: {
after: newMessageChannel,
},
},
],
await this.createMessageChannelService.createMessageChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
messageVisibility,
manager,
});
if (isCalendarEnabled) {
const newCalendarChannel = await calendarChannelRepository.save(
{
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
handle,
visibility:
calendarVisibility ||
CalendarChannelVisibility.SHARE_EVERYTHING,
},
{},
manager,
);
const calendarChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'calendarChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'calendarChannel',
action: DatabaseEventAction.CREATED,
events: [
{
recordId: newCalendarChannel.id,
objectMetadata: calendarChannelMetadata,
properties: {
after: newCalendarChannel,
},
},
],
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
}
} else {
const updatedConnectedAccount =
await connectedAccountRepository.update(
{
id: newOrExistingConnectedAccountId,
},
{
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
},
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
);
const connectedAccountMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'connectedAccount', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'connectedAccount',
action: DatabaseEventAction.UPDATED,
events: [
{
recordId: newOrExistingConnectedAccountId,
objectMetadata: connectedAccountMetadata,
properties: {
before: connectedAccount,
after: {
...connectedAccount,
...updatedConnectedAccount.raw[0],
},
},
},
],
workspaceId,
});
},
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
@ -270,81 +179,16 @@ export class GoogleAPIsService {
newOrExistingConnectedAccountId,
);
const messageChannels = await messageChannelRepository.find({
where: { connectedAccountId: newOrExistingConnectedAccountId },
});
const messageChannelUpdates = await messageChannelRepository.update(
{
connectedAccountId: newOrExistingConnectedAccountId,
},
{
syncStage:
MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING,
syncStatus: null,
syncCursor: '',
syncStageStartedAt: null,
},
manager,
);
const messageChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'messageChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'messageChannel',
action: DatabaseEventAction.UPDATED,
events: messageChannels.map((messageChannel) => ({
recordId: messageChannel.id,
objectMetadata: messageChannelMetadata,
properties: {
before: messageChannel,
after: { ...messageChannel, ...messageChannelUpdates.raw[0] },
},
})),
await this.resetMessageChannelService.resetMessageChannels({
workspaceId,
});
const calendarChannels = await calendarChannelRepository.find({
where: { connectedAccountId: newOrExistingConnectedAccountId },
});
const calendarChannelUpdates = await calendarChannelRepository.update(
{
connectedAccountId: newOrExistingConnectedAccountId,
},
{
syncStage:
CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING,
syncStatus: null,
syncCursor: '',
syncStageStartedAt: null,
},
connectedAccountId: newOrExistingConnectedAccountId,
manager,
);
});
const calendarChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'calendarChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'calendarChannel',
action: DatabaseEventAction.UPDATED,
events: calendarChannels.map((calendarChannel) => ({
recordId: calendarChannel.id,
objectMetadata: calendarChannelMetadata,
properties: {
before: calendarChannel,
after: {
...calendarChannel,
...calendarChannelUpdates.raw[0],
},
},
})),
await this.resetCalendarChannelService.resetCalendarChannels({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
manager,
});
}
},

View File

@ -0,0 +1,272 @@
import { Test, 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 { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service';
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
import { CreateMessageFolderService } from 'src/engine/core-modules/auth/services/create-message-folder.service';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service';
import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service';
import { ResetMessageFolderService } from 'src/engine/core-modules/auth/services/reset-message-folder.service';
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import {
CalendarChannelSyncStage,
CalendarChannelVisibility,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
jest.mock('uuid', () => ({
v4: jest.fn(() => 'mocked-uuid'),
}));
describe('MicrosoftAPIsService', () => {
let service: MicrosoftAPIsService;
let resetCalendarChannelService: ResetCalendarChannelService;
let resetMessageChannelService: ResetMessageChannelService;
let createMessageChannelService: CreateMessageChannelService;
const mockConnectedAccountRepository = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockCalendarChannelRepository = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockMessageChannelRepository = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockWorkspaceMemberRepository = {
findOneOrFail: jest.fn(),
};
const mockWorkspaceDataSource = {
transaction: jest.fn((callback) => callback({})),
};
const mockTwentyConfigService = {
get: jest.fn(),
};
const mockMessageQueueService = {
add: jest.fn(),
};
const mockCalendarQueueService = {
add: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MicrosoftAPIsService,
{
provide: TwentyORMGlobalManager,
useValue: {
getRepositoryForWorkspace: jest
.fn()
.mockImplementation((workspaceId, entity) => {
if (entity === 'connectedAccount')
return mockConnectedAccountRepository;
if (entity === 'calendarChannel')
return mockCalendarChannelRepository;
if (entity === 'messageChannel')
return mockMessageChannelRepository;
if (entity === 'workspaceMember')
return mockWorkspaceMemberRepository;
return {};
}),
getDataSourceForWorkspace: jest
.fn()
.mockImplementation(() => mockWorkspaceDataSource),
},
},
{
provide: getRepositoryToken(ObjectMetadataEntity, 'metadata'),
useValue: {
findOneOrFail: jest.fn(),
},
},
{
provide: TwentyConfigService,
useValue: mockTwentyConfigService,
},
{
provide: ResetCalendarChannelService,
useValue: {
resetCalendarChannels: jest.fn(),
},
},
{
provide: ResetMessageChannelService,
useValue: {
resetMessageChannels: jest.fn(),
},
},
{
provide: ResetMessageFolderService,
useValue: {
resetMessageFolders: jest.fn(),
},
},
{
provide: CreateConnectedAccountService,
useValue: {
createConnectedAccount: jest.fn(),
},
},
{
provide: CreateMessageChannelService,
useValue: {
createMessageChannel: jest
.fn()
.mockResolvedValue('message-channel-id'),
},
},
{
provide: CreateMessageFolderService,
useValue: {
createMessageFolders: jest.fn(),
},
},
{
provide: CreateCalendarChannelService,
useValue: {
createCalendarChannel: jest.fn(),
},
},
{
provide: UpdateConnectedAccountOnReconnectService,
useValue: {
updateConnectedAccountOnReconnect: jest.fn(),
},
},
{
provide: AccountsToReconnectService,
useValue: {
removeAccountToReconnect: jest.fn(),
},
},
{
provide: WorkspaceEventEmitter,
useValue: {
emitDatabaseBatchEvent: jest.fn(),
},
},
{
provide: getQueueToken(MessageQueue.messagingQueue),
useValue: mockMessageQueueService,
},
{
provide: getQueueToken(MessageQueue.calendarQueue),
useValue: mockCalendarQueueService,
},
],
}).compile();
service = module.get<MicrosoftAPIsService>(MicrosoftAPIsService);
resetCalendarChannelService = module.get<ResetCalendarChannelService>(
ResetCalendarChannelService,
);
resetMessageChannelService = module.get<ResetMessageChannelService>(
ResetMessageChannelService,
);
createMessageChannelService = module.get<CreateMessageChannelService>(
CreateMessageChannelService,
);
});
describe('refreshMicrosoftRefreshToken', () => {
it('should reset calendar channels and message channels', async () => {
mockTwentyConfigService.get.mockImplementation((key) => {
if (key === 'CALENDAR_PROVIDER_MICROSOFT_ENABLED') return true;
if (key === 'MESSAGING_PROVIDER_MICROSOFT_ENABLED') return true;
return false;
});
const existingConnectedAccount = {
id: 'existing-account-id',
handle: 'test@example.com',
accountOwnerId: 'workspace-member-id',
provider: ConnectedAccountProvider.MICROSOFT,
} as ConnectedAccountWorkspaceEntity;
mockConnectedAccountRepository.findOne.mockResolvedValue(
existingConnectedAccount,
);
mockWorkspaceMemberRepository.findOneOrFail.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
const failedCalendarChannel = {
id: 'calendar-channel-id',
connectedAccountId: 'existing-account-id',
syncStatus: 'FAILED_UNKNOWN',
syncStage: CalendarChannelSyncStage.FAILED,
};
mockCalendarChannelRepository.find.mockResolvedValue([
failedCalendarChannel,
]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'message-channel-id',
connectedAccountId: 'existing-account-id',
},
]);
await service.refreshMicrosoftRefreshToken({
handle: 'test@example.com',
workspaceMemberId: 'workspace-member-id',
workspaceId: 'workspace-id',
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING,
messageVisibility: MessageChannelVisibility.SHARE_EVERYTHING,
});
expect(
resetCalendarChannelService.resetCalendarChannels,
).toHaveBeenCalledWith({
workspaceId: 'workspace-id',
connectedAccountId: 'existing-account-id',
manager: expect.any(Object),
});
expect(
resetMessageChannelService.resetMessageChannels,
).toHaveBeenCalledWith({
workspaceId: 'workspace-id',
connectedAccountId: 'existing-account-id',
manager: expect.any(Object),
});
expect(
createMessageChannelService.createMessageChannel,
).not.toHaveBeenCalled();
});
});
});

View File

@ -5,7 +5,14 @@ import { ConnectedAccountProvider } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service';
import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service';
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
import { CreateMessageFolderService } from 'src/engine/core-modules/auth/services/create-message-folder.service';
import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service';
import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service';
import { ResetMessageFolderService } from 'src/engine/core-modules/auth/services/reset-message-folder.service';
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
import { getMicrosoftApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes';
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';
@ -20,21 +27,15 @@ import {
CalendarEventListFetchJobData,
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
import {
CalendarChannelSyncStage,
CalendarChannelVisibility,
CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
MessageChannelType,
MessageChannelVisibility,
MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/microsoft/types/folders';
import {
MessagingMessageListFetchJob,
MessagingMessageListFetchJobData,
@ -50,6 +51,14 @@ export class MicrosoftAPIsService {
@InjectMessageQueue(MessageQueue.calendarQueue)
private readonly calendarQueueService: MessageQueueService,
private readonly accountsToReconnectService: AccountsToReconnectService,
private readonly resetMessageChannelService: ResetMessageChannelService,
private readonly resetMessageFolderService: ResetMessageFolderService,
private readonly resetCalendarChannelService: ResetCalendarChannelService,
private readonly createMessageChannelService: CreateMessageChannelService,
private readonly createCalendarChannelService: CreateCalendarChannelService,
private readonly createMessageFolderService: CreateMessageFolderService,
private readonly createConnectedAccountService: CreateConnectedAccountService,
private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@ -98,12 +107,6 @@ export class MicrosoftAPIsService {
'messageChannel',
);
const messageFolderRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageFolderWorkspaceEntity>(
workspaceId,
'messageFolder',
);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
@ -114,169 +117,56 @@ export class MicrosoftAPIsService {
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
if (!existingAccountId) {
const newConnectedAccount = await connectedAccountRepository.save(
{
id: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
},
{},
manager,
);
const connectedAccountMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'connectedAccount', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'connectedAccount',
action: DatabaseEventAction.CREATED,
events: [
{
recordId: newConnectedAccount.id,
objectMetadata: connectedAccountMetadata,
properties: {
after: newConnectedAccount,
},
},
],
await this.createConnectedAccountService.createConnectedAccount({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
accountOwnerId: workspaceMemberId,
scopes,
manager,
});
const newMessageChannel = await messageChannelRepository.save(
{
id: v4(),
const newMessageChannelId =
await this.createMessageChannelService.createMessageChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
type: MessageChannelType.EMAIL,
handle,
visibility:
messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING,
syncStatus: MessageChannelSyncStatus.ONGOING,
},
{},
manager,
);
await messageFolderRepository.save(
{
id: v4(),
messageChannelId: newMessageChannel.id,
name: MessageFolderName.INBOX,
syncCursor: '',
},
{},
manager,
);
await messageFolderRepository.save(
{
id: v4(),
messageChannelId: newMessageChannel.id,
name: MessageFolderName.SENT_ITEMS,
syncCursor: '',
},
{},
manager,
);
const messageChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'messageChannel', workspaceId },
messageVisibility,
manager,
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'messageChannel',
action: DatabaseEventAction.CREATED,
events: [
{
recordId: newMessageChannel.id,
objectMetadata: messageChannelMetadata,
properties: {
after: newMessageChannel,
},
},
],
await this.createMessageFolderService.createMessageFolders({
workspaceId,
messageChannelId: newMessageChannelId,
manager,
});
if (
this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')
) {
const newCalendarChannel = await calendarChannelRepository.save(
{
id: v4(),
connectedAccountId: newOrExistingConnectedAccountId,
handle,
visibility:
calendarVisibility ||
CalendarChannelVisibility.SHARE_EVERYTHING,
},
{},
manager,
);
const calendarChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'calendarChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'calendarChannel',
action: DatabaseEventAction.CREATED,
events: [
{
recordId: newCalendarChannel.id,
objectMetadata: calendarChannelMetadata,
properties: {
after: newCalendarChannel,
},
},
],
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
handle,
calendarVisibility,
manager,
});
}
} else {
const updatedConnectedAccount =
await connectedAccountRepository.update(
{
id: newOrExistingConnectedAccountId,
},
{
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
},
await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect(
{
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
);
const connectedAccountMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'connectedAccount', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'connectedAccount',
action: DatabaseEventAction.UPDATED,
events: [
{
recordId: newOrExistingConnectedAccountId,
objectMetadata: connectedAccountMetadata,
properties: {
before: connectedAccount,
after: {
...connectedAccount,
...updatedConnectedAccount.raw[0],
},
},
},
],
workspaceId,
});
},
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
@ -298,82 +188,22 @@ export class MicrosoftAPIsService {
newOrExistingConnectedAccountId,
);
const messageChannels = await messageChannelRepository.find({
where: { connectedAccountId: newOrExistingConnectedAccountId },
});
const messageChannelUpdates = await messageChannelRepository.update(
{
connectedAccountId: newOrExistingConnectedAccountId,
},
{
syncStage:
MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING,
syncStatus: MessageChannelSyncStatus.ONGOING,
syncCursor: '',
syncStageStartedAt: null,
},
manager,
);
const messageChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'messageChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'messageChannel',
action: DatabaseEventAction.UPDATED,
events: messageChannels.map((messageChannel) => ({
recordId: messageChannel.id,
objectMetadata: messageChannelMetadata,
properties: {
before: messageChannel,
after: { ...messageChannel, ...messageChannelUpdates.raw[0] },
},
})),
await this.resetMessageChannelService.resetMessageChannels({
workspaceId,
});
// now for the calendar channels
const calendarChannels = await calendarChannelRepository.find({
where: { connectedAccountId: newOrExistingConnectedAccountId },
});
const calendarChannelUpdates = await calendarChannelRepository.update(
{
connectedAccountId: newOrExistingConnectedAccountId,
},
{
syncStage:
CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING,
syncStatus: null,
syncCursor: '',
syncStageStartedAt: null,
},
connectedAccountId: newOrExistingConnectedAccountId,
manager,
);
});
const calendarChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'calendarChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'calendarChannel',
action: DatabaseEventAction.UPDATED,
events: calendarChannels.map((calendarChannel) => ({
recordId: calendarChannel.id,
objectMetadata: calendarChannelMetadata,
properties: {
before: calendarChannel,
after: {
...calendarChannel,
...calendarChannelUpdates.raw[0],
},
},
})),
await this.resetMessageFolderService.resetMessageFolders({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
manager,
});
await this.resetCalendarChannelService.resetCalendarChannels({
workspaceId,
connectedAccountId: newOrExistingConnectedAccountId,
manager,
});
}
},

View File

@ -0,0 +1,84 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import {
CalendarChannelSyncStage,
CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export type ResetCalendarChannelsInput = {
workspaceId: string;
connectedAccountId: string;
manager: WorkspaceEntityManager;
};
@Injectable()
export class ResetCalendarChannelService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
async resetCalendarChannels(
input: ResetCalendarChannelsInput,
): Promise<void> {
const { workspaceId, connectedAccountId, manager } = input;
const calendarChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const calendarChannels = await calendarChannelRepository.find({
where: { connectedAccountId },
});
const calendarChannelUpdates = await calendarChannelRepository.update(
{
connectedAccountId,
},
{
syncStage:
CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING,
syncStatus: null,
syncCursor: '',
syncStageStartedAt: null,
},
manager,
);
const calendarChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'calendarChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'calendarChannel',
action: DatabaseEventAction.UPDATED,
events: calendarChannels.map((calendarChannel) => ({
recordId: calendarChannel.id,
objectMetadata: calendarChannelMetadata,
properties: {
before: calendarChannel,
after: {
...calendarChannel,
...calendarChannelUpdates.raw[0],
},
},
})),
workspaceId,
});
return;
}
}

View File

@ -0,0 +1,78 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import {
MessageChannelSyncStage,
MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
export type ResetMessageChannelsInput = {
workspaceId: string;
connectedAccountId: string;
manager: WorkspaceEntityManager;
};
@Injectable()
export class ResetMessageChannelService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
async resetMessageChannels(input: ResetMessageChannelsInput): Promise<void> {
const { workspaceId, connectedAccountId, manager } = input;
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const messageChannels = await messageChannelRepository.find({
where: { connectedAccountId },
});
const messageChannelUpdates = await messageChannelRepository.update(
{
connectedAccountId,
},
{
syncStage: MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING,
syncStatus: null,
syncCursor: '',
syncStageStartedAt: null,
},
manager,
);
const messageChannelMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'messageChannel', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'messageChannel',
action: DatabaseEventAction.UPDATED,
events: messageChannels.map((messageChannel) => ({
recordId: messageChannel.id,
objectMetadata: messageChannelMetadata,
properties: {
before: messageChannel,
after: { ...messageChannel, ...messageChannelUpdates.raw[0] },
},
})),
workspaceId,
});
return;
}
}

View File

@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
export type ResetMessageFoldersInput = {
workspaceId: string;
connectedAccountId: string;
manager: WorkspaceEntityManager;
};
@Injectable()
export class ResetMessageFolderService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resetMessageFolders(input: ResetMessageFoldersInput): Promise<void> {
const { workspaceId, connectedAccountId, manager } = input;
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const messageChannels = await messageChannelRepository.find({
where: { connectedAccountId },
});
const messageChannelIds = messageChannels.map((channel) => channel.id);
if (messageChannelIds.length === 0) {
return;
}
const messageFolderRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageFolderWorkspaceEntity>(
workspaceId,
'messageFolder',
);
const messageFolders = await messageFolderRepository.find({
where: {
messageChannelId: In(messageChannelIds),
},
});
if (messageFolders.length === 0) {
return;
}
await messageFolderRepository.update(
{
messageChannelId: In(messageChannelIds),
},
{
syncCursor: '',
},
manager,
);
return;
}
}

View File

@ -0,0 +1,88 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type UpdateConnectedAccountOnReconnectInput = {
workspaceId: string;
connectedAccountId: string;
accessToken: string;
refreshToken: string;
scopes: string[];
connectedAccount: ConnectedAccountWorkspaceEntity;
manager: WorkspaceEntityManager;
};
@Injectable()
export class UpdateConnectedAccountOnReconnectService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
async updateConnectedAccountOnReconnect(
input: UpdateConnectedAccountOnReconnectInput,
): Promise<void> {
const {
workspaceId,
connectedAccountId,
accessToken,
refreshToken,
scopes,
connectedAccount,
manager,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const updatedConnectedAccount = await connectedAccountRepository.update(
{
id: connectedAccountId,
},
{
accessToken,
refreshToken,
scopes,
authFailedAt: null,
},
manager,
);
const connectedAccountMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: { nameSingular: 'connectedAccount', workspaceId },
});
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'connectedAccount',
action: DatabaseEventAction.UPDATED,
events: [
{
recordId: connectedAccountId,
objectMetadata: connectedAccountMetadata,
properties: {
before: connectedAccount,
after: {
...connectedAccount,
...updatedConnectedAccount.raw[0],
},
},
},
],
workspaceId,
});
}
}