feat: migrate ConnectedAccount infrastructure entities to metadata schema (#18784)

## Summary

- Migrates 4 entities (`connectedAccount`, `messageChannel`,
`calendarChannel`, `messageFolder`) from per-workspace schemas to the
shared `core` metadata schema
- Introduces a `IS_CONNECTED_ACCOUNT_MIGRATED` feature flag to control
the migration: when enabled, reads come from core metadata and all
writes are dual-written to both workspace and core
- Extracts 12 enums from workspace entity files to `twenty-shared` for
reuse across frontend and backend
- Creates new TypeORM entities, metadata services, GraphQL
resolvers/DTOs, and exception interceptors per entity
- Each entity owns its own data access module
(`ConnectedAccountDataAccessModule`, `MessageChannelDataAccessModule`,
`CalendarChannelDataAccessModule`, `MessageFolderDataAccessModule`) — no
umbrella infrastructure module
- Adds a 1.20 upgrade command that backfills data from workspace schemas
to core (preserving UUIDs) and enables the feature flag
- Replaces direct repository access with data access service calls
across ~50 files in messaging, calendar, and connected-account modules
- Adds `lastSignedInAt` and `oidcTokenClaims` fields to the new
`ConnectedAccountEntity`
- Drops unused `lastSyncHistoryId` field from the migrated connected
account entity

## Test plan

- [x] Lint passes (`npx nx lint:diff-with-main twenty-server`)
- [x] Typecheck passes (`npx nx typecheck twenty-server`)
- [x] All unit tests pass (477 suites, 4267 tests, 0 failures)
- [ ] Manual test: verify messaging sync works with feature flag
disabled (existing behavior)
- [ ] Manual test: run upgrade command on a workspace, verify data
backfilled to core tables
- [ ] Manual test: verify messaging/calendar sync works with feature
flag enabled (dual-write path)
- [ ] Manual test: verify GraphQL metadata resolvers return correct data
when flag enabled
This commit is contained in:
Charles Bochet
2026-03-20 00:34:58 +01:00
committed by GitHub
parent cd594ce8bd
commit cee4cf6452
149 changed files with 7338 additions and 1699 deletions
@@ -59,6 +59,9 @@ import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-inv
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
@@ -104,6 +107,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
ConnectedAccountModule,
MessagingCommonModule,
MessagingFolderSyncManagerModule,
CalendarChannelDataAccessModule,
ConnectedAccountDataAccessModule,
MessageChannelDataAccessModule,
WorkspaceSSOModule,
FeatureFlagModule,
WorkspaceInvitationModule,
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import { CalendarChannelDataAccessService } from 'src/engine/metadata-modules/calendar-channel/data-access/services/calendar-channel-data-access.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';
@@ -9,7 +10,6 @@ import {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
CalendarChannelVisibility,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
export type CreateCalendarChannelInput = {
@@ -25,6 +25,7 @@ export type CreateCalendarChannelInput = {
export class CreateCalendarChannelService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
) {}
async createCalendarChannel(
@@ -43,15 +44,12 @@ export class CreateCalendarChannelService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const newCalendarChannelId = v4();
const newCalendarChannel = await calendarChannelRepository.save(
await this.calendarChannelDataAccessService.save(
workspaceId,
{
id: v4(),
id: newCalendarChannelId,
connectedAccountId,
handle,
visibility:
@@ -63,11 +61,10 @@ export class CreateCalendarChannelService {
? CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING
: CalendarChannelSyncStage.PENDING_CONFIGURATION,
},
{},
manager,
);
return newCalendarChannel.id;
return newCalendarChannelId;
},
authContext,
);
@@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common';
import { type ConnectedAccountProvider } from 'twenty-shared/types';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.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 ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type CreateConnectedAccountInput = {
workspaceId: string;
@@ -23,6 +23,7 @@ export type CreateConnectedAccountInput = {
export class CreateConnectedAccountService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
) {}
async createConnectedAccount(
@@ -43,13 +44,8 @@ export class CreateConnectedAccountService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
await connectedAccountRepository.save(
await this.connectedAccountDataAccessService.save(
workspaceId,
{
id: connectedAccountId,
handle,
@@ -59,7 +55,6 @@ export class CreateConnectedAccountService {
accountOwnerId,
scopes,
},
{},
manager,
);
}, authContext);
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
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 { 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';
@@ -29,6 +30,7 @@ export type CreateMessageChannelInput = {
export class CreateMessageChannelService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly syncMessageFoldersService: SyncMessageFoldersService,
) {}
@@ -48,15 +50,10 @@ export class CreateMessageChannelService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const newMessageChannelId = v4();
await messageChannelRepository.insert(
await this.messageChannelDataAccessService.save(
workspaceId,
{
id: newMessageChannelId,
connectedAccountId,
@@ -76,17 +73,19 @@ export class CreateMessageChannelService {
manager,
);
const createdMessageChannel = await messageChannelRepository.findOne({
where: { id: newMessageChannelId },
relations: ['connectedAccount', 'messageFolders'],
});
const createdMessageChannel =
await this.messageChannelDataAccessService.findOne(workspaceId, {
where: { id: newMessageChannelId },
relations: ['connectedAccount', 'messageFolders'],
});
if (!isDefined(createdMessageChannel)) {
throw new Error('Message channel not found');
}
await this.syncMessageFoldersService.syncMessageFolders({
messageChannel: createdMessageChannel,
messageChannel:
createdMessageChannel as unknown as MessageChannelWorkspaceEntity,
workspaceId,
});
@@ -11,6 +11,9 @@ import { GoogleApisServiceAvailabilityService } from 'src/engine/core-modules/au
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
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 { 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';
@@ -36,22 +39,16 @@ describe('GoogleAPIsService', () => {
let messagingChannelSyncStatusService: MessageChannelSyncStatusService;
let createMessageChannelService: CreateMessageChannelService;
const mockConnectedAccountRepository = {
const mockConnectedAccountDataAccessService = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockCalendarChannelRepository = {
findOne: jest.fn(),
const mockMessageChannelDataAccessService = {
find: jest.fn(),
update: jest.fn(),
};
const mockMessageChannelRepository = {
findOne: jest.fn(),
const mockCalendarChannelDataAccessService = {
find: jest.fn(),
update: jest.fn(),
};
const mockWorkspaceMemberRepository = {
@@ -84,12 +81,6 @@ describe('GoogleAPIsService', () => {
getRepository: 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;
@@ -188,6 +179,18 @@ describe('GoogleAPIsService', () => {
isFeatureEnabled: jest.fn().mockResolvedValue(false),
},
},
{
provide: ConnectedAccountDataAccessService,
useValue: mockConnectedAccountDataAccessService,
},
{
provide: MessageChannelDataAccessService,
useValue: mockMessageChannelDataAccessService,
},
{
provide: CalendarChannelDataAccessService,
useValue: mockCalendarChannelDataAccessService,
},
],
}).compile();
@@ -221,7 +224,7 @@ describe('GoogleAPIsService', () => {
provider: ConnectedAccountProvider.GOOGLE,
} as ConnectedAccountWorkspaceEntity;
mockConnectedAccountRepository.findOne.mockResolvedValue(
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(
existingConnectedAccount,
);
@@ -237,11 +240,11 @@ describe('GoogleAPIsService', () => {
syncStage: CalendarChannelSyncStage.FAILED,
};
mockCalendarChannelRepository.find.mockResolvedValue([
mockCalendarChannelDataAccessService.find.mockResolvedValue([
failedCalendarChannel,
]);
mockMessageChannelRepository.find.mockResolvedValue([]);
mockMessageChannelDataAccessService.find.mockResolvedValue([]);
await service.refreshGoogleRefreshToken({
handle: 'test@example.com',
@@ -18,6 +18,9 @@ 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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
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 { 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';
@@ -29,16 +32,14 @@ import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/se
import {
CalendarChannelSyncStage,
type CalendarChannelVisibility,
type 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 { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
type MessageChannelVisibility,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessageListFetchJob,
@@ -65,6 +66,9 @@ export class GoogleAPIsService {
private readonly googleAPIScopesService: GoogleAPIScopesService,
private readonly googleApisServiceAvailabilityService: GoogleApisServiceAvailabilityService,
private readonly featureFlagService: FeatureFlagService,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
) {}
async refreshGoogleRefreshToken(input: {
@@ -128,31 +132,14 @@ export class GoogleAPIsService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
@@ -200,7 +187,6 @@ export class GoogleAPIsService {
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
@@ -229,9 +215,10 @@ export class GoogleAPIsService {
);
if (isMessagingEnabled) {
const messageChannels = await messageChannelRepository.find({
where: { connectedAccountId: newOrExistingConnectedAccountId },
});
const messageChannels =
await this.messageChannelDataAccessService.find(workspaceId, {
connectedAccountId: newOrExistingConnectedAccountId,
});
if (!isMessagingAvailable && messageChannels.length > 0) {
await this.messagingChannelSyncStatusService.markAsFailed(
@@ -261,9 +248,12 @@ export class GoogleAPIsService {
}
if (isCalendarEnabled) {
const calendarChannels = await calendarChannelRepository.find({
where: { connectedAccountId: newOrExistingConnectedAccountId },
});
const calendarChannels =
await this.calendarChannelDataAccessService.find(workspaceId, {
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
if (!isCalendarAvailable && calendarChannels.length > 0) {
await this.calendarChannelSyncStatusService.markAsFailedInsufficientPermissionsAndFlushCalendarEventsToImport(
@@ -8,6 +8,9 @@ import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/serv
import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service';
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 { 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';
@@ -33,22 +36,16 @@ describe('MicrosoftAPIsService', () => {
let calendarChannelSyncStatusService: CalendarChannelSyncStatusService;
let createMessageChannelService: CreateMessageChannelService;
const mockConnectedAccountRepository = {
const mockConnectedAccountDataAccessService = {
findOne: jest.fn(),
find: jest.fn(),
update: jest.fn(),
};
const mockCalendarChannelRepository = {
findOne: jest.fn(),
const mockMessageChannelDataAccessService = {
find: jest.fn(),
update: jest.fn(),
};
const mockMessageChannelRepository = {
findOne: jest.fn(),
const mockCalendarChannelDataAccessService = {
find: jest.fn(),
update: jest.fn(),
};
const mockWorkspaceMemberRepository = {
@@ -81,12 +78,6 @@ describe('MicrosoftAPIsService', () => {
getRepository: 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;
@@ -162,6 +153,18 @@ describe('MicrosoftAPIsService', () => {
provide: getQueueToken(MessageQueue.calendarQueue),
useValue: mockCalendarQueueService,
},
{
provide: ConnectedAccountDataAccessService,
useValue: mockConnectedAccountDataAccessService,
},
{
provide: MessageChannelDataAccessService,
useValue: mockMessageChannelDataAccessService,
},
{
provide: CalendarChannelDataAccessService,
useValue: mockCalendarChannelDataAccessService,
},
],
}).compile();
@@ -195,7 +198,7 @@ describe('MicrosoftAPIsService', () => {
provider: ConnectedAccountProvider.MICROSOFT,
} as ConnectedAccountWorkspaceEntity;
mockConnectedAccountRepository.findOne.mockResolvedValue(
mockConnectedAccountDataAccessService.findOne.mockResolvedValue(
existingConnectedAccount,
);
@@ -211,11 +214,11 @@ describe('MicrosoftAPIsService', () => {
syncStage: CalendarChannelSyncStage.FAILED,
};
mockCalendarChannelRepository.find.mockResolvedValue([
mockCalendarChannelDataAccessService.find.mockResolvedValue([
failedCalendarChannel,
]);
mockMessageChannelRepository.find.mockResolvedValue([
mockMessageChannelDataAccessService.find.mockResolvedValue([
{
id: 'message-channel-id',
connectedAccountId: 'existing-account-id',
@@ -12,6 +12,9 @@ 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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
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 { 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';
@@ -23,15 +26,13 @@ import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/se
import {
CalendarChannelSyncStage,
type CalendarChannelVisibility,
type 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 { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageChannelSyncStage,
type MessageChannelVisibility,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessageListFetchJob,
@@ -55,6 +56,9 @@ export class MicrosoftAPIsService {
private readonly createConnectedAccountService: CreateConnectedAccountService,
private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService,
private readonly twentyConfigService: TwentyConfigService,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly calendarChannelDataAccessService: CalendarChannelDataAccessService,
) {}
async refreshMicrosoftRefreshToken(input: {
@@ -82,31 +86,14 @@ export class MicrosoftAPIsService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { handle, accountOwnerId: workspaceMemberId },
});
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { handle, accountOwnerId: workspaceMemberId },
});
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
const calendarChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<CalendarChannelWorkspaceEntity>(
workspaceId,
'calendarChannel',
);
const messageChannelRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageChannelWorkspaceEntity>(
workspaceId,
'messageChannel',
);
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
@@ -156,7 +143,6 @@ export class MicrosoftAPIsService {
accessToken: input.accessToken,
refreshToken: input.refreshToken,
scopes,
connectedAccount,
manager,
},
);
@@ -197,11 +183,10 @@ export class MicrosoftAPIsService {
if (
this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED')
) {
const messageChannels = await messageChannelRepository.find({
where: {
const messageChannels =
await this.messageChannelDataAccessService.find(workspaceId, {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
});
for (const messageChannel of messageChannels) {
if (
@@ -222,11 +207,12 @@ export class MicrosoftAPIsService {
if (
this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')
) {
const calendarChannels = await calendarChannelRepository.find({
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
const calendarChannels =
await this.calendarChannelDataAccessService.find(workspaceId, {
where: {
connectedAccountId: newOrExistingConnectedAccountId,
},
});
for (const calendarChannel of calendarChannels) {
if (
@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.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 ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
export type UpdateConnectedAccountOnReconnectInput = {
workspaceId: string;
@@ -11,7 +11,6 @@ export type UpdateConnectedAccountOnReconnectInput = {
accessToken: string;
refreshToken: string;
scopes: string[];
connectedAccount: ConnectedAccountWorkspaceEntity;
manager: WorkspaceEntityManager;
};
@@ -19,6 +18,7 @@ export type UpdateConnectedAccountOnReconnectInput = {
export class UpdateConnectedAccountOnReconnectService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
) {}
async updateConnectedAccountOnReconnect(
@@ -30,19 +30,13 @@ export class UpdateConnectedAccountOnReconnectService {
accessToken,
refreshToken,
scopes,
manager,
} = input;
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
await connectedAccountRepository.update(
await this.connectedAccountDataAccessService.update(
workspaceId,
{
id: connectedAccountId,
},
@@ -52,7 +46,6 @@ export class UpdateConnectedAccountOnReconnectService {
scopes,
authFailedAt: null,
},
manager,
);
}, authContext);
}
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ImapSmtpCaldavValidatorModule } from 'src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection-validator.module';
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
@@ -21,6 +22,7 @@ import { ImapSmtpCaldavService } from './services/imap-smtp-caldav-connection.se
IMAPAPIsModule,
MessagingImportManagerModule,
MessageQueueModule,
ConnectedAccountDataAccessModule,
TwentyORMModule,
FeatureFlagModule,
ImapSmtpCaldavValidatorModule,
@@ -6,6 +6,7 @@ import { createTransport } from 'nodemailer';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { UserInputError } 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 {
type AccountType,
type ConnectionParameters,
@@ -21,6 +22,7 @@ export class ImapSmtpCaldavService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
) {}
async testImapConnection(
@@ -188,20 +190,15 @@ export class ImapSmtpCaldavService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: {
id: connectionId,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
},
});
const connectedAccount = await connectedAccountRepository.findOne({
where: {
id: connectionId,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
},
});
return connectedAccount;
return connectedAccount as ConnectedAccountWorkspaceEntity | null;
},
authContext,
);
@@ -15,6 +15,7 @@ import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool
import { NavigateAppTool } from 'src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool';
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
@@ -35,6 +36,7 @@ import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbou
ViewModule,
NavigationMenuItemModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ConnectedAccountDataAccessModule,
],
providers: [
HttpTool,
@@ -20,11 +20,12 @@ import { EmailComposerResult } from 'src/engine/core-modules/tool/tools/email-to
import { EmailToolInput } from 'src/engine/core-modules/tool/tools/email-tool/types/email-tool-input.type';
import { parseCommaSeparatedEmails } from 'src/engine/core-modules/tool/tools/email-tool/utils/parse-comma-separated-emails.util';
import { ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool.type';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-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 { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessagingAccountAuthenticationService } from 'src/modules/messaging/message-import-manager/services/messaging-account-authentication.service';
import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { parseEmailBody } from 'src/utils/parse-email-body';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@Injectable()
@@ -33,6 +34,7 @@ export class EmailComposerService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
@@ -54,20 +56,15 @@ export class EmailComposerService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
relations: {
messageChannels: {
messageFolders: true,
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { id: connectedAccountId },
relations: {
messageChannels: {
messageFolders: true,
},
},
},
});
});
if (!isDefined(connectedAccount)) {
throw new EmailToolException(
@@ -76,7 +73,7 @@ export class EmailComposerService {
);
}
return connectedAccount;
return connectedAccount as unknown as ConnectedAccountWorkspaceEntity;
},
authContext,
);
@@ -89,12 +86,8 @@ export class EmailComposerService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const connectedAccountRepository =
await this.globalWorkspaceOrmManager.getRepository<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const allAccounts = await connectedAccountRepository.find();
const allAccounts =
await this.connectedAccountDataAccessService.find(workspaceId);
if (!allAccounts || allAccounts.length === 0) {
throw new EmailToolException(
@@ -291,7 +284,7 @@ export class EmailComposerService {
...connectedAccount,
accessToken,
refreshToken,
};
} as unknown as ConnectedAccountWorkspaceEntity;
const attachments = await this.getAttachments(files || [], workspaceId);